Connect chart filters to backend (#15133)

This PR connects the chart filters settings page to the backend.

Both for persisting the filters in the chart's configuration and also
for querying with those filters.

I made sure that the filters configuration is reset in the draft if we
change the data source object.
This commit is contained in:
Lucas Bordeau
2025-10-16 17:17:01 +02:00
committed by GitHub
parent 1b6c9e851f
commit 59004306c9
23 changed files with 414 additions and 112 deletions
@@ -1,67 +0,0 @@
import { CommandMenuSubPageNavigationHeader } from '@/command-menu/pages/common/components/CommandMenuSubPageNavigationHeader';
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { AdvancedFilterCommandMenuContainer } from '@/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuContainer';
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { InputLabel } from '@/ui/input/components/InputLabel';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { type PageLayoutWidget } from '~/generated/graphql';
const StyledChartFiltersPageContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(3)};
`;
const StyledFiltersContainer = styled.div``;
export const ChartFilters = ({
objectMetadataItem,
widget,
}: {
objectMetadataItem: ObjectMetadataItem;
widget: PageLayoutWidget;
}) => {
const { navigatePageLayoutCommandMenu } = useNavigatePageLayoutCommandMenu();
const handleBackClick = () => {
navigatePageLayoutCommandMenu({
commandMenuPage: CommandMenuPages.PageLayoutGraphTypeSelect,
});
};
const instanceId = `chart-filters-widget${widget.id}-${objectMetadataItem.id}`;
return (
<StyledChartFiltersPageContainer>
<CommandMenuSubPageNavigationHeader
title={t`Filter`}
onBackClick={handleBackClick}
/>
<StyledFiltersContainer>
<InputLabel>{t`Conditions`}</InputLabel>
<RecordFilterGroupsComponentInstanceContext.Provider
value={{ instanceId }}
>
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId }}
>
<AdvancedFilterCommandMenuContainer
onUpdate={() => {}}
objectMetadataItem={objectMetadataItem}
isWorkflowFindRecords={false}
/>
{/* TODO: create chart record filter effect to persist filters
<WorkflowFindRecordsFiltersEffect defaultValue={formData.filter} /> */}
</RecordFiltersComponentInstanceContext.Provider>
</RecordFilterGroupsComponentInstanceContext.Provider>
</StyledFiltersContainer>
</StyledChartFiltersPageContainer>
);
};
@@ -0,0 +1,131 @@
import { CommandMenuSubPageNavigationHeader } from '@/command-menu/pages/common/components/CommandMenuSubPageNavigationHeader';
import { ChartFiltersSettingsInitializeStateEffect } from '@/command-menu/pages/page-layout/components/ChartFiltersSettingsInitializeStateEffect';
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { type ChartWidget } from '@/command-menu/pages/page-layout/types/ChartWidget';
import { type ChartWidgetConfiguration } from '@/command-menu/pages/page-layout/types/ChartWidgetConfiguration';
import { getChartFiltersSettingsInstanceId } from '@/command-menu/pages/page-layout/utils/getChartFiltersSettingsInstanceId';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { AdvancedFilterCommandMenuContainer } from '@/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuContainer';
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useRecoilCallback } from 'recoil';
const StyledChartFiltersPageContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(3)};
`;
export type ChartFiltersSettingsProps = {
objectMetadataItem: ObjectMetadataItem;
widget: ChartWidget;
};
export const ChartFiltersSettings = ({
objectMetadataItem,
widget,
}: ChartFiltersSettingsProps) => {
const { navigatePageLayoutCommandMenu } = useNavigatePageLayoutCommandMenu();
const handleBackClick = () => {
navigatePageLayoutCommandMenu({
commandMenuPage: CommandMenuPages.PageLayoutGraphTypeSelect,
});
};
const { instanceId } = getChartFiltersSettingsInstanceId({
widgetId: widget.id,
objectMetadataItemId: objectMetadataItem.id,
});
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { updateCurrentWidgetConfig } =
useUpdateCurrentWidgetConfig(pageLayoutId);
const currentRecordFiltersCallbackState = useRecoilComponentCallbackState(
currentRecordFiltersComponentState,
instanceId,
);
const currentRecordFilterGroupsCallbackState =
useRecoilComponentCallbackState(
currentRecordFilterGroupsComponentState,
instanceId,
);
const chartWidgetConfiguration = widget.configuration;
const handleFiltersUpdate = useRecoilCallback(
({ snapshot }) =>
() => {
const currentRecordFilters = getSnapshotValue(
snapshot,
currentRecordFiltersCallbackState,
);
const currentRecordFilterGroups = getSnapshotValue(
snapshot,
currentRecordFilterGroupsCallbackState,
);
updateCurrentWidgetConfig({
objectMetadataId: objectMetadataItem.id,
configToUpdate: {
filter: {
recordFilters: currentRecordFilters,
recordFilterGroups: currentRecordFilterGroups,
},
} satisfies Partial<ChartWidgetConfiguration>,
});
},
[
currentRecordFiltersCallbackState,
currentRecordFilterGroupsCallbackState,
objectMetadataItem,
updateCurrentWidgetConfig,
],
);
return (
<StyledChartFiltersPageContainer>
<CommandMenuSubPageNavigationHeader
title={t`Filter`}
onBackClick={handleBackClick}
/>
<div>
<InputLabel>{t`Conditions`}</InputLabel>
<RecordFilterGroupsComponentInstanceContext.Provider
value={{ instanceId }}
>
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId }}
>
<AdvancedFilterCommandMenuContainer
onUpdate={handleFiltersUpdate}
objectMetadataItem={objectMetadataItem}
isWorkflowFindRecords={false}
/>
<ChartFiltersSettingsInitializeStateEffect
initialChartFilters={chartWidgetConfiguration.filter}
/>
</RecordFiltersComponentInstanceContext.Provider>
</RecordFilterGroupsComponentInstanceContext.Provider>
</div>
</StyledChartFiltersPageContainer>
);
};
@@ -0,0 +1,63 @@
import { hasInitializedChartFiltersComponentState } from '@/command-menu/pages/page-layout/states/hasInitializedChartFiltersComponentState';
import { type ChartFilters } from '@/command-menu/pages/page-layout/types/ChartFilters';
import { useSetAdvancedFilterDropdownStates } from '@/object-record/advanced-filter/hooks/useSetAdvancedFilterDropdownAllRowsStates';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { useEffect, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
export type ChartFiltersSettingsInitializeStateEffectProps = {
initialChartFilters?: ChartFilters;
};
export const ChartFiltersSettingsInitializeStateEffect = ({
initialChartFilters,
}: ChartFiltersSettingsInitializeStateEffectProps) => {
const [hasInitializedChartFilters, setHasInitializedChartFilters] =
useRecoilComponentState(hasInitializedChartFiltersComponentState);
const setCurrentRecordFilters = useSetRecoilComponentState(
currentRecordFiltersComponentState,
);
const setCurrentRecordFilterGroups = useSetRecoilComponentState(
currentRecordFilterGroupsComponentState,
);
const { setAdvancedFilterDropdownStates } =
useSetAdvancedFilterDropdownStates();
const [
shouldSetAdvancedFilterDropdownStates,
setShouldSetAdvancedFilterDropdownStates,
] = useState(false);
useEffect(() => {
if (!hasInitializedChartFilters && isDefined(initialChartFilters)) {
setCurrentRecordFilters(initialChartFilters.recordFilters ?? []);
setCurrentRecordFilterGroups(
initialChartFilters.recordFilterGroups ?? [],
);
setShouldSetAdvancedFilterDropdownStates(true);
setHasInitializedChartFilters(true);
}
}, [
setCurrentRecordFilters,
setCurrentRecordFilterGroups,
setHasInitializedChartFilters,
hasInitializedChartFilters,
initialChartFilters,
]);
useEffect(() => {
if (shouldSetAdvancedFilterDropdownStates) {
setAdvancedFilterDropdownStates();
setShouldSetAdvancedFilterDropdownStates(false);
}
}, [shouldSetAdvancedFilterDropdownStates, setAdvancedFilterDropdownStates]);
return null;
};
@@ -4,12 +4,12 @@ import { CommandMenuItemDropdown } from '@/command-menu/components/CommandMenuIt
import { CommandMenuItemToggle } from '@/command-menu/components/CommandMenuItemToggle';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { COMMAND_MENU_LIST_SELECTABLE_LIST_ID } from '@/command-menu/constants/CommandMenuListSelectableListId';
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
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 { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
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';
@@ -24,7 +24,6 @@ import { SelectableListItem } from '@/ui/layout/selectable-list/components/Selec
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { IconFilter } from 'twenty-ui/display';
import {
BarChartGroupMode,
@@ -35,7 +34,7 @@ import {
export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => {
const { updateCommandMenuPageInfo } = useUpdateCommandMenuPageInfo();
const { navigateCommandMenu } = useNavigateCommandMenu();
const { navigatePageLayoutCommandMenu } = useNavigatePageLayoutCommandMenu();
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
@@ -144,10 +143,8 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => {
};
const handleFilterSettingsClick = () => {
navigateCommandMenu({
page: CommandMenuPages.PageLayoutGraphFilter,
pageTitle: t`Filters`,
pageIcon: IconFilter,
navigatePageLayoutCommandMenu({
commandMenuPage: CommandMenuPages.PageLayoutGraphFilter,
});
};
@@ -1,7 +1,8 @@
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { ChartFilters } from '@/command-menu/pages/page-layout/components/ChartFilters';
import { ChartFiltersSettings } from '@/command-menu/pages/page-layout/components/ChartFiltersSettings';
import { GRAPH_TYPE_INFORMATION } from '@/command-menu/pages/page-layout/constants/GraphTypeInformation';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { isChartWidget } from '@/command-menu/pages/page-layout/utils/isChartWidget';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
@@ -11,6 +12,8 @@ import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
export const CommandMenuPageLayoutGraphFilter = () => {
const theme = useTheme();
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const draftPageLayout = useRecoilComponentValue(
@@ -27,26 +30,25 @@ export const CommandMenuPageLayoutGraphFilter = () => {
.flatMap((tab) => tab.widgets)
.find((widget) => widget.id === pageLayoutEditingWidgetId);
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: widgetInEditMode?.objectMetadataId,
});
if (!isDefined(pageLayoutEditingWidgetId)) {
throw new Error('Widget ID must be present while editing the widget');
}
if (!isDefined(widgetInEditMode)) {
throw new Error(
`Widget with ID ${pageLayoutEditingWidgetId} not found in page layout`,
);
}
const theme = useTheme();
if (!isDefined(widgetInEditMode?.objectMetadataId)) {
throw new Error('No data source in chart');
}
if (
!isDefined(widgetInEditMode.configuration) ||
!('graphType' in widgetInEditMode.configuration)
) {
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: widgetInEditMode.objectMetadataId,
});
if (!isDefined(pageLayoutEditingWidgetId)) {
throw new Error('Widget ID must be present while editing the widget');
}
if (!isChartWidget(widgetInEditMode)) {
return null;
}
@@ -62,7 +64,7 @@ export const CommandMenuPageLayoutGraphFilter = () => {
headerType={t`${graphTypeLabel} Chart`}
disabled
/>
<ChartFilters
<ChartFiltersSettings
widget={widgetInEditMode}
objectMetadataItem={objectMetadataItem}
/>
@@ -1,4 +1,5 @@
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { useResetChartDraftFiltersSettings } from '@/command-menu/pages/page-layout/hooks/useResetChartDraftFiltersSettings';
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';
@@ -35,7 +36,9 @@ export const ChartDataSourceDropdownContent = () => {
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const currentSource = widgetInEditMode?.objectMetadataId;
const currentObjectMetadataItemId = widgetInEditMode?.objectMetadataId as
| string
| undefined;
const dropdownId = useAvailableComponentInstanceIdOrThrow(
DropdownComponentInstanceContext,
@@ -80,10 +83,13 @@ export const ChartDataSourceDropdownContent = () => {
const { getIcon } = useIcons();
const handleSelectSource = (objectMetadataId: string) => {
if (currentSource !== objectMetadataId) {
const { resetChartDraftFiltersSettings } =
useResetChartDraftFiltersSettings();
const handleSelectSource = (newObjectMetadataItemId: string) => {
if (currentObjectMetadataItemId !== newObjectMetadataItemId) {
updateCurrentWidgetConfig({
objectMetadataId,
objectMetadataId: newObjectMetadataItemId,
configToUpdate: {
aggregateFieldMetadataId: null,
primaryAxisGroupByFieldMetadataId: null,
@@ -94,8 +100,13 @@ export const ChartDataSourceDropdownContent = () => {
secondaryAxisOrderBy: null,
groupByFieldMetadataId: null,
groupBySubFieldName: null,
filter: {},
},
});
if (isDefined(currentObjectMetadataItemId)) {
resetChartDraftFiltersSettings(currentObjectMetadataItemId);
}
}
closeDropdown();
};
@@ -155,7 +166,7 @@ export const ChartDataSourceDropdownContent = () => {
>
<MenuItemSelect
text={objectMetadataItem.labelPlural}
selected={currentSource === objectMetadataItem.id}
selected={currentObjectMetadataItemId === objectMetadataItem.id}
focused={selectedItemId === objectMetadataItem.id}
LeftIcon={getIcon(objectMetadataItem.icon)}
onClick={() => {
@@ -11,6 +11,7 @@ import { SelectableListItem } from '@/ui/layout/selectable-list/components/Selec
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 { isDefined } from 'twenty-shared/utils';
import { MenuItemSelect } from 'twenty-ui/navigation';
import { type GraphOrderBy } from '~/generated/graphql';
@@ -28,6 +29,9 @@ export const ChartSortByGroupByFieldDropdownContent = () => {
throw new Error('Invalid configuration type');
}
if (!isDefined(widgetInEditMode?.objectMetadataId)) {
throw new Error('No data source in chart');
}
const dropdownId = useAvailableComponentInstanceIdOrThrow(
DropdownComponentInstanceContext,
);
@@ -50,7 +54,7 @@ export const ChartSortByGroupByFieldDropdownContent = () => {
};
const { getGroupBySortOptionLabel } = useGraphGroupBySortOptionLabels({
objectMetadataId: widgetInEditMode?.objectMetadataId,
objectMetadataId: widgetInEditMode.objectMetadataId,
});
return (
@@ -12,6 +12,7 @@ import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { type CompositeFieldSubFieldName } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { MenuItemSelect } from 'twenty-ui/navigation';
import { type GraphOrderBy } from '~/generated/graphql';
@@ -27,6 +28,10 @@ export const ChartXAxisSortBySelectionDropdownContent = () => {
throw new Error('Invalid configuration type');
}
if (!isDefined(widgetInEditMode?.objectMetadataId)) {
throw new Error('No data source in chart');
}
const dropdownId = useAvailableComponentInstanceIdOrThrow(
DropdownComponentInstanceContext,
);
@@ -41,7 +46,7 @@ export const ChartXAxisSortBySelectionDropdownContent = () => {
const { closeDropdown } = useCloseDropdown();
const { getXSortOptionLabel } = useGraphXSortOptionLabels({
objectMetadataId: widgetInEditMode?.objectMetadataId,
objectMetadataId: widgetInEditMode.objectMetadataId,
});
const handleSelect = (orderBy: GraphOrderBy) => {
@@ -1,16 +1,13 @@
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { type PageLayoutCommandMenuPage } from '@/command-menu/pages/page-layout/types/PageLayoutCommandMenuPage';
import { getPageLayoutIcon } from '@/command-menu/pages/page-layout/utils/getPageLayoutIcon';
import { getPageLayoutPageTitle } from '@/command-menu/pages/page-layout/utils/getPageLayoutPageTitle';
import { type CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { type IconComponent } from 'twenty-ui/display';
type NavigatePageLayoutCommandMenuProps = {
commandMenuPage:
| CommandMenuPages.PageLayoutWidgetTypeSelect
| CommandMenuPages.PageLayoutGraphTypeSelect
| CommandMenuPages.PageLayoutIframeConfig;
commandMenuPage: PageLayoutCommandMenuPage;
pageTitle?: string;
pageIcon?: IconComponent;
};
@@ -0,0 +1,58 @@
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
import { getChartFiltersSettingsInstanceId } from '@/command-menu/pages/page-layout/utils/getChartFiltersSettingsInstanceId';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { hasInitializedCurrentRecordFilterGroupsComponentFamilyState } from '@/views/states/hasInitializedCurrentRecordFilterGroupsComponentFamilyState';
import { hasInitializedCurrentRecordFiltersComponentFamilyState } from '@/views/states/hasInitializedCurrentRecordFiltersComponentFamilyState';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const useResetChartDraftFiltersSettings = () => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const resetChartDraftFiltersSettings = useRecoilCallback(
({ set }) =>
(objectMetadataItemId: string) => {
if (!isDefined(widgetInEditMode)) {
return;
}
const { instanceId } = getChartFiltersSettingsInstanceId({
widgetId: widgetInEditMode.id,
objectMetadataItemId: objectMetadataItemId,
});
set(
hasInitializedCurrentRecordFilterGroupsComponentFamilyState.atomFamily(
{
familyKey: {},
instanceId,
},
),
false,
);
set(
hasInitializedCurrentRecordFiltersComponentFamilyState.atomFamily({
familyKey: {},
instanceId,
}),
false,
);
set(currentRecordFiltersComponentState.atomFamily({ instanceId }), []);
set(
currentRecordFilterGroupsComponentState.atomFamily({ instanceId }),
[],
);
},
[widgetInEditMode],
);
return {
resetChartDraftFiltersSettings,
};
};
@@ -0,0 +1,9 @@
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const hasInitializedChartFiltersComponentState =
createComponentState<boolean>({
key: 'hasInitializedCurrentRecordFiltersComponentFamilyState',
defaultValue: false,
componentInstanceContext: RecordFiltersComponentInstanceContext,
});
@@ -0,0 +1,7 @@
import { type RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
export type ChartFilters = {
recordFilters?: RecordFilter[];
recordFilterGroups?: RecordFilterGroup[];
};
@@ -0,0 +1,15 @@
import { type ChartFilters } from '@/command-menu/pages/page-layout/types/ChartFilters';
import { type ChartWidgetConfiguration } from '@/command-menu/pages/page-layout/types/ChartWidgetConfiguration';
import { type ModifiedProperties } from 'twenty-shared/types';
import { type PageLayoutWidget, type WidgetType } from '~/generated/graphql';
export type ChartWidget = ModifiedProperties<
PageLayoutWidget,
{
type: WidgetType.GRAPH;
configuration: ModifiedProperties<
ChartWidgetConfiguration,
{ filter?: ChartFilters }
>;
}
>;
@@ -0,0 +1,14 @@
import {
type BarChartConfiguration,
type GaugeChartConfiguration,
type LineChartConfiguration,
type NumberChartConfiguration,
type PieChartConfiguration,
} from '~/generated/graphql';
export type ChartWidgetConfiguration =
| BarChartConfiguration
| GaugeChartConfiguration
| LineChartConfiguration
| NumberChartConfiguration
| PieChartConfiguration;
@@ -3,4 +3,5 @@ import { type CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
export type PageLayoutCommandMenuPage =
| CommandMenuPages.PageLayoutWidgetTypeSelect
| CommandMenuPages.PageLayoutGraphTypeSelect
| CommandMenuPages.PageLayoutIframeConfig;
| CommandMenuPages.PageLayoutIframeConfig
| CommandMenuPages.PageLayoutGraphFilter;
@@ -0,0 +1,13 @@
export const getChartFiltersSettingsInstanceId = ({
widgetId,
objectMetadataItemId,
}: {
widgetId: string;
objectMetadataItemId: string;
}) => {
const instanceId = `chart-filters-widget-${widgetId}-${objectMetadataItemId}`;
return {
instanceId,
};
};
@@ -1,7 +1,12 @@
import { type PageLayoutCommandMenuPage } from '@/command-menu/pages/page-layout/types/PageLayoutCommandMenuPage';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { assertUnreachable } from 'twenty-shared/utils';
import { IconAppWindow, IconChartPie, IconFrame } from 'twenty-ui/display';
import {
IconAppWindow,
IconChartPie,
IconFilter,
IconFrame,
} from 'twenty-ui/display';
export const getPageLayoutIcon = (page: PageLayoutCommandMenuPage) => {
switch (page) {
@@ -11,6 +16,8 @@ export const getPageLayoutIcon = (page: PageLayoutCommandMenuPage) => {
return IconChartPie;
case CommandMenuPages.PageLayoutIframeConfig:
return IconFrame;
case CommandMenuPages.PageLayoutGraphFilter:
return IconFilter;
default:
assertUnreachable(page);
}
@@ -11,6 +11,8 @@ export const getPageLayoutPageTitle = (page: PageLayoutCommandMenuPage) => {
return t`Select Graph Type`;
case CommandMenuPages.PageLayoutIframeConfig:
return t`Configure iFrame`;
case CommandMenuPages.PageLayoutGraphFilter:
return t`Configure filters`;
default:
assertUnreachable(page);
}
@@ -0,0 +1,8 @@
import { type ChartWidget } from '@/command-menu/pages/page-layout/types/ChartWidget';
import { WidgetType } from '~/generated/graphql';
export const isChartWidget = (
pageLayoutWidget: any,
): pageLayoutWidget is ChartWidget => {
return pageLayoutWidget.type === WidgetType.GRAPH;
};
@@ -1,10 +1,14 @@
import { type ModifiedProperties, type Nullable } from 'twenty-shared/types';
import {
type PageLayoutTab as PageLayoutTabGenerated,
type PageLayoutWidget,
} from '~/generated/graphql';
export type PageLayoutTab = Omit<PageLayoutTabGenerated, 'widgets'> & {
widgets: PageLayoutWidget[];
widgets: ModifiedProperties<
PageLayoutWidget,
{ objectMetadataId?: Nullable<string> }
>[];
/**
* Only available behind IS_RECORD_PAGE_LAYOUT_ENABLED for now.
*/
@@ -5,7 +5,10 @@ import { generateGroupByQuery } from '@/page-layout/widgets/graph/utils/generate
import { generateGroupByQueryVariablesFromBarChartConfiguration } from '@/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarChartConfiguration';
import { useQuery } from '@apollo/client';
import { useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
import {
computeRecordGqlOperationFilter,
isDefined,
} from 'twenty-shared/utils';
import { type BarChartConfiguration } from '~/generated-metadata/graphql';
export const useGraphWidgetGroupByQuery = ({
@@ -46,17 +49,34 @@ export const useGraphWidgetGroupByQuery = ({
throw new Error('Aggregate operation not found');
}
const gqlOperationFilter = computeRecordGqlOperationFilter({
fields: objectMetadataItem.fields,
filterValueDependencies: {},
recordFilters: configuration.filter?.recordFilters ?? [],
recordFilterGroups: configuration.filter?.recordFilterGroups ?? [],
});
const filterQueryVariables = {
filter: gqlOperationFilter,
};
const groupByQueryVariables =
generateGroupByQueryVariablesFromBarChartConfiguration({
objectMetadataItem,
barChartConfiguration: configuration,
aggregateOperation,
});
const variables = {
...groupByQueryVariables,
...filterQueryVariables,
};
const query = generateGroupByQuery({
objectMetadataItem,
aggregateOperations: [aggregateOperation],
});
const variables = generateGroupByQueryVariablesFromBarChartConfiguration({
objectMetadataItem,
barChartConfiguration: configuration,
aggregateOperation,
});
const apolloCoreClient = useApolloCoreClient();
const { data, loading, error, refetch } = useQuery(query, {
@@ -69,7 +89,6 @@ export const useGraphWidgetGroupByQuery = ({
loading,
error,
refetch,
variables,
aggregateOperation,
};
};
@@ -0,0 +1 @@
export type ModifiedProperties<T, R> = Omit<T, keyof R> & R;
@@ -29,6 +29,7 @@ export { FILTERABLE_FIELD_TYPES } from './FilterableFieldType';
export type { FromTo } from './FromToType';
export type { IsEmptyRecord } from './IsEmptyRecord.type';
export type { IsExactly } from './IsExactly';
export type { ModifiedProperties } from './ModifiedProperties';
export type { NonNullableRequired } from './NonNullableRequired';
export type { Nullable } from './Nullable';
export type { NullablePartial } from './NullablePartial';