diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index e5b405fb60a..c6c9d1f49cc 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -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; description?: Maybe; + displayDataLabel: Scalars['Boolean']; filter?: Maybe; graphType: GraphType; groupByFieldMetadataIdX: Scalars['UUID']; @@ -285,8 +295,6 @@ export type BarChartConfiguration = { orderByY?: Maybe; rangeMax?: Maybe; rangeMin?: Maybe; - xAxisName?: Maybe; - yAxisName?: Maybe; }; 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; description?: Maybe; + displayDataLabel: Scalars['Boolean']; filter?: Maybe; 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; description?: Maybe; + displayDataLabel: Scalars['Boolean']; filter?: Maybe; graphType: GraphType; groupByFieldMetadataIdX: Scalars['UUID']; @@ -1579,8 +1606,6 @@ export type LineChartConfiguration = { orderByY?: Maybe; rangeMax?: Maybe; rangeMin?: Maybe; - xAxisName?: Maybe; - yAxisName?: Maybe; }; export type LinkMetadata = { @@ -2675,9 +2700,10 @@ export type NativeModelCapabilities = { export type NumberChartConfiguration = { __typename?: 'NumberChartConfiguration'; aggregateFieldMetadataId: Scalars['UUID']; - aggregateOperation: AggregateOperations; + aggregateOperation: ExtendedAggregateOperations; color?: Maybe; description?: Maybe; + displayDataLabel: Scalars['Boolean']; filter?: Maybe; format?: Maybe; graphType: GraphType; @@ -2919,9 +2945,10 @@ export enum PermissionFlagType { export type PieChartConfiguration = { __typename?: 'PieChartConfiguration'; aggregateFieldMetadataId: Scalars['UUID']; - aggregateOperation: AggregateOperations; + aggregateOperation: ExtendedAggregateOperations; color?: Maybe; description?: Maybe; + displayDataLabel: Scalars['Boolean']; filter?: Maybe; graphType: GraphType; groupByFieldMetadataId: Scalars['UUID']; diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index e42ef690c26..2b2e70cecf0 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -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; description?: Maybe; + displayDataLabel: Scalars['Boolean']; filter?: Maybe; graphType: GraphType; groupByFieldMetadataIdX: Scalars['UUID']; @@ -285,8 +295,6 @@ export type BarChartConfiguration = { orderByY?: Maybe; rangeMax?: Maybe; rangeMin?: Maybe; - xAxisName?: Maybe; - yAxisName?: Maybe; }; 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; description?: Maybe; + displayDataLabel: Scalars['Boolean']; filter?: Maybe; 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; description?: Maybe; + displayDataLabel: Scalars['Boolean']; filter?: Maybe; graphType: GraphType; groupByFieldMetadataIdX: Scalars['UUID']; @@ -1529,8 +1556,6 @@ export type LineChartConfiguration = { orderByY?: Maybe; rangeMax?: Maybe; rangeMin?: Maybe; - xAxisName?: Maybe; - yAxisName?: Maybe; }; export type LinkMetadata = { @@ -2561,9 +2586,10 @@ export type NativeModelCapabilities = { export type NumberChartConfiguration = { __typename?: 'NumberChartConfiguration'; aggregateFieldMetadataId: Scalars['UUID']; - aggregateOperation: AggregateOperations; + aggregateOperation: ExtendedAggregateOperations; color?: Maybe; description?: Maybe; + displayDataLabel: Scalars['Boolean']; filter?: Maybe; format?: Maybe; graphType: GraphType; @@ -2805,9 +2831,10 @@ export enum PermissionFlagType { export type PieChartConfiguration = { __typename?: 'PieChartConfiguration'; aggregateFieldMetadataId: Scalars['UUID']; - aggregateOperation: AggregateOperations; + aggregateOperation: ExtendedAggregateOperations; color?: Maybe; description?: Maybe; + displayDataLabel: Scalars['Boolean']; filter?: Maybe; 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 } diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuItem.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuItem.tsx index 691a3f95181..5ddffd22b13 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuItem.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuItem.tsx @@ -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} /> ); }; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuItemDropdown.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuItemDropdown.tsx new file mode 100644 index 00000000000..31d8bd4426e --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuItemDropdown.tsx @@ -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 ( + + } + dropdownComponents={dropdownComponents} + dropdownId={dropdownId} + dropdownPlacement={dropdownPlacement} + dropdownOffset={dropdownOffset} + disableClickForClickableComponent={disabled} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuItemToggle.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuItemToggle.tsx new file mode 100644 index 00000000000..20d810af705 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuItemToggle.tsx @@ -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 ( + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuList.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuList.tsx index 3f6a3defcc3..8f8b568e206 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuList.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuList.tsx @@ -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) { diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepHeader.tsx b/packages/twenty-front/src/modules/command-menu/components/SidePanelHeader.tsx similarity index 93% rename from packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepHeader.tsx rename to packages/twenty-front/src/modules/command-menu/components/SidePanelHeader.tsx index 6a8e8984af3..5772f2f5c2d 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepHeader.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/SidePanelHeader.tsx @@ -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 ( - + = { - title: 'Modules/Workflow/WorkflowStepHeader', - component: WorkflowStepHeader, +const meta: Meta = { + title: 'Modules/CommandMenu/SidePanelHeader', + component: SidePanelHeader, args: { onTitleChange: fn(), }, @@ -20,7 +20,7 @@ const meta: Meta = { export default meta; -type Story = StoryObj; +type Story = StoryObj; 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(); }, }; diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup.ts index 6b598b28187..a4d3685c68c 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup.ts @@ -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, { diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx new file mode 100644 index 00000000000..7068d5e3821 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx @@ -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 ( + group.items.map((item) => item.id)), + ]} + > + + {chartSettings.map((group) => ( + + {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 ? ( + + + + ) : ( + + + + {getChartSettingsDropdownContent(item.id)} + + + } + dropdownPlacement="bottom-end" + description={getChartSettingsValues(item.id) as string} + contextualTextPosition={'right'} + hasSubMenu + disabled={isDisabled} + /> + + ); + })} + + ))} + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartTypeSelectionSection.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartTypeSelectionSection.tsx new file mode 100644 index 00000000000..3a2b02a2acf --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartTypeSelectionSection.tsx @@ -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 ( + + {graphTypeOptions.map((graphType) => ( + { + setCurrentGraphType(graphType); + }} + label={t(GRAPH_TYPE_INFORMATION[graphType].label)} + showLabel={false} + /> + ))} + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx index 914b6a0e348..812df460837 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx @@ -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 ( - - Graph type + <> + {}} + /> - {graphTypeOptions.map((option) => ( - handleSelectGraphType(option.type)} - /> - ))} - + + ); }; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeConfig.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeConfig.tsx index 18b61b2f2a5..d4534357058 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeConfig.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeConfig.tsx @@ -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 ( - {isEditMode ? 'Edit iFrame Widget' : 'Configure iFrame Widget'} + {isEditMode ? t`Edit iFrame Widget` : t`Configure iFrame Widget`} diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect.tsx index 8116cb677a2..764d49fcbd9 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect.tsx @@ -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 ( - - Widget type - {widgetTypeOptions.map((option) => { - const MenuItemComponent = ( - handleSelectWidget(option.type)} + + + + - ); - - return option.disabled ? ( - - {MenuItemComponent} - - ) : ( - MenuItemComponent - ); - })} - + + { + handleNavigateToIframeConfig(); + }} + > + + + + ); }; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartAggregateOperationSelectionDropdownContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartAggregateOperationSelectionDropdownContent.tsx new file mode 100644 index 00000000000..562c3e7843e --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartAggregateOperationSelectionDropdownContent.tsx @@ -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 ( + <> + setIsSubMenuOpen(false)} + Icon={IconChevronLeft} + /> + } + > + Y-Axis Aggregate Operation + + setSearchQuery(event.target.value)} + value={searchQuery} + /> + + + item.operation, + )} + > + {filteredAggregateOperationsWithLabels.map((item) => ( + { + handleSelectAggregateOperation(item.operation); + }} + > + { + handleSelectAggregateOperation(item.operation); + }} + /> + + ))} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartAxisNameSelectionDropdownContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartAxisNameSelectionDropdownContent.tsx new file mode 100644 index 00000000000..9d85a66b576 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartAxisNameSelectionDropdownContent.tsx @@ -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 ( + <> + + Axis Name + + + + {axisOptions.map((option) => ( + { + handleSelectAxisNameOption(option); + }} + > + { + handleSelectAxisNameOption(option); + }} + /> + + ))} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartColorSelectionDropdownContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartColorSelectionDropdownContent.tsx new file mode 100644 index 00000000000..9e59be4caa9 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartColorSelectionDropdownContent.tsx @@ -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 ( + <> + + Color + + setSearchQuery(event.target.value)} + value={searchQuery} + /> + + colorOption.id, + )} + > + {filteredColorOptions.map((colorOption) => ( + { + handleSelectColor(colorOption.colorName); + }} + > + ( + + )} + onClick={() => { + handleSelectColor(colorOption.colorName); + }} + /> + + ))} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartDataSourceDropdownContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartDataSourceDropdownContent.tsx new file mode 100644 index 00000000000..f8d628d3973 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartDataSourceDropdownContent.tsx @@ -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 ( + <> + + Source + + setSearchQuery(event.target.value)} + value={searchQuery} + /> + + + objectMetadataItem.id, + )} + > + {availableObjectMetadataItems.map((objectMetadataItem) => ( + { + handleSelectSource(objectMetadataItem.id); + }} + > + { + handleSelectSource(objectMetadataItem.id); + }} + /> + + ))} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartFieldSelectionDropdownContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartFieldSelectionDropdownContent.tsx new file mode 100644 index 00000000000..1983313d614 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartFieldSelectionDropdownContent.tsx @@ -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 ( + <> + + X-Axis Field + + setSearchQuery(event.target.value)} + value={searchQuery} + /> + + + item.id, + )} + > + {availableFieldMetadataItems.map((fieldMetadataItem) => ( + { + handleSelectField(fieldMetadataItem.id); + }} + > + { + handleSelectField(fieldMetadataItem.id); + }} + /> + + ))} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartFieldSelectionForAggregateOperationDropdownContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartFieldSelectionForAggregateOperationDropdownContent.tsx new file mode 100644 index 00000000000..e3c2d0440f5 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartFieldSelectionForAggregateOperationDropdownContent.tsx @@ -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 ( + + ); + } + + return ( + <> + + Y-Axis Field + + setSearchQuery(event.target.value)} + value={searchQuery} + /> + + + item.id, + )} + > + {availableFieldMetadataItems.map((fieldMetadataItem) => ( + { + setIsSubMenuOpen(true); + setSelectedFieldMetadataId(fieldMetadataItem.id); + }} + > + { + setIsSubMenuOpen(true); + setSelectedFieldMetadataId(fieldMetadataItem.id); + }} + /> + + ))} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionDropdownContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionDropdownContent.tsx new file mode 100644 index 00000000000..643f068d0cb --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionDropdownContent.tsx @@ -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 ( + <> + + Y-Axis Group By Field + + setSearchQuery(event.target.value)} + value={searchQuery} + /> + + + item.id, + )} + > + {availableFieldMetadataItems.map((fieldMetadataItem) => ( + { + handleSelectField(fieldMetadataItem.id); + }} + > + { + handleSelectField(fieldMetadataItem.id); + }} + /> + + ))} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartSortByGroupByFieldDropdownContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartSortByGroupByFieldDropdownContent.tsx new file mode 100644 index 00000000000..39a3ac7d38d --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartSortByGroupByFieldDropdownContent.tsx @@ -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 ( + <> + {title} + + option.value, + )} + > + {AGGREGATE_SORT_BY_OPTIONS.map((sortOption) => ( + { + handleSelectSortOption(sortOption.value); + }} + > + { + handleSelectSortOption(sortOption.value); + }} + /> + + ))} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartXAxisSortBySelectionDropdownContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartXAxisSortBySelectionDropdownContent.tsx new file mode 100644 index 00000000000..628dd070361 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartXAxisSortBySelectionDropdownContent.tsx @@ -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 ( + <> + + X-Axis Sort By + + + option.value, + )} + > + {X_SORT_BY_OPTIONS.map((sortOption) => ( + { + handleSelectSortOption(sortOption.value); + }} + > + { + handleSelectSortOption(sortOption.value); + }} + /> + + ))} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/AggregateSortByOptions.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/AggregateSortByOptions.ts new file mode 100644 index 00000000000..4ebd6824d7d --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/AggregateSortByOptions.ts @@ -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, + }, +]; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/BarChartSettings.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/BarChartSettings.ts new file mode 100644 index 00000000000..d79a3a7206d --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/BarChartSettings.ts @@ -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], + }, +]; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/GaugeChartSettings.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/GaugeChartSettings.ts new file mode 100644 index 00000000000..0f256b657ab --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/GaugeChartSettings.ts @@ -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], + }, +]; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/GraphTypeInformation.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/GraphTypeInformation.ts new file mode 100644 index 00000000000..025bd382c53 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/GraphTypeInformation.ts @@ -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, + }, +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/GraphTypeToConfigTypename.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/GraphTypeToConfigTypename.ts new file mode 100644 index 00000000000..33121c2ae45 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/GraphTypeToConfigTypename.ts @@ -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; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/LineChartSettings.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/LineChartSettings.ts new file mode 100644 index 00000000000..d4543d8ccae --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/LineChartSettings.ts @@ -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], + }, +]; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/NumberChartSettings.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/NumberChartSettings.ts new file mode 100644 index 00000000000..34a46017258 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/NumberChartSettings.ts @@ -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], + }, +]; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/PieChartSettings.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/PieChartSettings.ts new file mode 100644 index 00000000000..5d9af574d49 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/PieChartSettings.ts @@ -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], + }, +]; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/XSortByOptions.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/XSortByOptions.ts new file mode 100644 index 00000000000..37523423ff4 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/XSortByOptions.ts @@ -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, + }, +]; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/AxisNameSetting.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/AxisNameSetting.ts new file mode 100644 index 00000000000..847c42cf2a0 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/AxisNameSetting.ts @@ -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, +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels.ts new file mode 100644 index 00000000000..b3b606f3f4c --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels.ts @@ -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`, +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/ChartDataSourceSetting.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/ChartDataSourceSetting.ts new file mode 100644 index 00000000000..734ffa61251 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/ChartDataSourceSetting.ts @@ -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, +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/ColorsSetting.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/ColorsSetting.ts new file mode 100644 index 00000000000..6988b0804f7 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/ColorsSetting.ts @@ -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, +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DataDisplayPieChartSetting.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DataDisplayPieChartSetting.ts new file mode 100644 index 00000000000..695033a90ec --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DataDisplayPieChartSetting.ts @@ -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], +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DataDisplayXSetting.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DataDisplayXSetting.ts new file mode 100644 index 00000000000..d61e8ffeb98 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DataDisplayXSetting.ts @@ -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], +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DataDisplayYSetting.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DataDisplayYSetting.ts new file mode 100644 index 00000000000..d0bb10269cc --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DataDisplayYSetting.ts @@ -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], +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DataLabelsSetting.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DataLabelsSetting.ts new file mode 100644 index 00000000000..a9b92256dfc --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/DataLabelsSetting.ts @@ -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, +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/EachSliceRepresentsSetting.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/EachSliceRepresentsSetting.ts new file mode 100644 index 00000000000..3e745706ea9 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/EachSliceRepresentsSetting.ts @@ -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, +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/FilterSetting.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/FilterSetting.ts new file mode 100644 index 00000000000..4597ea79ac9 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/FilterSetting.ts @@ -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], +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/GroupBySetting.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/GroupBySetting.ts new file mode 100644 index 00000000000..0f8caae90d0 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/GroupBySetting.ts @@ -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], +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/SortByGroupByFieldSetting.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/SortByGroupByFieldSetting.ts new file mode 100644 index 00000000000..5cb95057dc5 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/SortByGroupByFieldSetting.ts @@ -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, +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/SortByXSetting.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/SortByXSetting.ts new file mode 100644 index 00000000000..258df0caafe --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/SortByXSetting.ts @@ -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, +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useChartSettingsValues.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useChartSettingsValues.ts new file mode 100644 index 00000000000..3b87490d77b --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useChartSettingsValues.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useGraphGroupBySortOptionLabels.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useGraphGroupBySortOptionLabels.ts new file mode 100644 index 00000000000..fc433f9afdb --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useGraphGroupBySortOptionLabels.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useGraphXSortOptionLabels.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useGraphXSortOptionLabels.ts new file mode 100644 index 00000000000..cefed01a8f2 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useGraphXSortOptionLabels.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig.ts new file mode 100644 index 00000000000..6b5e065d2fc --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig.ts @@ -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; + }) => { + 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 }; +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useWidgetInEditMode.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useWidgetInEditMode.ts new file mode 100644 index 00000000000..614de08460d --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useWidgetInEditMode.ts @@ -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 }; +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartConfiguration.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartConfiguration.ts new file mode 100644 index 00000000000..c4336634fb5 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartConfiguration.ts @@ -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; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartConfigurationSettingIds.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartConfigurationSettingIds.ts new file mode 100644 index 00000000000..5ddd62d3c22 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartConfigurationSettingIds.ts @@ -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; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartSettingsGroup.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartSettingsGroup.ts new file mode 100644 index 00000000000..cb054ea70f1 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartSettingsGroup.ts @@ -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[]; +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getChartAxisNameDisplayOptions.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getChartAxisNameDisplayOptions.ts new file mode 100644 index 00000000000..6aed2c464ba --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getChartAxisNameDisplayOptions.ts @@ -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); + } +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getChartSettingsDropdownContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getChartSettingsDropdownContent.tsx new file mode 100644 index 00000000000..d6fc3c7ad03 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getChartSettingsDropdownContent.tsx @@ -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 ; + case CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_X: + return ; + case CHART_CONFIGURATION_SETTING_IDS.SORT_BY_X: + return ; + case CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_Y: + return ; + case CHART_CONFIGURATION_SETTING_IDS.SORT_BY_GROUP_BY_FIELD: + return ( + + ); + case CHART_CONFIGURATION_SETTING_IDS.GROUP_BY: + return ; + case CHART_CONFIGURATION_SETTING_IDS.COLORS: + return ; + case CHART_CONFIGURATION_SETTING_IDS.AXIS_NAME: + return ; + default: + return
Configuration options will be implemented here
; + } +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/mapToGraphQLExtendedAggregateOperation.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/mapToGraphQLExtendedAggregateOperation.ts new file mode 100644 index 00000000000..f7cd3d91fdf --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/mapToGraphQLExtendedAggregateOperation.ts @@ -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]; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/getAggregateOperationLabel.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/getAggregateOperationLabel.ts index e7e96809e38..6af0747836c 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/getAggregateOperationLabel.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/getAggregateOperationLabel.ts @@ -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: diff --git a/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutRenderer.stories.tsx b/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutRenderer.stories.tsx index 3993b83b8c8..b30c235027c 100644 --- a/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutRenderer.stories.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutRenderer.stories.tsx @@ -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, diff --git a/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts b/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts index a87258a9ad7..f418af22738 100644 --- a/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts +++ b/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts @@ -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 } diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutGraphWidget.test.tsx b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutGraphWidget.test.tsx index c89e4d149a4..a75a54ef3da 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutGraphWidget.test.tsx +++ b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutGraphWidget.test.tsx @@ -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'); }); }); diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutDraftState.test.tsx b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutDraftState.test.tsx index 9209e36007e..5e3a8e7eb11 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutDraftState.test.tsx +++ b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutDraftState.test.tsx @@ -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(), diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts index fe727c733fd..d8456664a87 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts @@ -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 }; }; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useEditPageLayoutWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useEditPageLayoutWidget.ts index d50ac1339b7..da29cfa2363 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useEditPageLayoutWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useEditPageLayoutWidget.ts @@ -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], ); diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/addWidgetToTab.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/addWidgetToTab.test.ts index 232a521667e..8d24f76f230 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/__tests__/addWidgetToTab.test.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/addWidgetToTab.test.ts @@ -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, diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertLayoutsToWidgets.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertLayoutsToWidgets.test.ts index bb5be3e45e3..44ebfbf3d3a 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertLayoutsToWidgets.test.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertLayoutsToWidgets.test.ts @@ -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', diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertPageLayoutToTabLayouts.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertPageLayoutToTabLayouts.test.ts index b2c5673e1b9..06caca48fc7 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertPageLayoutToTabLayouts.test.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertPageLayoutToTabLayouts.test.ts @@ -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', diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/extractFieldMetadataIdsFromWidget.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/extractFieldMetadataIdsFromWidget.test.ts index 3b1886e146b..ec374d2e551 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/__tests__/extractFieldMetadataIdsFromWidget.test.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/extractFieldMetadataIdsFromWidget.test.ts @@ -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', }); diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/removeWidgetFromTab.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/removeWidgetFromTab.test.ts index 20669c92400..31165ae0a2c 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/__tests__/removeWidgetFromTab.test.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/removeWidgetFromTab.test.ts @@ -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, diff --git a/packages/twenty-front/src/modules/page-layout/utils/createDefaultGraphWidget.ts b/packages/twenty-front/src/modules/page-layout/utils/createDefaultGraphWidget.ts index a12ee541b99..f39e60245d8 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/createDefaultGraphWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/createDefaultGraphWidget.ts @@ -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, diff --git a/packages/twenty-front/src/modules/page-layout/utils/extractFieldMetadataIdsFromWidget.ts b/packages/twenty-front/src/modules/page-layout/utils/extractFieldMetadataIdsFromWidget.ts index e64868e31cd..77ef562ce11 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/extractFieldMetadataIdsFromWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/extractFieldMetadataIdsFromWidget.ts @@ -31,10 +31,7 @@ export const extractFieldMetadataIdsFromWidget = ( return [config.aggregateFieldMetadataId]; case 'GaugeChartConfiguration': - return [ - config.aggregateFieldMetadataId, - config.aggregateFieldMetadataIdTotal, - ]; + return [config.aggregateFieldMetadataId]; case 'IframeConfiguration': return []; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx index c1ee47153cd..7ce50f79cd7 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx @@ -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 = { title: 'Modules/PageLayout/Widgets/WidgetRenderer', @@ -30,18 +30,18 @@ type Story = StoryObj; 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) => (
@@ -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) => (
@@ -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) => (
@@ -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: { diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepInputDetail.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepInputDetail.tsx index 008693edc7a..cdd7af955a6 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepInputDetail.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepInputDetail.tsx @@ -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 ( <> - { return ( <> - ) : ( <> - { if (actionOptions.readonly === true) { return; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx index faec8029bf7..8cedc90610b 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx @@ -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 } /> - { updateAction({ name: newName }); }} diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowReadonlyActionServerlessFunction.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowReadonlyActionServerlessFunction.tsx index 4013f6ff31c..5ba1341bf0c 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowReadonlyActionServerlessFunction.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowReadonlyActionServerlessFunction.tsx @@ -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 ( <> - - { if (actionOptions.readonly === true) { return; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord.tsx index 0a68b7fb946..f461943dee5 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord.tsx @@ -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 ( <> - { if (actionOptions.readonly === true) { return; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmpty.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmpty.tsx index 865667882cd..c2a058af66d 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmpty.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmpty.tsx @@ -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 ( <> - { if (actionOptions.readonly === true) { return; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail.tsx index e96c111332c..0b2fffa7c96 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail.tsx @@ -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 && ( <> - { if (actionOptions.readonly === true) { return; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord.tsx index 9c387aab95a..f84100212a1 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord.tsx @@ -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 ( <> - { if (actionOptions.readonly === true) { return; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter.tsx index e29f0c95947..d95b8386137 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter.tsx @@ -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 ( <> - { if (actionOptions.readonly === true) { return; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords.tsx index 544681d2002..66ca5d1c6d2 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords.tsx @@ -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 ( <> - { if (actionOptions.readonly === true) { return; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder.tsx index 0a4d8aeebb6..4df5ed95e33 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder.tsx @@ -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 ( <> - { if (actionOptions.readonly === true) { return; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFiller.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFiller.tsx index f5a28292b41..9faa2a3712c 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFiller.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFiller.tsx @@ -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 ( <> - - { if (actionOptions.readonly === true) { return; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/http-request-action/components/__stories__/WorkflowEditActionHttpRequest.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/http-request-action/components/__stories__/WorkflowEditActionHttpRequest.stories.tsx index f6f6ccabfde..de6f03c16eb 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/http-request-action/components/__stories__/WorkflowEditActionHttpRequest.stories.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/http-request-action/components/__stories__/WorkflowEditActionHttpRequest.stories.tsx @@ -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(); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator.tsx index 71bb02d0ca0..ea4da14c4d6 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator.tsx @@ -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 ( <> - - { if (triggerOptions.readonly === true) { return; diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm.tsx b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm.tsx index c184771891e..a20aeca73db 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm.tsx @@ -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 ( <> - { if (triggerOptions.readonly === true) { return; diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerManual.tsx b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerManual.tsx index 5d21ffe5ff4..93b0b17e37b 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerManual.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerManual.tsx @@ -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 ( <> - { if (triggerOptions.readonly === true) { return; diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerManualDeprecated.tsx b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerManualDeprecated.tsx index 446db6a32b6..846573c988a 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerManualDeprecated.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerManualDeprecated.tsx @@ -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 ( <> - { if (triggerOptions.readonly === true) { return; diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx index 512b708f23b..e93fbbdc8f5 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx @@ -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 ( <> - { if (triggerOptions.readonly === true) { return; diff --git a/packages/twenty-front/src/utils/__tests__/filterBySearchQuery.test.ts b/packages/twenty-front/src/utils/__tests__/filterBySearchQuery.test.ts new file mode 100644 index 00000000000..d309436f45d --- /dev/null +++ b/packages/twenty-front/src/utils/__tests__/filterBySearchQuery.test.ts @@ -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]]); + }); +}); diff --git a/packages/twenty-front/src/utils/filterBySearchQuery.ts b/packages/twenty-front/src/utils/filterBySearchQuery.ts new file mode 100644 index 00000000000..9d413f4b5f0 --- /dev/null +++ b/packages/twenty-front/src/utils/filterBySearchQuery.ts @@ -0,0 +1,28 @@ +import { isNonEmptyString } from '@sniptt/guards'; + +import { normalizeSearchText } from './normalizeSearchText'; + +type FilterBySearchQueryParams = { + items: T[]; + searchQuery: string; + getSearchableValues: (item: T) => string[]; +}; + +export const filterBySearchQuery = ({ + items, + searchQuery, + getSearchableValues, +}: FilterBySearchQueryParams): 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), + ); + }); +}; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/extended-aggregate-operations.constant.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/extended-aggregate-operations.constant.ts new file mode 100644 index 00000000000..b8b81e390fb --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/extended-aggregate-operations.constant.ts @@ -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', +}); diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto.ts index 76b519edbb1..42a83fa012b 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto.ts @@ -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() diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/gauge-chart-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/gauge-chart-configuration.dto.ts index 022d93a5cd1..366dcb08e86 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/gauge-chart-configuration.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/gauge-chart-configuration.dto.ts @@ -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() diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto.ts index 516df0eee4f..d4f6f4d52da 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto.ts @@ -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() diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/number-chart-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/number-chart-configuration.dto.ts index 5458e611c72..de54ea03b6c 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/number-chart-configuration.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/number-chart-configuration.dto.ts @@ -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() diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto.ts index f699efa5453..141db21f125 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto.ts @@ -1,6 +1,7 @@ import { Field, ObjectType } from '@nestjs/graphql'; import { + IsBoolean, IsEnum, IsNotEmpty, IsObject, @@ -12,10 +13,10 @@ 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 { 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('PieChartConfiguration') export class PieChartConfigurationDTO { @@ -29,10 +30,10 @@ export class PieChartConfigurationDTO { @IsNotEmpty() aggregateFieldMetadataId: string; - @Field(() => AggregateOperations) - @IsEnum(AggregateOperations) + @Field(() => ExtendedAggregateOperations) + @IsEnum(ExtendedAggregateOperations) @IsNotEmpty() - aggregateOperation: AggregateOperations; + aggregateOperation: ExtendedAggregateOperations; @Field(() => UUIDScalarType) @IsUUID() @@ -44,6 +45,11 @@ export class PieChartConfigurationDTO { @IsNotEmpty() orderBy: GraphOrderBy; + @Field(() => Boolean) + @IsBoolean() + @IsNotEmpty() + displayDataLabel: boolean; + @Field(() => String, { nullable: true }) @IsString() @IsOptional() diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/enums/axis-name-display.enum.ts b/packages/twenty-server/src/engine/core-modules/page-layout/enums/axis-name-display.enum.ts new file mode 100644 index 00000000000..caf647bf4db --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/page-layout/enums/axis-name-display.enum.ts @@ -0,0 +1,13 @@ +import { registerEnumType } from '@nestjs/graphql'; + +export enum AxisNameDisplay { + NONE = 'NONE', + X = 'X', + Y = 'Y', + BOTH = 'BOTH', +} + +registerEnumType(AxisNameDisplay, { + name: 'AxisNameDisplay', + description: 'Which axes should display labels', +}); diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util.ts index 8d16b657ce7..d28b88d9c2d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util.ts @@ -1,6 +1,7 @@ import { isDefined } from 'twenty-shared/utils'; import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; +import { AxisNameDisplay } from 'src/engine/core-modules/page-layout/enums/axis-name-display.enum'; import { WidgetType } from 'src/engine/core-modules/page-layout/enums/widget-type.enum'; import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { PAGE_LAYOUT_TAB_SEEDS } from 'src/engine/workspace-manager/dev-seeder/core/constants/page-layout-tab-seeds.constant'; @@ -94,6 +95,7 @@ export const getPageLayoutWidgetDataSeeds = ( graphType: 'NUMBER', aggregateFieldMetadataId: opportunityAmountFieldId, aggregateOperation: AggregateOperations.SUM, + displayDataLabel: true, } : null, objectMetadataId: opportunityObject?.id ?? null, @@ -115,6 +117,7 @@ export const getPageLayoutWidgetDataSeeds = ( graphType: 'NUMBER', aggregateFieldMetadataId: rocketIdFieldId, aggregateOperation: AggregateOperations.COUNT, + displayDataLabel: true, } : null, objectMetadataId: rocketObject?.id ?? null, @@ -140,6 +143,8 @@ export const getPageLayoutWidgetDataSeeds = ( aggregateOperation: AggregateOperations.SUM, groupByFieldMetadataIdX: opportunityCloseDateFieldId, orderByX: 'FIELD_ASC', + axisNameDisplay: AxisNameDisplay.NONE, + displayDataLabel: false, } : null, objectMetadataId: opportunityObject?.id ?? null, @@ -165,6 +170,8 @@ export const getPageLayoutWidgetDataSeeds = ( aggregateOperation: AggregateOperations.SUM, groupByFieldMetadataIdX: opportunityStageFieldId, orderByX: 'FIELD_DESC', + axisNameDisplay: AxisNameDisplay.NONE, + displayDataLabel: false, } : null, objectMetadataId: opportunityObject?.id ?? null, @@ -191,6 +198,8 @@ export const getPageLayoutWidgetDataSeeds = ( aggregateOperation: AggregateOperations.COUNT, groupByFieldMetadataIdX: rocketCreatedAtFieldId, orderByX: 'FIELD_ASC', + axisNameDisplay: AxisNameDisplay.NONE, + displayDataLabel: false, } : null, objectMetadataId: rocketObject?.id ?? null, @@ -212,6 +221,7 @@ export const getPageLayoutWidgetDataSeeds = ( graphType: 'NUMBER', aggregateFieldMetadataId: opportunityIdFieldId, aggregateOperation: AggregateOperations.COUNT, + displayDataLabel: true, } : null, objectMetadataId: opportunityObject?.id ?? null, @@ -235,6 +245,7 @@ export const getPageLayoutWidgetDataSeeds = ( graphType: 'NUMBER', aggregateFieldMetadataId: companyIdFieldId, aggregateOperation: AggregateOperations.COUNT, + displayDataLabel: true, } : null, objectMetadataId: companyObject?.id ?? null, @@ -259,6 +270,8 @@ export const getPageLayoutWidgetDataSeeds = ( aggregateOperation: AggregateOperations.COUNT, groupByFieldMetadataIdX: companyCreatedAtFieldId, orderByX: 'FIELD_ASC', + axisNameDisplay: AxisNameDisplay.NONE, + displayDataLabel: false, } : null, objectMetadataId: companyObject?.id ?? null, @@ -283,6 +296,8 @@ export const getPageLayoutWidgetDataSeeds = ( aggregateOperation: AggregateOperations.COUNT, groupByFieldMetadataIdX: companyEmployeesFieldId, orderByX: 'FIELD_ASC', + axisNameDisplay: AxisNameDisplay.NONE, + displayDataLabel: false, } : null, objectMetadataId: companyObject?.id ?? null, @@ -306,6 +321,7 @@ export const getPageLayoutWidgetDataSeeds = ( graphType: 'NUMBER', aggregateFieldMetadataId: companyArrFieldId, aggregateOperation: AggregateOperations.SUM, + displayDataLabel: true, } : null, objectMetadataId: companyObject?.id ?? null, @@ -330,6 +346,7 @@ export const getPageLayoutWidgetDataSeeds = ( aggregateOperation: AggregateOperations.SUM, groupByFieldMetadataId: companyNameFieldId, orderBy: 'VALUE_DESC', + displayDataLabel: true, } : null, objectMetadataId: companyObject?.id ?? null, @@ -351,8 +368,7 @@ export const getPageLayoutWidgetDataSeeds = ( graphType: 'GAUGE', aggregateFieldMetadataId: companyArrFieldId, aggregateOperation: AggregateOperations.AVG, - aggregateFieldMetadataIdTotal: companyArrFieldId, - aggregateOperationTotal: AggregateOperations.MAX, + displayDataLabel: true, } : null, objectMetadataId: companyObject?.id ?? null, @@ -374,6 +390,7 @@ export const getPageLayoutWidgetDataSeeds = ( graphType: 'NUMBER', aggregateFieldMetadataId: companyLinkedinLinkFieldId, aggregateOperation: AggregateOperations.COUNT, + displayDataLabel: true, } : null, objectMetadataId: companyObject?.id ?? null, @@ -398,6 +415,7 @@ export const getPageLayoutWidgetDataSeeds = ( aggregateOperation: AggregateOperations.COUNT, groupByFieldMetadataId: companyLinkedinLinkFieldId, orderBy: 'VALUE_DESC', + displayDataLabel: true, } : null, objectMetadataId: companyObject?.id ?? null, @@ -418,6 +436,7 @@ export const getPageLayoutWidgetDataSeeds = ( graphType: 'NUMBER', aggregateFieldMetadataId: personIdFieldId, aggregateOperation: AggregateOperations.COUNT, + displayDataLabel: true, } : null, objectMetadataId: personObject?.id ?? null, @@ -442,6 +461,8 @@ export const getPageLayoutWidgetDataSeeds = ( aggregateOperation: AggregateOperations.COUNT, groupByFieldMetadataIdX: personCityFieldId, orderByX: 'FIELD_DESC', + axisNameDisplay: AxisNameDisplay.NONE, + displayDataLabel: false, } : null, objectMetadataId: personObject?.id ?? null, @@ -468,6 +489,7 @@ export const getPageLayoutWidgetDataSeeds = ( aggregateOperation: AggregateOperations.COUNT, groupByFieldMetadataId: personJobTitleFieldId, orderBy: 'VALUE_DESC', + displayDataLabel: true, } : null, objectMetadataId: personObject?.id ?? null, @@ -486,6 +508,7 @@ export const getPageLayoutWidgetDataSeeds = ( graphType: 'NUMBER', aggregateFieldMetadataId: taskIdFieldId, aggregateOperation: AggregateOperations.COUNT, + displayDataLabel: true, } : null, objectMetadataId: taskObject?.id ?? null, diff --git a/packages/twenty-server/test/integration/constants/page-layout-gql-fields.constants.ts b/packages/twenty-server/test/integration/constants/page-layout-gql-fields.constants.ts index c51ecf21942..955327aa111 100644 --- a/packages/twenty-server/test/integration/constants/page-layout-gql-fields.constants.ts +++ b/packages/twenty-server/test/integration/constants/page-layout-gql-fields.constants.ts @@ -31,8 +31,8 @@ export const PAGE_LAYOUT_WIDGET_CONFIGURATION_FIELDS = ` groupByFieldMetadataIdY orderByY omitNullValues - xAxisName - yAxisName + axisNameDisplay + displayDataLabel rangeMin rangeMax filter @@ -48,8 +48,8 @@ export const PAGE_LAYOUT_WIDGET_CONFIGURATION_FIELDS = ` groupByFieldMetadataIdY orderByY omitNullValues - xAxisName - yAxisName + axisNameDisplay + displayDataLabel rangeMin rangeMax filter @@ -62,6 +62,7 @@ export const PAGE_LAYOUT_WIDGET_CONFIGURATION_FIELDS = ` aggregateFieldMetadataId aggregateOperation orderBy + displayDataLabel filter color description @@ -80,8 +81,6 @@ export const PAGE_LAYOUT_WIDGET_CONFIGURATION_FIELDS = ` graphType aggregateFieldMetadataId aggregateOperation - aggregateOperationTotal - aggregateFieldMetadataIdTotal description filter } diff --git a/packages/twenty-server/test/integration/constants/widget-configuration-test-data.constants.ts b/packages/twenty-server/test/integration/constants/widget-configuration-test-data.constants.ts index 2cd1ef42780..8b9a09e34d1 100644 --- a/packages/twenty-server/test/integration/constants/widget-configuration-test-data.constants.ts +++ b/packages/twenty-server/test/integration/constants/widget-configuration-test-data.constants.ts @@ -1,4 +1,5 @@ import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; +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'; @@ -23,12 +24,14 @@ export const TEST_NUMBER_CHART_CONFIG = { description: 'Count of all records', color: 'blue', format: '0,0', + displayDataLabel: true, }; export const TEST_NUMBER_CHART_CONFIG_MINIMAL = { graphType: GraphType.NUMBER, aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1, aggregateOperation: AggregateOperations.SUM, + displayDataLabel: false, }; export const TEST_BAR_CHART_CONFIG = { @@ -37,8 +40,8 @@ export const TEST_BAR_CHART_CONFIG = { aggregateOperation: AggregateOperations.SUM, groupByFieldMetadataIdX: TEST_FIELD_METADATA_ID_2, orderByX: GraphOrderBy.FIELD_ASC, - xAxisName: 'Month', - yAxisName: 'Revenue', + displayDataLabel: true, + axisNameDisplay: AxisNameDisplay.BOTH, color: 'red', description: 'Monthly revenue breakdown', omitNullValues: true, @@ -52,6 +55,8 @@ export const TEST_BAR_CHART_CONFIG_MINIMAL = { aggregateOperation: AggregateOperations.COUNT, groupByFieldMetadataIdX: TEST_FIELD_METADATA_ID_2, orderByX: GraphOrderBy.VALUE_DESC, + displayDataLabel: false, + axisNameDisplay: AxisNameDisplay.NONE, }; export const TEST_LINE_CHART_CONFIG = { @@ -62,8 +67,8 @@ export const TEST_LINE_CHART_CONFIG = { orderByX: GraphOrderBy.FIELD_ASC, groupByFieldMetadataIdY: TEST_FIELD_METADATA_ID_3, orderByY: GraphOrderBy.FIELD_DESC, - xAxisName: 'Date', - yAxisName: 'Average Value', + displayDataLabel: true, + axisNameDisplay: AxisNameDisplay.BOTH, color: 'cyan', description: 'Trend over time', omitNullValues: false, @@ -77,6 +82,8 @@ export const TEST_LINE_CHART_CONFIG_MINIMAL = { aggregateOperation: AggregateOperations.MAX, groupByFieldMetadataIdX: TEST_FIELD_METADATA_ID_2, orderByX: GraphOrderBy.VALUE_ASC, + displayDataLabel: false, + axisNameDisplay: AxisNameDisplay.NONE, }; export const TEST_PIE_CHART_CONFIG = { @@ -85,6 +92,7 @@ export const TEST_PIE_CHART_CONFIG = { aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_2, aggregateOperation: AggregateOperations.SUM, orderBy: GraphOrderBy.VALUE_DESC, + displayDataLabel: true, color: 'yellow', description: 'Distribution by category', filter: { @@ -104,15 +112,15 @@ export const TEST_PIE_CHART_CONFIG_MINIMAL = { aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_2, aggregateOperation: AggregateOperations.COUNT, orderBy: GraphOrderBy.FIELD_ASC, + displayDataLabel: false, }; export const TEST_GAUGE_CHART_CONFIG = { graphType: GraphType.GAUGE, aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1, aggregateOperation: AggregateOperations.SUM, - aggregateOperationTotal: AggregateOperations.COUNT, - aggregateFieldMetadataIdTotal: TEST_FIELD_METADATA_ID_2, description: 'Completion percentage', + displayDataLabel: true, filter: { or: [ { @@ -128,8 +136,7 @@ export const TEST_GAUGE_CHART_CONFIG_MINIMAL = { graphType: GraphType.GAUGE, aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1, aggregateOperation: AggregateOperations.COUNT_TRUE, - aggregateOperationTotal: AggregateOperations.COUNT, - aggregateFieldMetadataIdTotal: TEST_FIELD_METADATA_ID_2, + displayDataLabel: false, }; export const INVALID_IFRAME_CONFIG_BAD_URL = { diff --git a/packages/twenty-shared/src/utils/strings/index.ts b/packages/twenty-shared/src/utils/strings/index.ts index 89859f65b12..399e9198a2c 100644 --- a/packages/twenty-shared/src/utils/strings/index.ts +++ b/packages/twenty-shared/src/utils/strings/index.ts @@ -1 +1,2 @@ export * from './capitalize'; + diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index 40ab2783da1..531f8a4b76c 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -17,10 +17,13 @@ export { IconArrowMerge, IconArrowRight, IconArrowsDiagonal, + IconArrowsSort, IconArrowsVertical, IconArrowUp, IconArrowUpRight, IconAt, + IconAxisX, + IconAxisY, IconBaselineDensitySmall, IconBell, IconBlockquote, @@ -158,6 +161,7 @@ export { IconFilterCog, IconFilterOff, IconFilterPlus, + IconFilters, IconFilterX, IconFlag, IconFlask, @@ -172,6 +176,7 @@ export { IconGauge, IconGitBranchDeleted, IconGitCommit, + IconGizmo, IconGripVertical, IconH1, IconH2, @@ -227,6 +232,7 @@ export { IconMailCog, IconMailX, IconMap, + IconMathXy, IconMaximize, IconMessage, IconMessageCirclePlus, diff --git a/packages/twenty-ui/src/display/index.ts b/packages/twenty-ui/src/display/index.ts index 52ca2b989f8..5f4788e23ba 100644 --- a/packages/twenty-ui/src/display/index.ts +++ b/packages/twenty-ui/src/display/index.ts @@ -79,10 +79,13 @@ export { IconArrowMerge, IconArrowRight, IconArrowsDiagonal, + IconArrowsSort, IconArrowsVertical, IconArrowUp, IconArrowUpRight, IconAt, + IconAxisX, + IconAxisY, IconBaselineDensitySmall, IconBell, IconBlockquote, @@ -220,6 +223,7 @@ export { IconFilterCog, IconFilterOff, IconFilterPlus, + IconFilters, IconFilterX, IconFlag, IconFlask, @@ -234,6 +238,7 @@ export { IconGauge, IconGitBranchDeleted, IconGitCommit, + IconGizmo, IconGripVertical, IconH1, IconH2, @@ -289,6 +294,7 @@ export { IconMailCog, IconMailX, IconMap, + IconMathXy, IconMaximize, IconMessage, IconMessageCirclePlus, diff --git a/packages/twenty-ui/src/navigation/index.ts b/packages/twenty-ui/src/navigation/index.ts index 3c882043e7d..e4f2dd2a5ec 100644 --- a/packages/twenty-ui/src/navigation/index.ts +++ b/packages/twenty-ui/src/navigation/index.ts @@ -48,6 +48,7 @@ export { export { MenuItemSelectTag } from './menu/menu-item/components/MenuItemSelectTag'; export type { MenuItemSuggestionProps } from './menu/menu-item/components/MenuItemSuggestion'; export { MenuItemSuggestion } from './menu/menu-item/components/MenuItemSuggestion'; +export type { MenuItemToggleProps } from './menu/menu-item/components/MenuItemToggle'; export { MenuItemToggle } from './menu/menu-item/components/MenuItemToggle'; export { MenuItemLeftContent } from './menu/menu-item/internals/components/MenuItemLeftContent'; export type { MenuItemBaseProps } from './menu/menu-item/internals/components/StyledMenuItemBase'; diff --git a/packages/twenty-ui/src/navigation/menu/components/MenuPicker.tsx b/packages/twenty-ui/src/navigation/menu/components/MenuPicker.tsx index 8073a9a7656..162b5ea9aba 100644 --- a/packages/twenty-ui/src/navigation/menu/components/MenuPicker.tsx +++ b/packages/twenty-ui/src/navigation/menu/components/MenuPicker.tsx @@ -137,6 +137,7 @@ export const MenuPicker = ({ data-testid={testId} aria-pressed={selected} aria-disabled={disabled} + aria-label={label} > diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelect.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelect.tsx index f68c968039f..524c5ca4509 100644 --- a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelect.tsx +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelect.tsx @@ -35,7 +35,7 @@ export const StyledMenuItemSelect = styled(StyledMenuItemBase)<{ `; } else if (focused === true) { return css` - background: transparent; + background: ${theme.background.transparent.light}; `; } }} diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemToggle.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemToggle.tsx index 7b98c586477..61ad8fb29c6 100644 --- a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemToggle.tsx +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemToggle.tsx @@ -16,7 +16,7 @@ const StyledToggleContainer = styled.label` width: 100%; `; -type MenuItemToggleProps = { +export type MenuItemToggleProps = { focused?: boolean; LeftIcon?: IconComponent; withIconContainer?: boolean;