Skip to content

Commit fa87603

Browse files
authored
[Dashboards] Relation fields groupby (#16093)
1 parent f23aa63 commit fa87603

22 files changed

+597
-136
lines changed

packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,9 @@ import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectab
2525
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
2626
import styled from '@emotion/styled';
2727
import { t } from '@lingui/core/macro';
28+
import { isFieldMetadataDateKind } from 'twenty-shared/utils';
2829

29-
import {
30-
FieldMetadataType,
31-
GraphType,
32-
type PageLayoutWidget,
33-
} from '~/generated/graphql';
30+
import { GraphType, type PageLayoutWidget } from '~/generated/graphql';
3431

3532
const StyledCommandMenuContainer = styled.div`
3633
display: flex;
@@ -136,9 +133,7 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => {
136133
(field) => field.id === primaryAxisFieldMetadataId,
137134
);
138135

139-
const isPrimaryAxisDate =
140-
primaryAxisField?.type === FieldMetadataType.DATE ||
141-
primaryAxisField?.type === FieldMetadataType.DATE_TIME;
136+
const isPrimaryAxisDate = isFieldMetadataDateKind(primaryAxisField?.type);
142137

143138
const primaryAxisDateGranularity =
144139
configuration.__typename === 'BarChartConfiguration' ||

packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionDropdownContentBase.tsx

Lines changed: 72 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ChartGroupByFieldSelectionCompositeFieldView } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionCompositeFieldView';
2+
import { ChartGroupByFieldSelectionRelationFieldView } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionRelationFieldView';
23
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
34
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
45
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
@@ -23,6 +24,7 @@ import { useMemo, useState } from 'react';
2324
import { isDefined } from 'twenty-shared/utils';
2425
import { useIcons } from 'twenty-ui/display';
2526
import { MenuItemSelect } from 'twenty-ui/navigation';
27+
import { RelationType } from '~/generated/graphql';
2628
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
2729

2830
type ChartGroupByFieldSelectionDropdownContentBaseProps<
@@ -43,6 +45,9 @@ export const ChartGroupByFieldSelectionDropdownContentBase = <
4345
const [selectedCompositeField, setSelectedCompositeField] =
4446
useState<FieldMetadataItem | null>(null);
4547

48+
const [selectedRelationField, setSelectedRelationField] =
49+
useState<FieldMetadataItem | null>(null);
50+
4651
const { objectMetadataItems } = useObjectMetadataItems();
4752

4853
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
@@ -77,8 +82,15 @@ export const ChartGroupByFieldSelectionDropdownContentBase = <
7782
items: sourceObjectMetadataItem?.fields || [],
7883
searchQuery,
7984
getSearchableValues: (item) => [item.label, item.name],
80-
// TODO: remove the relation filter once group by is supported for relation fields
81-
}).filter((field) => !isFieldRelation(field) && !field.isSystem),
85+
}).filter((field) => {
86+
if (field.isSystem === true) {
87+
return false;
88+
}
89+
if (isFieldRelation(field)) {
90+
return field.relation?.type === RelationType.MANY_TO_ONE;
91+
}
92+
return true;
93+
}),
8294
[sourceObjectMetadataItem?.fields, searchQuery],
8395
);
8496

@@ -94,20 +106,26 @@ export const ChartGroupByFieldSelectionDropdownContentBase = <
94106
}
95107

96108
const handleSelectField = (fieldMetadataItem: FieldMetadataItem) => {
109+
if (isFieldRelation(fieldMetadataItem)) {
110+
setSelectedRelationField(fieldMetadataItem);
111+
return;
112+
}
113+
97114
if (isCompositeFieldType(fieldMetadataItem.type)) {
98115
setSelectedCompositeField(fieldMetadataItem);
99-
} else {
100-
updateCurrentWidgetConfig({
101-
configToUpdate: buildChartGroupByFieldConfigUpdate({
102-
configuration,
103-
fieldMetadataIdKey,
104-
subFieldNameKey,
105-
fieldId: fieldMetadataItem.id,
106-
subFieldName: null,
107-
}),
108-
});
109-
closeDropdown();
116+
return;
110117
}
118+
119+
updateCurrentWidgetConfig({
120+
configToUpdate: buildChartGroupByFieldConfigUpdate({
121+
configuration,
122+
fieldMetadataIdKey,
123+
subFieldNameKey,
124+
fieldId: fieldMetadataItem.id,
125+
subFieldName: null,
126+
}),
127+
});
128+
closeDropdown();
111129
};
112130

113131
const handleSelectNone = () => {
@@ -123,11 +141,15 @@ export const ChartGroupByFieldSelectionDropdownContentBase = <
123141
closeDropdown();
124142
};
125143

126-
const handleBack = () => {
144+
const handleBackFromComposite = () => {
127145
setSelectedCompositeField(null);
128146
};
129147

130-
const handleSelectSubField = (subFieldName: string) => {
148+
const handleBackFromRelation = () => {
149+
setSelectedRelationField(null);
150+
};
151+
152+
const handleSelectCompositeSubField = (subFieldName: string) => {
131153
if (!isDefined(selectedCompositeField)) {
132154
return;
133155
}
@@ -144,13 +166,41 @@ export const ChartGroupByFieldSelectionDropdownContentBase = <
144166
closeDropdown();
145167
};
146168

169+
const handleSelectRelationSubField = (subFieldName: string) => {
170+
if (!isDefined(selectedRelationField)) {
171+
return;
172+
}
173+
174+
updateCurrentWidgetConfig({
175+
configToUpdate: buildChartGroupByFieldConfigUpdate({
176+
configuration,
177+
fieldMetadataIdKey,
178+
subFieldNameKey,
179+
fieldId: selectedRelationField.id,
180+
subFieldName,
181+
}),
182+
});
183+
closeDropdown();
184+
};
185+
186+
if (isDefined(selectedRelationField)) {
187+
return (
188+
<ChartGroupByFieldSelectionRelationFieldView
189+
relationField={selectedRelationField}
190+
currentSubFieldName={currentSubFieldName}
191+
onBack={handleBackFromRelation}
192+
onSelectSubField={handleSelectRelationSubField}
193+
/>
194+
);
195+
}
196+
147197
if (isDefined(selectedCompositeField)) {
148198
return (
149199
<ChartGroupByFieldSelectionCompositeFieldView
150200
compositeField={selectedCompositeField}
151201
currentSubFieldName={currentSubFieldName}
152-
onBack={handleBack}
153-
onSelectSubField={handleSelectSubField}
202+
onBack={handleBackFromComposite}
203+
onSelectSubField={handleSelectCompositeSubField}
154204
/>
155205
);
156206
}
@@ -195,11 +245,15 @@ export const ChartGroupByFieldSelectionDropdownContentBase = <
195245
text={fieldMetadataItem.label}
196246
selected={
197247
!isCompositeFieldType(fieldMetadataItem.type) &&
248+
!isFieldRelation(fieldMetadataItem) &&
198249
currentGroupByFieldMetadataId === fieldMetadataItem.id
199250
}
200251
focused={selectedItemId === fieldMetadataItem.id}
201252
LeftIcon={getIcon(fieldMetadataItem.icon)}
202-
hasSubMenu={isCompositeFieldType(fieldMetadataItem.type)}
253+
hasSubMenu={
254+
isCompositeFieldType(fieldMetadataItem.type) ||
255+
isFieldRelation(fieldMetadataItem)
256+
}
203257
onClick={() => {
204258
handleSelectField(fieldMetadataItem);
205259
}}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { ChartGroupByFieldSelectionCompositeFieldView } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionCompositeFieldView';
2+
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
3+
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
4+
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
5+
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
6+
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
7+
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
8+
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
9+
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
10+
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
11+
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
12+
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
13+
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
14+
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
15+
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
16+
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
17+
import { t } from '@lingui/core/macro';
18+
import { useMemo, useState } from 'react';
19+
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
20+
import { IconChevronLeft, useIcons } from 'twenty-ui/display';
21+
import { MenuItem, MenuItemSelect } from 'twenty-ui/navigation';
22+
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
23+
24+
type ChartGroupByFieldSelectionRelationFieldViewProps = {
25+
relationField: FieldMetadataItem;
26+
currentSubFieldName: string | undefined;
27+
onBack: () => void;
28+
onSelectSubField: (subFieldName: string) => void;
29+
};
30+
31+
export const ChartGroupByFieldSelectionRelationFieldView = ({
32+
relationField,
33+
currentSubFieldName,
34+
onBack,
35+
onSelectSubField,
36+
}: ChartGroupByFieldSelectionRelationFieldViewProps) => {
37+
const { getIcon } = useIcons();
38+
39+
const [searchQuery, setSearchQuery] = useState('');
40+
41+
const [selectedCompositeField, setSelectedCompositeField] =
42+
useState<FieldMetadataItem | null>(null);
43+
44+
const dropdownId = useAvailableComponentInstanceIdOrThrow(
45+
DropdownComponentInstanceContext,
46+
);
47+
48+
const selectedItemId = useRecoilComponentValue(
49+
selectedItemIdComponentState,
50+
dropdownId,
51+
);
52+
53+
const { objectMetadataItems } = useObjectMetadataItems();
54+
55+
const targetObjectNameSingular =
56+
relationField.relation?.targetObjectMetadata?.nameSingular;
57+
58+
const targetObjectMetadataItem = useMemo(
59+
() =>
60+
objectMetadataItems.find(
61+
(item) => item.nameSingular === targetObjectNameSingular,
62+
),
63+
[objectMetadataItems, targetObjectNameSingular],
64+
);
65+
66+
const availableFields = useMemo(() => {
67+
if (!isDefined(targetObjectMetadataItem)) {
68+
return [];
69+
}
70+
71+
return filterBySearchQuery({
72+
items: targetObjectMetadataItem.fields.filter(
73+
(field) =>
74+
!field.isSystem &&
75+
!isFieldRelation(field) &&
76+
// 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
77+
!isFieldMetadataDateKind(field.type),
78+
),
79+
searchQuery,
80+
getSearchableValues: (field) => [field.label, field.name],
81+
});
82+
}, [targetObjectMetadataItem, searchQuery]);
83+
84+
const handleSelectField = (fieldMetadataItem: FieldMetadataItem) => {
85+
if (isCompositeFieldType(fieldMetadataItem.type)) {
86+
setSelectedCompositeField(fieldMetadataItem);
87+
} else {
88+
onSelectSubField(fieldMetadataItem.name);
89+
}
90+
};
91+
92+
const handleSelectCompositeSubField = (compositeSubFieldName: string) => {
93+
if (!isDefined(selectedCompositeField)) {
94+
return;
95+
}
96+
onSelectSubField(`${selectedCompositeField.name}.${compositeSubFieldName}`);
97+
};
98+
99+
const handleBackFromComposite = () => {
100+
setSelectedCompositeField(null);
101+
};
102+
103+
const [currentNestedFieldName, currentNestedSubFieldName] =
104+
currentSubFieldName?.split('.') ?? [];
105+
106+
if (isDefined(selectedCompositeField)) {
107+
return (
108+
<ChartGroupByFieldSelectionCompositeFieldView
109+
compositeField={selectedCompositeField}
110+
currentSubFieldName={currentNestedSubFieldName}
111+
onBack={handleBackFromComposite}
112+
onSelectSubField={handleSelectCompositeSubField}
113+
/>
114+
);
115+
}
116+
117+
return (
118+
<>
119+
<DropdownMenuHeader
120+
StartComponent={
121+
<DropdownMenuHeaderLeftComponent
122+
onClick={onBack}
123+
Icon={IconChevronLeft}
124+
/>
125+
}
126+
>
127+
{relationField.label}
128+
</DropdownMenuHeader>
129+
<DropdownMenuSearchInput
130+
autoFocus
131+
type="text"
132+
placeholder={t`Search fields`}
133+
onChange={(event) => setSearchQuery(event.target.value)}
134+
value={searchQuery}
135+
/>
136+
<DropdownMenuSeparator />
137+
<DropdownMenuItemsContainer>
138+
{availableFields.length === 0 ? (
139+
<MenuItem text={t`No fields available`} />
140+
) : (
141+
<SelectableList
142+
selectableListInstanceId={dropdownId}
143+
focusId={dropdownId}
144+
selectableItemIdArray={availableFields.map((field) => field.id)}
145+
>
146+
{availableFields.map((fieldMetadataItem) => (
147+
<SelectableListItem
148+
key={fieldMetadataItem.id}
149+
itemId={fieldMetadataItem.id}
150+
onEnter={() => {
151+
handleSelectField(fieldMetadataItem);
152+
}}
153+
>
154+
<MenuItemSelect
155+
text={fieldMetadataItem.label}
156+
selected={
157+
!isCompositeFieldType(fieldMetadataItem.type) &&
158+
currentNestedFieldName === fieldMetadataItem.name
159+
}
160+
focused={selectedItemId === fieldMetadataItem.id}
161+
LeftIcon={getIcon(fieldMetadataItem.icon)}
162+
hasSubMenu={isCompositeFieldType(fieldMetadataItem.type)}
163+
onClick={() => {
164+
handleSelectField(fieldMetadataItem);
165+
}}
166+
/>
167+
</SelectableListItem>
168+
))}
169+
</SelectableList>
170+
)}
171+
</DropdownMenuItemsContainer>
172+
</>
173+
);
174+
};

packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useChartSettingsValues.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export const useChartSettingsValues = ({
8484
? getFieldLabelWithSubField({
8585
field: groupByFieldX,
8686
subFieldName: groupBySubFieldNameX,
87+
objectMetadataItems,
8788
})
8889
: undefined;
8990

@@ -181,6 +182,7 @@ export const useChartSettingsValues = ({
181182
? getFieldLabelWithSubField({
182183
field: pieChartGroupByField,
183184
subFieldName: finalGroupBySubFieldNameY,
185+
objectMetadataItems,
184186
})
185187
: undefined;
186188

packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useGraphGroupBySortOptionLabels.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const useGraphGroupBySortOptionLabels = ({
3232
const fieldLabel = getFieldLabelWithSubField({
3333
field,
3434
subFieldName: groupBySubFieldName,
35+
objectMetadataItems,
3536
});
3637

3738
switch (graphOrderBy) {

packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useGraphXSortOptionLabels.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export const useGraphXSortOptionLabels = ({
3939
const fieldLabel = getFieldLabelWithSubField({
4040
field: groupByField,
4141
subFieldName: groupBySubFieldNameX,
42+
objectMetadataItems,
4243
});
4344

4445
const aggregateField = objectMetadataItem?.fields.find(

0 commit comments

Comments
 (0)