[Dashboards] Relation fields groupby (#16093)

This commit is contained in:
nitin
2025-11-28 00:58:53 +05:30
committed by GitHub
parent f23aa632a7
commit fa87603fd8
22 changed files with 597 additions and 136 deletions
@@ -25,12 +25,9 @@ import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectab
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { isFieldMetadataDateKind } from 'twenty-shared/utils';
import {
FieldMetadataType,
GraphType,
type PageLayoutWidget,
} from '~/generated/graphql';
import { GraphType, type PageLayoutWidget } from '~/generated/graphql';
const StyledCommandMenuContainer = styled.div`
display: flex;
@@ -136,9 +133,7 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => {
(field) => field.id === primaryAxisFieldMetadataId,
);
const isPrimaryAxisDate =
primaryAxisField?.type === FieldMetadataType.DATE ||
primaryAxisField?.type === FieldMetadataType.DATE_TIME;
const isPrimaryAxisDate = isFieldMetadataDateKind(primaryAxisField?.type);
const primaryAxisDateGranularity =
configuration.__typename === 'BarChartConfiguration' ||
@@ -1,4 +1,5 @@
import { ChartGroupByFieldSelectionCompositeFieldView } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionCompositeFieldView';
import { ChartGroupByFieldSelectionRelationFieldView } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionRelationFieldView';
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';
@@ -23,6 +24,7 @@ import { useMemo, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { MenuItemSelect } from 'twenty-ui/navigation';
import { RelationType } from '~/generated/graphql';
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
type ChartGroupByFieldSelectionDropdownContentBaseProps<
@@ -43,6 +45,9 @@ export const ChartGroupByFieldSelectionDropdownContentBase = <
const [selectedCompositeField, setSelectedCompositeField] =
useState<FieldMetadataItem | null>(null);
const [selectedRelationField, setSelectedRelationField] =
useState<FieldMetadataItem | null>(null);
const { objectMetadataItems } = useObjectMetadataItems();
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
@@ -77,8 +82,15 @@ export const ChartGroupByFieldSelectionDropdownContentBase = <
items: sourceObjectMetadataItem?.fields || [],
searchQuery,
getSearchableValues: (item) => [item.label, item.name],
// TODO: remove the relation filter once group by is supported for relation fields
}).filter((field) => !isFieldRelation(field) && !field.isSystem),
}).filter((field) => {
if (field.isSystem === true) {
return false;
}
if (isFieldRelation(field)) {
return field.relation?.type === RelationType.MANY_TO_ONE;
}
return true;
}),
[sourceObjectMetadataItem?.fields, searchQuery],
);
@@ -94,20 +106,26 @@ export const ChartGroupByFieldSelectionDropdownContentBase = <
}
const handleSelectField = (fieldMetadataItem: FieldMetadataItem) => {
if (isFieldRelation(fieldMetadataItem)) {
setSelectedRelationField(fieldMetadataItem);
return;
}
if (isCompositeFieldType(fieldMetadataItem.type)) {
setSelectedCompositeField(fieldMetadataItem);
} else {
updateCurrentWidgetConfig({
configToUpdate: buildChartGroupByFieldConfigUpdate({
configuration,
fieldMetadataIdKey,
subFieldNameKey,
fieldId: fieldMetadataItem.id,
subFieldName: null,
}),
});
closeDropdown();
return;
}
updateCurrentWidgetConfig({
configToUpdate: buildChartGroupByFieldConfigUpdate({
configuration,
fieldMetadataIdKey,
subFieldNameKey,
fieldId: fieldMetadataItem.id,
subFieldName: null,
}),
});
closeDropdown();
};
const handleSelectNone = () => {
@@ -123,11 +141,15 @@ export const ChartGroupByFieldSelectionDropdownContentBase = <
closeDropdown();
};
const handleBack = () => {
const handleBackFromComposite = () => {
setSelectedCompositeField(null);
};
const handleSelectSubField = (subFieldName: string) => {
const handleBackFromRelation = () => {
setSelectedRelationField(null);
};
const handleSelectCompositeSubField = (subFieldName: string) => {
if (!isDefined(selectedCompositeField)) {
return;
}
@@ -144,13 +166,41 @@ export const ChartGroupByFieldSelectionDropdownContentBase = <
closeDropdown();
};
const handleSelectRelationSubField = (subFieldName: string) => {
if (!isDefined(selectedRelationField)) {
return;
}
updateCurrentWidgetConfig({
configToUpdate: buildChartGroupByFieldConfigUpdate({
configuration,
fieldMetadataIdKey,
subFieldNameKey,
fieldId: selectedRelationField.id,
subFieldName,
}),
});
closeDropdown();
};
if (isDefined(selectedRelationField)) {
return (
<ChartGroupByFieldSelectionRelationFieldView
relationField={selectedRelationField}
currentSubFieldName={currentSubFieldName}
onBack={handleBackFromRelation}
onSelectSubField={handleSelectRelationSubField}
/>
);
}
if (isDefined(selectedCompositeField)) {
return (
<ChartGroupByFieldSelectionCompositeFieldView
compositeField={selectedCompositeField}
currentSubFieldName={currentSubFieldName}
onBack={handleBack}
onSelectSubField={handleSelectSubField}
onBack={handleBackFromComposite}
onSelectSubField={handleSelectCompositeSubField}
/>
);
}
@@ -195,11 +245,15 @@ export const ChartGroupByFieldSelectionDropdownContentBase = <
text={fieldMetadataItem.label}
selected={
!isCompositeFieldType(fieldMetadataItem.type) &&
!isFieldRelation(fieldMetadataItem) &&
currentGroupByFieldMetadataId === fieldMetadataItem.id
}
focused={selectedItemId === fieldMetadataItem.id}
LeftIcon={getIcon(fieldMetadataItem.icon)}
hasSubMenu={isCompositeFieldType(fieldMetadataItem.type)}
hasSubMenu={
isCompositeFieldType(fieldMetadataItem.type) ||
isFieldRelation(fieldMetadataItem)
}
onClick={() => {
handleSelectField(fieldMetadataItem);
}}
@@ -0,0 +1,174 @@
import { ChartGroupByFieldSelectionCompositeFieldView } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionCompositeFieldView';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
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 { 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 { useMemo, useState } from 'react';
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
import { IconChevronLeft, useIcons } from 'twenty-ui/display';
import { MenuItem, MenuItemSelect } from 'twenty-ui/navigation';
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
type ChartGroupByFieldSelectionRelationFieldViewProps = {
relationField: FieldMetadataItem;
currentSubFieldName: string | undefined;
onBack: () => void;
onSelectSubField: (subFieldName: string) => void;
};
export const ChartGroupByFieldSelectionRelationFieldView = ({
relationField,
currentSubFieldName,
onBack,
onSelectSubField,
}: ChartGroupByFieldSelectionRelationFieldViewProps) => {
const { getIcon } = useIcons();
const [searchQuery, setSearchQuery] = useState('');
const [selectedCompositeField, setSelectedCompositeField] =
useState<FieldMetadataItem | null>(null);
const dropdownId = useAvailableComponentInstanceIdOrThrow(
DropdownComponentInstanceContext,
);
const selectedItemId = useRecoilComponentValue(
selectedItemIdComponentState,
dropdownId,
);
const { objectMetadataItems } = useObjectMetadataItems();
const targetObjectNameSingular =
relationField.relation?.targetObjectMetadata?.nameSingular;
const targetObjectMetadataItem = useMemo(
() =>
objectMetadataItems.find(
(item) => item.nameSingular === targetObjectNameSingular,
),
[objectMetadataItems, targetObjectNameSingular],
);
const availableFields = useMemo(() => {
if (!isDefined(targetObjectMetadataItem)) {
return [];
}
return filterBySearchQuery({
items: targetObjectMetadataItem.fields.filter(
(field) =>
!field.isSystem &&
!isFieldRelation(field) &&
// TODO: Backend doesn't fully support date fields for relation fields yet so we hide them for now. https://github.com/twentyhq/core-team-issues/issues/1935
!isFieldMetadataDateKind(field.type),
),
searchQuery,
getSearchableValues: (field) => [field.label, field.name],
});
}, [targetObjectMetadataItem, searchQuery]);
const handleSelectField = (fieldMetadataItem: FieldMetadataItem) => {
if (isCompositeFieldType(fieldMetadataItem.type)) {
setSelectedCompositeField(fieldMetadataItem);
} else {
onSelectSubField(fieldMetadataItem.name);
}
};
const handleSelectCompositeSubField = (compositeSubFieldName: string) => {
if (!isDefined(selectedCompositeField)) {
return;
}
onSelectSubField(`${selectedCompositeField.name}.${compositeSubFieldName}`);
};
const handleBackFromComposite = () => {
setSelectedCompositeField(null);
};
const [currentNestedFieldName, currentNestedSubFieldName] =
currentSubFieldName?.split('.') ?? [];
if (isDefined(selectedCompositeField)) {
return (
<ChartGroupByFieldSelectionCompositeFieldView
compositeField={selectedCompositeField}
currentSubFieldName={currentNestedSubFieldName}
onBack={handleBackFromComposite}
onSelectSubField={handleSelectCompositeSubField}
/>
);
}
return (
<>
<DropdownMenuHeader
StartComponent={
<DropdownMenuHeaderLeftComponent
onClick={onBack}
Icon={IconChevronLeft}
/>
}
>
{relationField.label}
</DropdownMenuHeader>
<DropdownMenuSearchInput
autoFocus
type="text"
placeholder={t`Search fields`}
onChange={(event) => setSearchQuery(event.target.value)}
value={searchQuery}
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
{availableFields.length === 0 ? (
<MenuItem text={t`No fields available`} />
) : (
<SelectableList
selectableListInstanceId={dropdownId}
focusId={dropdownId}
selectableItemIdArray={availableFields.map((field) => field.id)}
>
{availableFields.map((fieldMetadataItem) => (
<SelectableListItem
key={fieldMetadataItem.id}
itemId={fieldMetadataItem.id}
onEnter={() => {
handleSelectField(fieldMetadataItem);
}}
>
<MenuItemSelect
text={fieldMetadataItem.label}
selected={
!isCompositeFieldType(fieldMetadataItem.type) &&
currentNestedFieldName === fieldMetadataItem.name
}
focused={selectedItemId === fieldMetadataItem.id}
LeftIcon={getIcon(fieldMetadataItem.icon)}
hasSubMenu={isCompositeFieldType(fieldMetadataItem.type)}
onClick={() => {
handleSelectField(fieldMetadataItem);
}}
/>
</SelectableListItem>
))}
</SelectableList>
)}
</DropdownMenuItemsContainer>
</>
);
};
@@ -84,6 +84,7 @@ export const useChartSettingsValues = ({
? getFieldLabelWithSubField({
field: groupByFieldX,
subFieldName: groupBySubFieldNameX,
objectMetadataItems,
})
: undefined;
@@ -181,6 +182,7 @@ export const useChartSettingsValues = ({
? getFieldLabelWithSubField({
field: pieChartGroupByField,
subFieldName: finalGroupBySubFieldNameY,
objectMetadataItems,
})
: undefined;
@@ -32,6 +32,7 @@ export const useGraphGroupBySortOptionLabels = ({
const fieldLabel = getFieldLabelWithSubField({
field,
subFieldName: groupBySubFieldName,
objectMetadataItems,
});
switch (graphOrderBy) {
@@ -39,6 +39,7 @@ export const useGraphXSortOptionLabels = ({
const fieldLabel = getFieldLabelWithSubField({
field: groupByField,
subFieldName: groupBySubFieldNameX,
objectMetadataItems,
});
const aggregateField = objectMetadataItem?.fields.find(
@@ -1,5 +1,9 @@
import { getRelationFieldLabel } from '@/command-menu/pages/page-layout/utils/getRelationFieldLabel';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
import { type CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType';
import { t } from '@lingui/core/macro';
import { type CompositeFieldSubFieldName } from 'twenty-shared/types';
@@ -8,24 +12,31 @@ import { isDefined } from 'twenty-shared/utils';
export const getFieldLabelWithSubField = ({
field,
subFieldName,
objectMetadataItems,
}: {
field: FieldMetadataItem | undefined;
subFieldName?: CompositeFieldSubFieldName;
subFieldName?: CompositeFieldSubFieldName | string;
objectMetadataItems?: ObjectMetadataItem[];
}): string => {
const subFieldNameLabel =
isDefined(subFieldName) && isDefined(field)
? getCompositeSubFieldLabel(
field.type as CompositeFieldType,
subFieldName,
)
: undefined;
if (!isDefined(field?.label)) {
return t`Field`;
}
const fieldLabel =
isDefined(subFieldNameLabel) && isDefined(field?.label)
? `${field.label} ${subFieldNameLabel}`
: isDefined(field?.label)
? field.label
: t`Field`;
if (!isDefined(subFieldName)) {
return field.label;
}
return fieldLabel;
if (isFieldRelation(field)) {
return getRelationFieldLabel(field, subFieldName, objectMetadataItems);
}
if (isCompositeFieldType(field.type)) {
const subFieldLabel = getCompositeSubFieldLabel(
field.type as CompositeFieldType,
subFieldName as CompositeFieldSubFieldName,
);
return subFieldLabel ? `${field.label} ${subFieldLabel}` : field.label;
}
return field.label;
};
@@ -0,0 +1,52 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { type CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType';
import { type CompositeFieldSubFieldName } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const getRelationFieldLabel = (
field: FieldMetadataItem,
subFieldName: string,
objectMetadataItems: ObjectMetadataItem[] | undefined,
): string => {
if (!isDefined(objectMetadataItems)) {
return field.label;
}
const targetObjectNameSingular =
field.relation?.targetObjectMetadata?.nameSingular;
const targetObjectMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === targetObjectNameSingular,
);
if (!isDefined(targetObjectMetadataItem)) {
return field.label;
}
const [nestedFieldName, nestedSubFieldName] = subFieldName.split('.');
const nestedField = targetObjectMetadataItem.fields.find(
(f) => f.name === nestedFieldName,
);
if (!isDefined(nestedField)) {
return field.label;
}
if (
!isDefined(nestedSubFieldName) ||
!isCompositeFieldType(nestedField.type)
) {
return `${field.label} ${nestedField.label}`;
}
const compositeSubFieldLabel = getCompositeSubFieldLabel(
nestedField.type as CompositeFieldType,
nestedSubFieldName as CompositeFieldSubFieldName,
);
return compositeSubFieldLabel
? `${field.label} ${nestedField.label} ${compositeSubFieldLabel}`
: `${field.label} ${nestedField.label}`;
};
@@ -2,6 +2,7 @@ import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
@@ -21,6 +22,10 @@ const shouldHideDateGranularityBasedOnFieldType = (
return true;
}
if (isFieldRelation(field)) {
return true;
}
return !isFieldMetadataDateKind(field.type);
};
@@ -13,9 +13,9 @@ import { useGetAvailableFieldsForCalendar } from '@/views/view-picker/hooks/useG
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useSetRecoilState } from 'recoil';
import { isFieldMetadataDateKind } from 'twenty-shared/utils';
import { IconChevronLeft, IconSettings, useIcons } from 'twenty-ui/display';
import { MenuItem, MenuItemSelect } from 'twenty-ui/navigation';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const ObjectOptionsDropdownCalendarFieldsContent = () => {
const { t } = useLingui();
@@ -32,10 +32,8 @@ export const ObjectOptionsDropdownCalendarFieldsContent = () => {
const setRecordIndexCalendarFieldMetadataId = useSetRecoilState(
recordIndexCalendarFieldMetadataIdState,
);
const availableFieldsForCalendar = objectMetadataItem.fields.filter(
(field) =>
field.type === FieldMetadataType.DATE ||
field.type === FieldMetadataType.DATE_TIME,
const availableFieldsForCalendar = objectMetadataItem.fields.filter((field) =>
isFieldMetadataDateKind(field.type),
);
const calendarFieldMetadata = currentView?.calendarFieldMetadataId
@@ -14,11 +14,10 @@ import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDim
import { filterGroupByResults } from '@/page-layout/widgets/graph/utils/filterGroupByResults';
import { getFieldKey } from '@/page-layout/widgets/graph/utils/getFieldKey';
import { type BarDatum } from '@nivo/bar';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
import { GraphType } from '~/generated-metadata/graphql';
import {
AxisNameDisplay,
FieldMetadataType,
type BarChartConfiguration,
} from '~/generated/graphql';
@@ -150,9 +149,7 @@ export const transformGroupByDataToBarChartData = ({
const showDataLabels = configuration.displayDataLabel ?? false;
const showLegend = configuration.displayLegend ?? true;
const isDateField =
groupByFieldX.type === FieldMetadataType.DATE ||
groupByFieldX.type === FieldMetadataType.DATE_TIME;
const isDateField = isFieldMetadataDateKind(groupByFieldX.type);
const omitNullValues = configuration.omitNullValues ?? false;
@@ -105,10 +105,14 @@ export const GraphWidgetPieChart = ({
const tooltipData = createTooltipData(datum);
if (!isDefined(tooltipData)) return null;
const handleTooltipClick: (() => void) | undefined = isDefined(onSliceClick)
? () => handleSliceClick(datum)
: undefined;
return (
<GraphWidgetTooltip
items={[tooltipData.tooltipItem]}
onGraphWidgetTooltipClick={() => handleSliceClick(datum)}
onGraphWidgetTooltipClick={handleTooltipClick}
/>
);
};
@@ -4,7 +4,7 @@ import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
import { FieldMetadataType } from '~/generated-metadata/graphql';
describe('buildGroupByFieldObject', () => {
it('should return field with Id suffix for relation fields', () => {
it('should return field with Id suffix for relation fields without subFieldName', () => {
const field = {
name: 'company',
type: FieldMetadataType.RELATION,
@@ -15,6 +15,33 @@ describe('buildGroupByFieldObject', () => {
expect(result).toEqual({ companyId: true });
});
it('should return nested object for relation field with subFieldName', () => {
const field = {
name: 'company',
type: FieldMetadataType.RELATION,
} as any;
const result = buildGroupByFieldObject({ field, subFieldName: 'name' });
expect(result).toEqual({ company: { name: true } });
});
it('should return deeply nested object for relation with composite subfield', () => {
const field = {
name: 'company',
type: FieldMetadataType.RELATION,
} as any;
const result = buildGroupByFieldObject({
field,
subFieldName: 'address.addressCity',
});
expect(result).toEqual({
company: { address: { addressCity: true } },
});
});
it('should return nested object for composite fields with subfield', () => {
const field = {
name: 'name',
@@ -1,4 +1,6 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { buildDateFilterForDayGranularity } from '@/page-layout/widgets/graph/utils/buildDateFilterForDayGranularity';
import { buildDateRangeFiltersForGranularity } from '@/page-layout/widgets/graph/utils/buildDateRangeFiltersForGranularity';
@@ -69,6 +71,13 @@ export const buildFilterFromChartBucket = ({
];
}
if (
isFieldRelation(fieldMetadataItem) ||
isFieldMorphRelation(fieldMetadataItem)
) {
return [];
}
if (isFieldMetadataDateKind(fieldMetadataItem.type)) {
const parsedBucketDate = new Date(String(bucketRawValue));
@@ -8,9 +8,12 @@ import {
type FirstDayOfTheWeek,
ObjectRecordGroupByDateGranularity,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export type GroupByFieldObject = Record<
string,
boolean | Record<string, boolean | string | Record<string, boolean | string>>
>;
export const buildGroupByFieldObject = ({
field,
@@ -22,15 +25,35 @@ export const buildGroupByFieldObject = ({
subFieldName?: string | null;
dateGranularity?: ObjectRecordGroupByDateGranularity;
firstDayOfTheWeek?: number | null;
}): Record<string, boolean | Record<string, boolean | string>> => {
}): GroupByFieldObject => {
const isRelation = isFieldRelation(field) || isFieldMorphRelation(field);
const isComposite = isCompositeFieldType(field.type);
const isDateField =
field.type === FieldMetadataType.DATE ||
field.type === FieldMetadataType.DATE_TIME;
const isDateField = isFieldMetadataDateKind(field.type);
if (isRelation) {
return { [`${field.name}Id`]: true };
if (!isDefined(subFieldName)) {
return { [`${field.name}Id`]: true };
}
const parts = subFieldName.split('.');
const nestedFieldName = parts[0];
const nestedSubFieldName = parts[1];
if (isDefined(nestedSubFieldName)) {
return {
[field.name]: {
[nestedFieldName]: {
[nestedSubFieldName]: true,
},
},
};
}
return {
[field.name]: {
[nestedFieldName]: true,
},
};
}
if (isComposite) {
@@ -1,17 +1,22 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant';
import { getGroupByOrderBy } from '@/page-layout/widgets/graph/utils/getGroupByOrderBy';
import {
type AggregateOrderByWithGroupByField,
type ObjectRecordOrderByForCompositeField,
type ObjectRecordOrderByForRelationField,
type ObjectRecordOrderByForScalarField,
type ObjectRecordOrderByWithGroupByDateField,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
import {
type BarChartConfiguration,
type LineChartConfiguration,
} from '~/generated/graphql';
import { buildGroupByFieldObject } from './buildGroupByFieldObject';
import {
buildGroupByFieldObject,
type GroupByFieldObject,
} from './buildGroupByFieldObject';
export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({
objectMetadataItem,
@@ -51,27 +56,34 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({
);
}
const groupBy: Array<
Record<string, boolean | Record<string, boolean | string>>
> = [];
const isFieldXDate = isFieldMetadataDateKind(groupByFieldX.type);
const groupBy: Array<GroupByFieldObject> = [];
groupBy.push(
buildGroupByFieldObject({
field: groupByFieldX,
subFieldName: groupBySubFieldNameX,
dateGranularity:
chartConfiguration.primaryAxisDateGranularity ?? undefined,
dateGranularity: isFieldXDate
? (chartConfiguration.primaryAxisDateGranularity ??
GRAPH_DEFAULT_DATE_GRANULARITY)
: undefined,
firstDayOfTheWeek,
}),
);
if (isDefined(groupByFieldY)) {
const isFieldYDate = isFieldMetadataDateKind(groupByFieldY.type);
groupBy.push(
buildGroupByFieldObject({
field: groupByFieldY,
subFieldName: groupBySubFieldNameY,
dateGranularity:
chartConfiguration.secondaryAxisGroupByDateGranularity ?? undefined,
dateGranularity: isFieldYDate
? (chartConfiguration.secondaryAxisGroupByDateGranularity ??
GRAPH_DEFAULT_DATE_GRANULARITY)
: undefined,
firstDayOfTheWeek,
}),
);
@@ -82,6 +94,7 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({
| ObjectRecordOrderByForScalarField
| ObjectRecordOrderByWithGroupByDateField
| ObjectRecordOrderByForCompositeField
| ObjectRecordOrderByForRelationField
> = [];
if (isDefined(chartConfiguration.primaryAxisOrderBy)) {
@@ -91,8 +104,10 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({
groupByField: groupByFieldX,
groupBySubFieldName: chartConfiguration.primaryAxisGroupBySubFieldName,
aggregateOperation,
dateGranularity:
chartConfiguration.primaryAxisDateGranularity ?? undefined,
dateGranularity: isFieldXDate
? (chartConfiguration.primaryAxisDateGranularity ??
GRAPH_DEFAULT_DATE_GRANULARITY)
: undefined,
}),
);
}
@@ -100,6 +115,8 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({
isDefined(groupByFieldY) &&
isDefined(chartConfiguration.secondaryAxisOrderBy)
) {
const isFieldYDateForOrderBy = isFieldMetadataDateKind(groupByFieldY.type);
orderBy.push(
getGroupByOrderBy({
graphOrderBy: chartConfiguration.secondaryAxisOrderBy,
@@ -107,8 +124,10 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({
groupBySubFieldName:
chartConfiguration.secondaryAxisGroupBySubFieldName,
aggregateOperation,
dateGranularity:
chartConfiguration.secondaryAxisGroupByDateGranularity ?? undefined,
dateGranularity: isFieldYDateForOrderBy
? (chartConfiguration.secondaryAxisGroupByDateGranularity ??
GRAPH_DEFAULT_DATE_GRANULARITY)
: undefined,
}),
);
}
@@ -1,14 +1,19 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant';
import { getGroupByOrderBy } from '@/page-layout/widgets/graph/utils/getGroupByOrderBy';
import {
type AggregateOrderByWithGroupByField,
type ObjectRecordOrderByForCompositeField,
type ObjectRecordOrderByForRelationField,
type ObjectRecordOrderByForScalarField,
type ObjectRecordOrderByWithGroupByDateField,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
import { type PieChartConfiguration } from '~/generated/graphql';
import { buildGroupByFieldObject } from './buildGroupByFieldObject';
import {
buildGroupByFieldObject,
type GroupByFieldObject,
} from './buildGroupByFieldObject';
export const generateGroupByQueryVariablesFromPieChartConfiguration = ({
objectMetadataItem,
@@ -38,13 +43,16 @@ export const generateGroupByQueryVariablesFromPieChartConfiguration = ({
);
}
const groupBy: Array<
Record<string, boolean | Record<string, boolean | string>>
> = [
const isFieldDate = isFieldMetadataDateKind(groupByField.type);
const groupBy: Array<GroupByFieldObject> = [
buildGroupByFieldObject({
field: groupByField,
subFieldName: groupBySubFieldName,
dateGranularity,
dateGranularity: isFieldDate
? (dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY)
: undefined,
firstDayOfTheWeek,
}),
];
@@ -54,6 +62,7 @@ export const generateGroupByQueryVariablesFromPieChartConfiguration = ({
| ObjectRecordOrderByForScalarField
| ObjectRecordOrderByWithGroupByDateField
| ObjectRecordOrderByForCompositeField
| ObjectRecordOrderByForRelationField
> = [];
if (isDefined(chartConfiguration.orderBy)) {
@@ -63,7 +72,9 @@ export const generateGroupByQueryVariablesFromPieChartConfiguration = ({
groupByField,
groupBySubFieldName,
aggregateOperation,
dateGranularity,
dateGranularity: isFieldDate
? (dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY)
: undefined,
}),
);
}
@@ -0,0 +1,60 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant';
import { getRelationFieldOrderBy } from '@/page-layout/widgets/graph/utils/getRelationFieldOrderBy';
import {
type ObjectRecordGroupByDateGranularity,
type ObjectRecordOrderByForCompositeField,
type ObjectRecordOrderByForRelationField,
type ObjectRecordOrderByForScalarField,
type ObjectRecordOrderByWithGroupByDateField,
type OrderByDirection,
} from 'twenty-shared/types';
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
export const getFieldOrderBy = (
groupByField: FieldMetadataItem,
groupBySubFieldName: string | null | undefined,
dateGranularity: ObjectRecordGroupByDateGranularity | undefined,
direction: OrderByDirection,
):
| ObjectRecordOrderByForScalarField
| ObjectRecordOrderByWithGroupByDateField
| ObjectRecordOrderByForCompositeField
| ObjectRecordOrderByForRelationField => {
if (isCompositeFieldType(groupByField.type)) {
if (!isDefined(groupBySubFieldName)) {
throw new Error(
`Group by subFieldName is required for composite fields (field: ${groupByField.name})`,
);
}
return {
[groupByField.name]: {
[groupBySubFieldName]: direction,
},
};
}
if (isFieldMetadataDateKind(groupByField.type)) {
return {
[groupByField.name]: {
orderBy: direction,
granularity: dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY,
},
};
}
if (isFieldRelation(groupByField) || isFieldMorphRelation(groupByField)) {
return getRelationFieldOrderBy(
groupByField,
groupBySubFieldName,
direction,
);
}
return {
[groupByField.name]: direction,
};
};
@@ -1,36 +1,17 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant';
import { getFieldOrderBy } from '@/page-layout/widgets/graph/utils/getFieldOrderBy';
import { mapOrderByToDirection } from '@/page-layout/widgets/graph/utils/mapOrderByToDirection';
import {
OrderByDirection,
type AggregateOrderByWithGroupByField,
type ObjectRecordGroupByDateGranularity,
type ObjectRecordOrderByForCompositeField,
type ObjectRecordOrderByForRelationField,
type ObjectRecordOrderByForScalarField,
type ObjectRecordOrderByWithGroupByDateField,
} from 'twenty-shared/types';
import {
assertUnreachable,
isDefined,
isFieldMetadataDateKind,
} from 'twenty-shared/utils';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
import { GraphOrderBy } from '~/generated/graphql';
const mapOrderByToDirection = (orderByEnum: GraphOrderBy): OrderByDirection => {
switch (orderByEnum) {
case GraphOrderBy.FIELD_ASC:
return OrderByDirection.AscNullsLast;
case GraphOrderBy.FIELD_DESC:
return OrderByDirection.DescNullsLast;
case GraphOrderBy.VALUE_ASC:
return OrderByDirection.AscNullsLast;
case GraphOrderBy.VALUE_DESC:
return OrderByDirection.DescNullsLast;
default:
assertUnreachable(orderByEnum);
}
};
export const getGroupByOrderBy = ({
graphOrderBy,
groupByField,
@@ -47,34 +28,17 @@ export const getGroupByOrderBy = ({
| AggregateOrderByWithGroupByField
| ObjectRecordOrderByForScalarField
| ObjectRecordOrderByWithGroupByDateField
| ObjectRecordOrderByForCompositeField => {
| ObjectRecordOrderByForCompositeField
| ObjectRecordOrderByForRelationField => {
switch (graphOrderBy) {
case GraphOrderBy.FIELD_ASC:
case GraphOrderBy.FIELD_DESC: {
if (isCompositeFieldType(groupByField.type)) {
if (!isDefined(groupBySubFieldName)) {
throw new Error(
`Group by subFieldName is required for composite fields (field: ${groupByField.name})`,
);
}
return {
[groupByField.name]: {
[groupBySubFieldName]: mapOrderByToDirection(graphOrderBy),
},
};
} else if (isFieldMetadataDateKind(groupByField.type)) {
return {
[groupByField.name]: {
orderBy: mapOrderByToDirection(graphOrderBy),
granularity: dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY,
},
};
} else {
return {
[groupByField.name]: mapOrderByToDirection(graphOrderBy),
};
}
}
case GraphOrderBy.FIELD_DESC:
return getFieldOrderBy(
groupByField,
groupBySubFieldName,
dateGranularity,
mapOrderByToDirection(graphOrderBy),
);
case GraphOrderBy.VALUE_ASC:
case GraphOrderBy.VALUE_DESC: {
if (!isDefined(aggregateOperation)) {
@@ -0,0 +1,37 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import {
type ObjectRecordOrderByForRelationField,
type ObjectRecordOrderByForScalarField,
type OrderByDirection,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const getRelationFieldOrderBy = (
groupByField: FieldMetadataItem,
groupBySubFieldName: string | null | undefined,
direction: OrderByDirection,
): ObjectRecordOrderByForScalarField | ObjectRecordOrderByForRelationField => {
if (!isDefined(groupBySubFieldName)) {
return {
[`${groupByField.name}Id`]: direction,
};
}
const [nestedFieldName, nestedSubFieldName] = groupBySubFieldName.split('.');
if (!isDefined(nestedSubFieldName)) {
return {
[groupByField.name]: {
[nestedFieldName]: direction,
},
};
}
return {
[groupByField.name]: {
[nestedFieldName]: {
[nestedSubFieldName]: direction,
},
},
};
};
@@ -0,0 +1,20 @@
import { OrderByDirection } from 'twenty-shared/types';
import { assertUnreachable } from 'twenty-shared/utils';
import { GraphOrderBy } from '~/generated/graphql';
export const mapOrderByToDirection = (
orderByEnum: GraphOrderBy,
): OrderByDirection => {
switch (orderByEnum) {
case GraphOrderBy.FIELD_ASC:
return OrderByDirection.AscNullsLast;
case GraphOrderBy.FIELD_DESC:
return OrderByDirection.DescNullsLast;
case GraphOrderBy.VALUE_ASC:
return OrderByDirection.AscNullsLast;
case GraphOrderBy.VALUE_DESC:
return OrderByDirection.DescNullsLast;
default:
assertUnreachable(orderByEnum);
}
};
@@ -6,9 +6,8 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { viewObjectMetadataIdComponentState } from '@/views/states/viewObjectMetadataIdComponentState';
import { SettingsPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldMetadataType, SettingsPath } from 'twenty-shared/types';
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
export const useGetAvailableFieldsForCalendar = () => {
@@ -26,10 +25,8 @@ export const useGetAvailableFieldsForCalendar = () => {
);
const availableFieldsForCalendar =
objectMetadataItem?.readableFields.filter(
(field) =>
field.type === FieldMetadataType.DATE ||
field.type === FieldMetadataType.DATE_TIME,
objectMetadataItem?.readableFields.filter((field) =>
isFieldMetadataDateKind(field.type),
) ?? [];
const navigate = useNavigateSettings();