Chart editor - Part 1 (#14820)
This PR is the first part of the creation of the Chart editor. https://github.com/user-attachments/assets/8b0af8ea-be41-4506-84cb-e40f5521cbf0 Done: - Bar chart settings (except filters) - Line chart settings (except filters) In progress: - Pie chart settings - Number chart settings - Gauge chart settings Left to do: - Loosen the backend validation to allow the user to save a partial configuration, validate the graph configuration in the frontend and display a error friendly message in the graph if the config is not completed yet - Implement the filter edition - Finish the other graph types settings
This commit is contained in:
+152
@@ -0,0 +1,152 @@
|
||||
import { CommandGroup } from '@/command-menu/components/CommandGroup';
|
||||
import { CommandMenuItemDropdown } from '@/command-menu/components/CommandMenuItemDropdown';
|
||||
import { CommandMenuItemToggle } from '@/command-menu/components/CommandMenuItemToggle';
|
||||
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
|
||||
import { useUpdateCommandMenuPageInfo } from '@/command-menu/hooks/useUpdateCommandMenuPageInfo';
|
||||
import { ChartTypeSelectionSection } from '@/command-menu/pages/page-layout/components/ChartTypeSelectionSection';
|
||||
import { GRAPH_TYPE_INFORMATION } from '@/command-menu/pages/page-layout/constants/GraphTypeInformation';
|
||||
import { GRAPH_TYPE_TO_CONFIG_TYPENAME } from '@/command-menu/pages/page-layout/constants/GraphTypeToConfigTypename';
|
||||
import { useChartSettingsValues } from '@/command-menu/pages/page-layout/hooks/useChartSettingsValues';
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration';
|
||||
import {
|
||||
CHART_CONFIGURATION_SETTING_IDS,
|
||||
CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP,
|
||||
} from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
|
||||
import { getChartSettingsDropdownContent } from '@/command-menu/pages/page-layout/utils/getChartSettingsDropdownContent';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { type GraphType, type PageLayoutWidget } from '~/generated/graphql';
|
||||
|
||||
export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => {
|
||||
const { updateCommandMenuPageInfo } = useUpdateCommandMenuPageInfo();
|
||||
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { openDropdown } = useOpenDropdown();
|
||||
|
||||
if (widget.configuration?.__typename === 'IframeConfiguration') {
|
||||
throw new Error(t`IframeConfiguration is not supported`);
|
||||
}
|
||||
|
||||
const configuration = widget.configuration as ChartConfiguration;
|
||||
|
||||
const { getChartSettingsValues } = useChartSettingsValues({
|
||||
objectMetadataId: widget.objectMetadataId,
|
||||
configuration,
|
||||
});
|
||||
|
||||
const currentGraphType = configuration?.graphType;
|
||||
|
||||
const handleGraphTypeChange = (graphType: GraphType) => {
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
__typename: GRAPH_TYPE_TO_CONFIG_TYPENAME[graphType],
|
||||
graphType,
|
||||
},
|
||||
});
|
||||
|
||||
updateCommandMenuPageInfo({
|
||||
pageIcon: GRAPH_TYPE_INFORMATION[graphType].icon,
|
||||
});
|
||||
};
|
||||
|
||||
const chartSettings = GRAPH_TYPE_INFORMATION[currentGraphType].settings;
|
||||
|
||||
return (
|
||||
<CommandMenuList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={[
|
||||
...chartSettings.flatMap((group) => group.items.map((item) => item.id)),
|
||||
]}
|
||||
>
|
||||
<ChartTypeSelectionSection
|
||||
currentGraphType={currentGraphType}
|
||||
setCurrentGraphType={handleGraphTypeChange}
|
||||
/>
|
||||
{chartSettings.map((group) => (
|
||||
<CommandGroup key={group.heading} heading={group.heading}>
|
||||
{group.items.map((item) => {
|
||||
const isDisabled =
|
||||
!isNonEmptyString(widget.objectMetadataId) &&
|
||||
(item?.dependsOn?.includes(
|
||||
CHART_CONFIGURATION_SETTING_IDS.SOURCE,
|
||||
) ??
|
||||
false);
|
||||
|
||||
const handleToggleChange = () => {
|
||||
const configKey =
|
||||
item.id === CHART_CONFIGURATION_SETTING_IDS.DATA_LABELS
|
||||
? CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP[
|
||||
CHART_CONFIGURATION_SETTING_IDS.DATA_LABELS
|
||||
]
|
||||
: item.id;
|
||||
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
[configKey]: !getChartSettingsValues(item.id),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDropdownOpen = () => {
|
||||
openDropdown({
|
||||
dropdownComponentInstanceIdFromProps: item.id,
|
||||
});
|
||||
};
|
||||
|
||||
return item.isBoolean ? (
|
||||
<SelectableListItem
|
||||
key={item.id}
|
||||
itemId={item.id}
|
||||
onEnter={isDisabled ? undefined : handleToggleChange}
|
||||
>
|
||||
<CommandMenuItemToggle
|
||||
LeftIcon={item.Icon}
|
||||
text={t(item.label)}
|
||||
id={item.id}
|
||||
toggled={getChartSettingsValues(item.id) as boolean}
|
||||
onToggleChange={handleToggleChange}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
) : (
|
||||
<SelectableListItem
|
||||
key={item.id}
|
||||
itemId={item.id}
|
||||
onEnter={isDisabled ? undefined : handleDropdownOpen}
|
||||
>
|
||||
<CommandMenuItemDropdown
|
||||
Icon={item.Icon}
|
||||
label={t(item.label)}
|
||||
id={item.id}
|
||||
dropdownId={item.id}
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer>
|
||||
{getChartSettingsDropdownContent(item.id)}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
dropdownPlacement="bottom-end"
|
||||
description={getChartSettingsValues(item.id) as string}
|
||||
contextualTextPosition={'right'}
|
||||
hasSubMenu
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandMenuList>
|
||||
);
|
||||
};
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { GRAPH_TYPE_INFORMATION } from '@/command-menu/pages/page-layout/constants/GraphTypeInformation';
|
||||
import styled from '@emotion/styled';
|
||||
import { GraphType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { MenuPicker } from 'twenty-ui/navigation';
|
||||
|
||||
const graphTypeOptions = [
|
||||
GraphType.BAR,
|
||||
GraphType.PIE,
|
||||
GraphType.LINE,
|
||||
GraphType.NUMBER,
|
||||
GraphType.GAUGE,
|
||||
];
|
||||
|
||||
const StyledChartTypeSelectionContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
type ChartTypeSelectionSectionProps = {
|
||||
currentGraphType: GraphType;
|
||||
setCurrentGraphType: (graphType: GraphType) => void;
|
||||
};
|
||||
|
||||
export const ChartTypeSelectionSection = ({
|
||||
currentGraphType,
|
||||
setCurrentGraphType,
|
||||
}: ChartTypeSelectionSectionProps) => {
|
||||
return (
|
||||
<StyledChartTypeSelectionContainer>
|
||||
{graphTypeOptions.map((graphType) => (
|
||||
<MenuPicker
|
||||
selected={currentGraphType === graphType}
|
||||
key={graphType}
|
||||
icon={GRAPH_TYPE_INFORMATION[graphType].icon}
|
||||
onClick={() => {
|
||||
setCurrentGraphType(graphType);
|
||||
}}
|
||||
label={t(GRAPH_TYPE_INFORMATION[graphType].label)}
|
||||
showLabel={false}
|
||||
/>
|
||||
))}
|
||||
</StyledChartTypeSelectionContainer>
|
||||
);
|
||||
};
|
||||
+42
-76
@@ -1,87 +1,53 @@
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { ChartSettings } from '@/command-menu/pages/page-layout/components/ChartSettings';
|
||||
import { GRAPH_TYPE_INFORMATION } from '@/command-menu/pages/page-layout/constants/GraphTypeInformation';
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useCreatePageLayoutGraphWidget } from '@/page-layout/hooks/useCreatePageLayoutGraphWidget';
|
||||
import { GraphType, WidgetType } from '~/generated-metadata/graphql';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import {
|
||||
IconChartBar,
|
||||
IconChartLine,
|
||||
IconChartPie,
|
||||
IconGauge,
|
||||
IconNumber,
|
||||
} from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledSectionTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const graphTypeOptions = [
|
||||
{
|
||||
type: GraphType.BAR,
|
||||
icon: IconChartBar,
|
||||
title: 'Bar Chart',
|
||||
},
|
||||
{
|
||||
type: GraphType.PIE,
|
||||
icon: IconChartPie,
|
||||
title: 'Pie Chart',
|
||||
},
|
||||
{
|
||||
type: GraphType.GAUGE,
|
||||
icon: IconGauge,
|
||||
title: 'Gauge',
|
||||
},
|
||||
{
|
||||
type: GraphType.NUMBER,
|
||||
icon: IconNumber,
|
||||
title: 'Number',
|
||||
},
|
||||
{
|
||||
type: GraphType.LINE,
|
||||
icon: IconChartLine,
|
||||
title: 'Line Chart',
|
||||
},
|
||||
];
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const CommandMenuPageLayoutGraphTypeSelect = () => {
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
|
||||
const { createPageLayoutWidget } =
|
||||
useCreatePageLayoutGraphWidget(pageLayoutId);
|
||||
const draftPageLayout = useRecoilComponentValue(
|
||||
pageLayoutDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const handleSelectGraphType = (graphType: GraphType) => {
|
||||
createPageLayoutWidget(WidgetType.GRAPH, graphType);
|
||||
closeCommandMenu();
|
||||
};
|
||||
const pageLayoutEditingWidgetId = useRecoilComponentValue(
|
||||
pageLayoutEditingWidgetIdComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const widgetInEditMode = draftPageLayout.tabs
|
||||
.flatMap((tab) => tab.widgets)
|
||||
.find((widget) => widget.id === pageLayoutEditingWidgetId);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
if (
|
||||
!isDefined(widgetInEditMode?.configuration) ||
|
||||
!('graphType' in widgetInEditMode.configuration)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentGraphType = widgetInEditMode.configuration.graphType;
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledSectionTitle>Graph type</StyledSectionTitle>
|
||||
<>
|
||||
<SidePanelHeader
|
||||
Icon={GRAPH_TYPE_INFORMATION[currentGraphType].icon}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={t`Chart`}
|
||||
headerType={t(GRAPH_TYPE_INFORMATION[currentGraphType].label)}
|
||||
onTitleChange={() => {}}
|
||||
/>
|
||||
|
||||
{graphTypeOptions.map((option) => (
|
||||
<MenuItem
|
||||
withIconContainer={true}
|
||||
key={option.type}
|
||||
LeftIcon={option.icon}
|
||||
text={option.title}
|
||||
onClick={() => handleSelectGraphType(option.type)}
|
||||
/>
|
||||
))}
|
||||
</StyledContainer>
|
||||
<ChartSettings widget={widgetInEditMode} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+4
-3
@@ -8,6 +8,7 @@ import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pa
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isString } from '@sniptt/guards';
|
||||
import { useState } from 'react';
|
||||
import { isValidUrl } from 'twenty-shared/utils';
|
||||
@@ -119,12 +120,12 @@ export const CommandMenuPageLayoutIframeConfig = () => {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledSectionTitle>
|
||||
{isEditMode ? 'Edit iFrame Widget' : 'Configure iFrame Widget'}
|
||||
{isEditMode ? t`Edit iFrame Widget` : t`Configure iFrame Widget`}
|
||||
</StyledSectionTitle>
|
||||
|
||||
<FormTextFieldInput
|
||||
label="Widget Title"
|
||||
placeholder="e.g., Analytics Dashboard"
|
||||
label={t`Widget Title`}
|
||||
placeholder={t`e.g., Analytics Dashboard`}
|
||||
defaultValue={title}
|
||||
onChange={setTitle}
|
||||
/>
|
||||
|
||||
+60
-91
@@ -1,109 +1,78 @@
|
||||
import { CommandGroup } from '@/command-menu/components/CommandGroup';
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
|
||||
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconChartPie, IconFrame, IconList } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import { WidgetType } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledSectionTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledDisabledMenuItem = styled.div`
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
const widgetTypeOptions = [
|
||||
{
|
||||
type: WidgetType.GRAPH,
|
||||
icon: IconChartPie,
|
||||
title: 'Add a graph',
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
type: WidgetType.VIEW,
|
||||
icon: IconList,
|
||||
title: 'Add a view',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
type: WidgetType.IFRAME,
|
||||
icon: IconFrame,
|
||||
title: 'Add an iframe',
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
import { useCreatePageLayoutGraphWidget } from '@/page-layout/hooks/useCreatePageLayoutGraphWidget';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconChartPie, IconFrame } from 'twenty-ui/display';
|
||||
import { GraphType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const CommandMenuPageLayoutWidgetTypeSelect = () => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
|
||||
const setPageLayoutDraggedArea = useSetRecoilComponentState(
|
||||
pageLayoutDraggedAreaComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const { navigatePageLayoutCommandMenu } = useNavigatePageLayoutCommandMenu();
|
||||
|
||||
const handleSelectWidget = (widgetType: WidgetType) => {
|
||||
switch (widgetType) {
|
||||
case WidgetType.GRAPH: {
|
||||
navigatePageLayoutCommandMenu({
|
||||
commandMenuPage: CommandMenuPages.PageLayoutGraphTypeSelect,
|
||||
});
|
||||
const { createPageLayoutGraphWidget } =
|
||||
useCreatePageLayoutGraphWidget(pageLayoutId);
|
||||
|
||||
break;
|
||||
}
|
||||
case WidgetType.IFRAME: {
|
||||
navigatePageLayoutCommandMenu({
|
||||
commandMenuPage: CommandMenuPages.PageLayoutIframeConfig,
|
||||
});
|
||||
const [pageLayoutEditingWidgetId, setPageLayoutEditingWidgetId] =
|
||||
useRecoilComponentState(
|
||||
pageLayoutEditingWidgetIdComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
setPageLayoutDraggedArea(null);
|
||||
break;
|
||||
const handleNavigateToGraphTypeSelect = () => {
|
||||
if (!isDefined(pageLayoutEditingWidgetId)) {
|
||||
const newWidget = createPageLayoutGraphWidget(GraphType.BAR);
|
||||
|
||||
setPageLayoutEditingWidgetId(newWidget.id);
|
||||
}
|
||||
|
||||
navigatePageLayoutCommandMenu({
|
||||
commandMenuPage: CommandMenuPages.PageLayoutGraphTypeSelect,
|
||||
});
|
||||
};
|
||||
|
||||
const handleNavigateToIframeConfig = () => {
|
||||
navigatePageLayoutCommandMenu({
|
||||
commandMenuPage: CommandMenuPages.PageLayoutIframeConfig,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledSectionTitle>Widget type</StyledSectionTitle>
|
||||
{widgetTypeOptions.map((option) => {
|
||||
const MenuItemComponent = (
|
||||
<MenuItem
|
||||
withIconContainer={true}
|
||||
key={option.type}
|
||||
LeftIcon={option.icon}
|
||||
text={option.title}
|
||||
onClick={() => handleSelectWidget(option.type)}
|
||||
<CommandMenuList commandGroups={[]} selectableItemIds={['chart', 'iframe']}>
|
||||
<CommandGroup heading={t`Widget type`}>
|
||||
<SelectableListItem
|
||||
itemId="chart"
|
||||
onEnter={handleNavigateToGraphTypeSelect}
|
||||
>
|
||||
<CommandMenuItem
|
||||
Icon={IconChartPie}
|
||||
label={t`Chart`}
|
||||
id="chart"
|
||||
onClick={handleNavigateToGraphTypeSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
return option.disabled ? (
|
||||
<StyledDisabledMenuItem key={option.type}>
|
||||
{MenuItemComponent}
|
||||
</StyledDisabledMenuItem>
|
||||
) : (
|
||||
MenuItemComponent
|
||||
);
|
||||
})}
|
||||
</StyledContainer>
|
||||
</SelectableListItem>
|
||||
<SelectableListItem
|
||||
itemId="iframe"
|
||||
onEnter={() => {
|
||||
handleNavigateToIframeConfig();
|
||||
}}
|
||||
>
|
||||
<CommandMenuItem
|
||||
Icon={IconFrame}
|
||||
label="iFrame"
|
||||
id="iframe"
|
||||
onClick={handleNavigateToIframeConfig}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</CommandGroup>
|
||||
</CommandMenuList>
|
||||
);
|
||||
};
|
||||
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { mapToGraphQLExtendedAggregateOperation } from '@/command-menu/pages/page-layout/utils/mapToGraphQLExtendedAggregateOperation';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
|
||||
import { getAvailableAggregateOperationsForFieldMetadataType } from '@/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType';
|
||||
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconChevronLeft } from 'twenty-ui/display';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
|
||||
|
||||
export const ChartAggregateOperationSelectionDropdownContent = ({
|
||||
currentFieldMetadataId,
|
||||
setIsSubMenuOpen,
|
||||
}: {
|
||||
currentFieldMetadataId: string;
|
||||
setIsSubMenuOpen: (isSubMenuOpen: boolean) => void;
|
||||
}) => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
if (
|
||||
widgetInEditMode?.configuration?.__typename !== 'BarChartConfiguration' &&
|
||||
widgetInEditMode?.configuration?.__typename !== 'LineChartConfiguration'
|
||||
) {
|
||||
throw new Error('Invalid configuration type');
|
||||
}
|
||||
|
||||
const currentAggregateOperation =
|
||||
widgetInEditMode.configuration.aggregateOperation;
|
||||
|
||||
const sourceObjectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === widgetInEditMode.objectMetadataId,
|
||||
);
|
||||
|
||||
const selectedField = sourceObjectMetadataItem?.fields.find(
|
||||
(field) => field.id === currentFieldMetadataId,
|
||||
);
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
selectedItemIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const availableAggregateOperations = selectedField
|
||||
? getAvailableAggregateOperationsForFieldMetadataType({
|
||||
fieldMetadataType: selectedField.type,
|
||||
})
|
||||
: [];
|
||||
|
||||
const aggregateOperationsWithLabels = availableAggregateOperations.map(
|
||||
(operation) => ({
|
||||
operation,
|
||||
label: getAggregateOperationLabel(operation),
|
||||
}),
|
||||
);
|
||||
|
||||
const filteredAggregateOperationsWithLabels = filterBySearchQuery({
|
||||
items: aggregateOperationsWithLabels,
|
||||
searchQuery,
|
||||
getSearchableValues: (item) => [item.label],
|
||||
});
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
if (!isDefined(sourceObjectMetadataItem) || !isDefined(selectedField)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSelectAggregateOperation = (
|
||||
aggregateOperation: ExtendedAggregateOperations,
|
||||
) => {
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
aggregateFieldMetadataId: currentFieldMetadataId,
|
||||
aggregateOperation:
|
||||
mapToGraphQLExtendedAggregateOperation(aggregateOperation),
|
||||
},
|
||||
});
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader
|
||||
StartComponent={
|
||||
<DropdownMenuHeaderLeftComponent
|
||||
onClick={() => setIsSubMenuOpen(false)}
|
||||
Icon={IconChevronLeft}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Trans>Y-Axis Aggregate Operation</Trans>
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder={t`Search operations`}
|
||||
onChange={(event) => setSearchQuery(event.target.value)}
|
||||
value={searchQuery}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={filteredAggregateOperationsWithLabels.map(
|
||||
(item) => item.operation,
|
||||
)}
|
||||
>
|
||||
{filteredAggregateOperationsWithLabels.map((item) => (
|
||||
<SelectableListItem
|
||||
key={item.operation}
|
||||
itemId={item.operation}
|
||||
onEnter={() => {
|
||||
handleSelectAggregateOperation(item.operation);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={item.label}
|
||||
selected={
|
||||
currentAggregateOperation ===
|
||||
mapToGraphQLExtendedAggregateOperation(item.operation)
|
||||
}
|
||||
focused={selectedItemId === item.operation}
|
||||
onClick={() => {
|
||||
handleSelectAggregateOperation(item.operation);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { getChartAxisNameDisplayOptions } from '@/command-menu/pages/page-layout/utils/getChartAxisNameDisplayOptions';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { AxisNameDisplay } from '~/generated/graphql';
|
||||
|
||||
export const ChartAxisNameSelectionDropdownContent = () => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
if (
|
||||
widgetInEditMode?.configuration?.__typename !== 'BarChartConfiguration' &&
|
||||
widgetInEditMode?.configuration?.__typename !== 'LineChartConfiguration'
|
||||
) {
|
||||
throw new Error('Invalid configuration type');
|
||||
}
|
||||
|
||||
const currentAxisNameDisplay = widgetInEditMode.configuration.axisNameDisplay;
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
selectedItemIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const axisOptions: AxisNameDisplay[] = [
|
||||
AxisNameDisplay.NONE,
|
||||
AxisNameDisplay.X,
|
||||
AxisNameDisplay.Y,
|
||||
AxisNameDisplay.BOTH,
|
||||
];
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const handleSelectAxisNameOption = (axisNameOption: AxisNameDisplay) => {
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
axisNameDisplay: axisNameOption,
|
||||
},
|
||||
});
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader>
|
||||
<Trans>Axis Name</Trans>
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={axisOptions}
|
||||
>
|
||||
{axisOptions.map((option) => (
|
||||
<SelectableListItem
|
||||
key={option}
|
||||
itemId={option}
|
||||
onEnter={() => {
|
||||
handleSelectAxisNameOption(option);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={getChartAxisNameDisplayOptions(option)}
|
||||
selected={currentAxisNameDisplay?.toUpperCase() === option}
|
||||
focused={selectedItemId === option}
|
||||
onClick={() => {
|
||||
handleSelectAxisNameOption(option);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { ColorSample } from 'twenty-ui/display';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { MAIN_COLOR_NAMES, type ThemeColor } from 'twenty-ui/theme';
|
||||
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
|
||||
|
||||
export const ChartColorSelectionDropdownContent = () => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
selectedItemIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
if (!isDefined(widgetInEditMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (widgetInEditMode.configuration?.__typename === 'IframeConfiguration') {
|
||||
throw new Error('Invalid configuration type');
|
||||
}
|
||||
|
||||
const configuration = widgetInEditMode.configuration as ChartConfiguration;
|
||||
|
||||
const currentColor = configuration.color;
|
||||
|
||||
const colorOptions = MAIN_COLOR_NAMES.map((colorName) => ({
|
||||
id: colorName,
|
||||
name: capitalize(colorName),
|
||||
colorName: colorName,
|
||||
}));
|
||||
|
||||
const filteredColorOptions = filterBySearchQuery({
|
||||
items: colorOptions,
|
||||
searchQuery,
|
||||
getSearchableValues: (item) => [item.name],
|
||||
});
|
||||
|
||||
const handleSelectColor = (colorName: ThemeColor) => {
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
color: colorName,
|
||||
},
|
||||
});
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader>
|
||||
<Trans>Color</Trans>
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder={t`Search colors`}
|
||||
onChange={(event) => setSearchQuery(event.target.value)}
|
||||
value={searchQuery}
|
||||
/>
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={filteredColorOptions.map(
|
||||
(colorOption) => colorOption.id,
|
||||
)}
|
||||
>
|
||||
{filteredColorOptions.map((colorOption) => (
|
||||
<SelectableListItem
|
||||
key={colorOption.id}
|
||||
itemId={colorOption.id}
|
||||
onEnter={() => {
|
||||
handleSelectColor(colorOption.colorName);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={colorOption.name}
|
||||
selected={currentColor === colorOption.id}
|
||||
focused={selectedItemId === colorOption.id}
|
||||
LeftIcon={() => (
|
||||
<ColorSample colorName={colorOption.colorName} />
|
||||
)}
|
||||
onClick={() => {
|
||||
handleSelectColor(colorOption.colorName);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
|
||||
|
||||
export const ChartDataSourceDropdownContent = () => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
const currentSource = widgetInEditMode?.objectMetadataId;
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
selectedItemIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const objectsWithReadAccess = objectMetadataItems.filter(
|
||||
(objectMetadataItem) => {
|
||||
const objectPermissions =
|
||||
objectPermissionsByObjectMetadataId[objectMetadataItem.id];
|
||||
|
||||
return (
|
||||
isDefined(objectPermissions) && objectPermissions.canReadObjectRecords
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const availableObjectMetadataItems = filterBySearchQuery({
|
||||
items: objectsWithReadAccess,
|
||||
searchQuery,
|
||||
getSearchableValues: (item) => [item.labelPlural, item.namePlural],
|
||||
});
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const handleSelectSource = (objectMetadataId: string) => {
|
||||
updateCurrentWidgetConfig({
|
||||
objectMetadataId,
|
||||
});
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader>
|
||||
<Trans>Source</Trans>
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder={t`Search objects`}
|
||||
onChange={(event) => setSearchQuery(event.target.value)}
|
||||
value={searchQuery}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={availableObjectMetadataItems.map(
|
||||
(objectMetadataItem) => objectMetadataItem.id,
|
||||
)}
|
||||
>
|
||||
{availableObjectMetadataItems.map((objectMetadataItem) => (
|
||||
<SelectableListItem
|
||||
key={objectMetadataItem.id}
|
||||
itemId={objectMetadataItem.id}
|
||||
onEnter={() => {
|
||||
handleSelectSource(objectMetadataItem.id);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={objectMetadataItem.labelPlural}
|
||||
selected={currentSource === objectMetadataItem.id}
|
||||
focused={selectedItemId === objectMetadataItem.id}
|
||||
LeftIcon={getIcon(objectMetadataItem.icon)}
|
||||
onClick={() => {
|
||||
handleSelectSource(objectMetadataItem.id);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
|
||||
|
||||
export const ChartFieldSelectionDropdownContent = () => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
if (
|
||||
widgetInEditMode?.configuration?.__typename !== 'BarChartConfiguration' &&
|
||||
widgetInEditMode?.configuration?.__typename !== 'LineChartConfiguration'
|
||||
) {
|
||||
throw new Error('Invalid configuration type');
|
||||
}
|
||||
|
||||
const currentXAxisFieldMetadataId =
|
||||
widgetInEditMode.configuration.groupByFieldMetadataIdX;
|
||||
|
||||
const sourceObjectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === widgetInEditMode.objectMetadataId,
|
||||
);
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
selectedItemIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const availableFieldMetadataItems = filterBySearchQuery({
|
||||
items: sourceObjectMetadataItem?.fields || [],
|
||||
searchQuery,
|
||||
getSearchableValues: (item) => [item.label, item.name],
|
||||
});
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
if (!isDefined(sourceObjectMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSelectField = (fieldMetadataId: string) => {
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
groupByFieldMetadataIdX: fieldMetadataId,
|
||||
},
|
||||
});
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader>
|
||||
<Trans>X-Axis Field</Trans>
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder={t`Search fields`}
|
||||
onChange={(event) => setSearchQuery(event.target.value)}
|
||||
value={searchQuery}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={availableFieldMetadataItems.map(
|
||||
(item) => item.id,
|
||||
)}
|
||||
>
|
||||
{availableFieldMetadataItems.map((fieldMetadataItem) => (
|
||||
<SelectableListItem
|
||||
key={fieldMetadataItem.id}
|
||||
itemId={fieldMetadataItem.id}
|
||||
onEnter={() => {
|
||||
handleSelectField(fieldMetadataItem.id);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={fieldMetadataItem.label}
|
||||
selected={currentXAxisFieldMetadataId === fieldMetadataItem.id}
|
||||
focused={selectedItemId === fieldMetadataItem.id}
|
||||
LeftIcon={getIcon(fieldMetadataItem.icon)}
|
||||
onClick={() => {
|
||||
handleSelectField(fieldMetadataItem.id);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import { ChartAggregateOperationSelectionDropdownContent } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartAggregateOperationSelectionDropdownContent';
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
|
||||
|
||||
export const ChartFieldSelectionForAggregateOperationDropdownContent = () => {
|
||||
const [isSubMenuOpen, setIsSubMenuOpen] = useState(false);
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
if (
|
||||
widgetInEditMode?.configuration?.__typename !== 'BarChartConfiguration' &&
|
||||
widgetInEditMode?.configuration?.__typename !== 'LineChartConfiguration'
|
||||
) {
|
||||
throw new Error('Invalid configuration type');
|
||||
}
|
||||
|
||||
const currentFieldMetadataId =
|
||||
widgetInEditMode.configuration.groupByFieldMetadataIdY;
|
||||
|
||||
const [selectedFieldMetadataId, setSelectedFieldMetadataId] = useState(
|
||||
currentFieldMetadataId,
|
||||
);
|
||||
|
||||
const sourceObjectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === widgetInEditMode.objectMetadataId,
|
||||
);
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
selectedItemIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const availableFieldMetadataItems = filterBySearchQuery({
|
||||
items: sourceObjectMetadataItem?.fields || [],
|
||||
searchQuery,
|
||||
getSearchableValues: (item) => [item.label, item.name],
|
||||
});
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
if (!isDefined(sourceObjectMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isSubMenuOpen) {
|
||||
return (
|
||||
<ChartAggregateOperationSelectionDropdownContent
|
||||
currentFieldMetadataId={selectedFieldMetadataId}
|
||||
setIsSubMenuOpen={setIsSubMenuOpen}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader>
|
||||
<Trans>Y-Axis Field</Trans>
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder={t`Search fields`}
|
||||
onChange={(event) => setSearchQuery(event.target.value)}
|
||||
value={searchQuery}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={availableFieldMetadataItems.map(
|
||||
(item) => item.id,
|
||||
)}
|
||||
>
|
||||
{availableFieldMetadataItems.map((fieldMetadataItem) => (
|
||||
<SelectableListItem
|
||||
key={fieldMetadataItem.id}
|
||||
itemId={fieldMetadataItem.id}
|
||||
onEnter={() => {
|
||||
setIsSubMenuOpen(true);
|
||||
setSelectedFieldMetadataId(fieldMetadataItem.id);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={fieldMetadataItem.label}
|
||||
selected={selectedFieldMetadataId === fieldMetadataItem.id}
|
||||
focused={selectedItemId === fieldMetadataItem.id}
|
||||
LeftIcon={getIcon(fieldMetadataItem.icon)}
|
||||
hasSubMenu={true}
|
||||
onClick={() => {
|
||||
setIsSubMenuOpen(true);
|
||||
setSelectedFieldMetadataId(fieldMetadataItem.id);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
|
||||
|
||||
export const ChartGroupByFieldSelectionDropdownContent = () => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
if (
|
||||
widgetInEditMode?.configuration?.__typename !== 'BarChartConfiguration' &&
|
||||
widgetInEditMode?.configuration?.__typename !== 'LineChartConfiguration'
|
||||
) {
|
||||
throw new Error('Invalid configuration type');
|
||||
}
|
||||
|
||||
const currentGroupByFieldMetadataId =
|
||||
widgetInEditMode.configuration.groupByFieldMetadataIdY;
|
||||
|
||||
const sourceObjectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === widgetInEditMode.objectMetadataId,
|
||||
);
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
selectedItemIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const availableFieldMetadataItems = filterBySearchQuery({
|
||||
items: sourceObjectMetadataItem?.fields || [],
|
||||
searchQuery,
|
||||
getSearchableValues: (item) => [item.label, item.name],
|
||||
});
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
if (!isDefined(sourceObjectMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSelectField = (fieldMetadataId: string) => {
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
groupByFieldMetadataIdY: fieldMetadataId,
|
||||
},
|
||||
});
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader>
|
||||
<Trans>Y-Axis Group By Field</Trans>
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder={t`Search fields`}
|
||||
onChange={(event) => setSearchQuery(event.target.value)}
|
||||
value={searchQuery}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={availableFieldMetadataItems.map(
|
||||
(item) => item.id,
|
||||
)}
|
||||
>
|
||||
{availableFieldMetadataItems.map((fieldMetadataItem) => (
|
||||
<SelectableListItem
|
||||
key={fieldMetadataItem.id}
|
||||
itemId={fieldMetadataItem.id}
|
||||
onEnter={() => {
|
||||
handleSelectField(fieldMetadataItem.id);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={fieldMetadataItem.label}
|
||||
selected={
|
||||
currentGroupByFieldMetadataId === fieldMetadataItem.id
|
||||
}
|
||||
focused={selectedItemId === fieldMetadataItem.id}
|
||||
LeftIcon={getIcon(fieldMetadataItem.icon)}
|
||||
onClick={() => {
|
||||
handleSelectField(fieldMetadataItem.id);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import { AGGREGATE_SORT_BY_OPTIONS } from '@/command-menu/pages/page-layout/constants/AggregateSortByOptions';
|
||||
import { useGraphGroupBySortOptionLabels } from '@/command-menu/pages/page-layout/hooks/useGraphGroupBySortOptionLabels';
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import {
|
||||
type BarChartConfiguration,
|
||||
type GraphOrderBy,
|
||||
type LineChartConfiguration,
|
||||
type NumberChartConfiguration,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
type ChartSortByGroupByFieldDropdownContentProps = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
export const ChartSortByGroupByFieldDropdownContent = ({
|
||||
title,
|
||||
}: ChartSortByGroupByFieldDropdownContentProps) => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
const configuration = widgetInEditMode?.configuration as
|
||||
| BarChartConfiguration
|
||||
| LineChartConfiguration
|
||||
| NumberChartConfiguration;
|
||||
|
||||
const currentOrderBy =
|
||||
'orderByY' in configuration
|
||||
? configuration.orderByY
|
||||
: 'orderBy' in configuration
|
||||
? configuration.orderBy
|
||||
: undefined;
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
selectedItemIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const orderByKey = 'orderByY' in configuration ? 'orderByY' : 'orderBy';
|
||||
|
||||
const handleSelectSortOption = (orderBy: GraphOrderBy) => {
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
[orderByKey]: orderBy,
|
||||
},
|
||||
});
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const { getGroupBySortOptionLabel } = useGraphGroupBySortOptionLabels({
|
||||
objectMetadataId: widgetInEditMode?.objectMetadataId,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader>{title}</DropdownMenuHeader>
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={AGGREGATE_SORT_BY_OPTIONS.map(
|
||||
(option) => option.value,
|
||||
)}
|
||||
>
|
||||
{AGGREGATE_SORT_BY_OPTIONS.map((sortOption) => (
|
||||
<SelectableListItem
|
||||
key={sortOption.value}
|
||||
itemId={sortOption.value}
|
||||
onEnter={() => {
|
||||
handleSelectSortOption(sortOption.value);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={getGroupBySortOptionLabel({
|
||||
graphOrderBy: sortOption.value,
|
||||
groupByFieldMetadataId:
|
||||
'groupByFieldMetadataIdY' in configuration
|
||||
? configuration.groupByFieldMetadataIdY
|
||||
: 'groupByFieldMetadataId' in configuration
|
||||
? configuration.groupByFieldMetadataId
|
||||
: undefined,
|
||||
})}
|
||||
selected={currentOrderBy === sortOption.value}
|
||||
focused={selectedItemId === sortOption.value}
|
||||
LeftIcon={sortOption.icon}
|
||||
onClick={() => {
|
||||
handleSelectSortOption(sortOption.value);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import { X_SORT_BY_OPTIONS } from '@/command-menu/pages/page-layout/constants/XSortByOptions';
|
||||
import { useGraphXSortOptionLabels } from '@/command-menu/pages/page-layout/hooks/useGraphXSortOptionLabels';
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { type GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
export const ChartXAxisSortBySelectionDropdownContent = () => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
if (
|
||||
widgetInEditMode?.configuration?.__typename !== 'BarChartConfiguration' &&
|
||||
widgetInEditMode?.configuration?.__typename !== 'LineChartConfiguration'
|
||||
) {
|
||||
throw new Error('Invalid configuration type');
|
||||
}
|
||||
|
||||
const configuration = widgetInEditMode?.configuration;
|
||||
const currentOrderByX = configuration.orderByX;
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
selectedItemIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const handleSelectSortOption = (orderByX: GraphOrderBy) => {
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
orderByX,
|
||||
},
|
||||
});
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const { getXSortOptionLabel } = useGraphXSortOptionLabels({
|
||||
objectMetadataId: widgetInEditMode?.objectMetadataId,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader>
|
||||
<Trans>X-Axis Sort By</Trans>
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={X_SORT_BY_OPTIONS.map(
|
||||
(option) => option.value,
|
||||
)}
|
||||
>
|
||||
{X_SORT_BY_OPTIONS.map((sortOption) => (
|
||||
<SelectableListItem
|
||||
key={sortOption.value}
|
||||
itemId={sortOption.value}
|
||||
onEnter={() => {
|
||||
handleSelectSortOption(sortOption.value);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={getXSortOptionLabel({
|
||||
graphOrderBy: sortOption.value,
|
||||
groupByFieldMetadataIdX:
|
||||
configuration.groupByFieldMetadataIdX,
|
||||
aggregateFieldMetadataId:
|
||||
configuration.aggregateFieldMetadataId,
|
||||
aggregateOperation: configuration.aggregateOperation,
|
||||
})}
|
||||
selected={currentOrderByX === sortOption.value}
|
||||
focused={selectedItemId === sortOption.value}
|
||||
LeftIcon={sortOption.icon}
|
||||
onClick={() => {
|
||||
handleSelectSortOption(sortOption.value);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user