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:
-67
@@ -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>
|
||||
);
|
||||
};
|
||||
+131
@@ -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>
|
||||
);
|
||||
};
|
||||
+63
@@ -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
-7
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+17
-15
@@ -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}
|
||||
/>
|
||||
|
||||
+16
-5
@@ -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={() => {
|
||||
|
||||
+5
-1
@@ -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 (
|
||||
|
||||
+6
-1
@@ -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) => {
|
||||
|
||||
+2
-5
@@ -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;
|
||||
};
|
||||
|
||||
+58
@@ -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,
|
||||
};
|
||||
};
|
||||
+9
@@ -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 }
|
||||
>;
|
||||
}
|
||||
>;
|
||||
+14
@@ -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;
|
||||
+2
-1
@@ -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;
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
export const getChartFiltersSettingsInstanceId = ({
|
||||
widgetId,
|
||||
objectMetadataItemId,
|
||||
}: {
|
||||
widgetId: string;
|
||||
objectMetadataItemId: string;
|
||||
}) => {
|
||||
const instanceId = `chart-filters-widget-${widgetId}-${objectMetadataItemId}`;
|
||||
|
||||
return {
|
||||
instanceId,
|
||||
};
|
||||
};
|
||||
+8
-1
@@ -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);
|
||||
}
|
||||
|
||||
+2
@@ -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);
|
||||
}
|
||||
|
||||
+8
@@ -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.
|
||||
*/
|
||||
|
||||
+27
-8
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user