Aggregate fast follows (1) (#9233)

Some fixes required: 

- [Aggregate value should not disappear when dropdown is
open](https://discord.com/channels/1130383047699738754/1319328950475817001/1319328950475817001)
- [Delay the apparition of the tooltip on
kanban](https://discord.com/channels/1130383047699738754/1319327824632352860/1319327824632352860)
- [Group options in
sub-menus](https://discord.com/channels/1130383047699738754/1319326443951362059/1319326443951362059)

![image](https://github.com/user-attachments/assets/b2b58cec-a042-4253-a185-01d273320960)
- Display the currently selected option with a checkmark

![image](https://github.com/user-attachments/assets/95270f9c-773e-4af7-aaf5-249469ae7d2d)
- [Loading -> Aggregates should appear at the same time that records,
not
before](https://discord.com/channels/1130383047699738754/1319329819749646456/1319329899630301318)
This commit is contained in:
Marie
2024-12-26 17:00:56 +01:00
committed by GitHub
parent 68d47e9543
commit b91656eb5d
31 changed files with 493 additions and 171 deletions

View File

@ -1,11 +1,13 @@
import { ObjectOptionsDropdownContextValue } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext'; import { ObjectOptionsDropdownContextValue } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext';
import { RecordBoardColumnHeaderAggregateDropdownContextValue } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext'; import { RecordBoardColumnHeaderAggregateDropdownContextValue } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext';
import { RecordTableColumnAggregateFooterDropdownContextValue } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
import { useDropdown as useDropdownUi } from '@/ui/layout/dropdown/hooks/useDropdown'; import { useDropdown as useDropdownUi } from '@/ui/layout/dropdown/hooks/useDropdown';
import { Context, useCallback, useContext } from 'react'; import { Context, useCallback, useContext } from 'react';
export const useDropdown = < export const useDropdown = <
T extends T extends
| RecordBoardColumnHeaderAggregateDropdownContextValue | RecordBoardColumnHeaderAggregateDropdownContextValue
| RecordTableColumnAggregateFooterDropdownContextValue
| ObjectOptionsDropdownContextValue, | ObjectOptionsDropdownContextValue,
>({ >({
context, context,

View File

@ -6,8 +6,8 @@ import { RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext } from
import { RecordBoardColumnHeaderAggregateDropdownButton } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownButton'; import { RecordBoardColumnHeaderAggregateDropdownButton } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownButton';
import { AggregateDropdownContent } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContent'; import { AggregateDropdownContent } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContent';
import { RecordBoardColumnHeaderAggregateDropdownContext } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext'; import { RecordBoardColumnHeaderAggregateDropdownContext } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext';
import { AggregateContentId } from '@/object-record/record-board/types/AggregateContentId';
import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope'; import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope';
import { RecordBoardColumnHeaderAggregateContentId } from '@/object-record/record-board/types/RecordBoardColumnHeaderAggregateContentId';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
type RecordBoardColumnHeaderAggregateDropdownProps = { type RecordBoardColumnHeaderAggregateDropdownProps = {
@ -24,13 +24,14 @@ export const RecordBoardColumnHeaderAggregateDropdown = ({
dropdownId, dropdownId,
}: RecordBoardColumnHeaderAggregateDropdownProps) => { }: RecordBoardColumnHeaderAggregateDropdownProps) => {
const { currentContentId, handleContentChange, handleResetContent } = const { currentContentId, handleContentChange, handleResetContent } =
useCurrentContentId<AggregateContentId>(); useCurrentContentId<RecordBoardColumnHeaderAggregateContentId>();
return ( return (
<RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext.Provider <RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext.Provider
value={{ instanceId: dropdownId }} value={{ instanceId: dropdownId }}
> >
<Dropdown <Dropdown
onClose={handleResetContent}
dropdownId={dropdownId} dropdownId={dropdownId}
dropdownHotkeyScope={{ dropdownHotkeyScope={{
scope: RecordBoardColumnHotkeyScope.ColumnHeader, scope: RecordBoardColumnHotkeyScope.ColumnHeader,

View File

@ -1,4 +1,5 @@
import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton'; import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { AppTooltip, Tag, TooltipDelay } from 'twenty-ui'; import { AppTooltip, Tag, TooltipDelay } from 'twenty-ui';
@ -15,18 +16,25 @@ export const RecordBoardColumnHeaderAggregateDropdownButton = ({
value?: string | number; value?: string | number;
tooltip?: string; tooltip?: string;
}) => { }) => {
const { isDropdownOpen } = useDropdown(dropdownId);
return ( return (
<div id={dropdownId}> <div id={dropdownId}>
<StyledButton> <StyledButton>
<Tag text={value ? value.toString() : '-'} color={'transparent'} /> <Tag
<AppTooltip text={value ? value.toString() : '-'}
anchorSelect={`#${dropdownId}`} color={'transparent'}
content={tooltip} weight={'regular'}
noArrow
place="right"
positionStrategy="fixed"
delay={TooltipDelay.shortDelay}
/> />
{!isDropdownOpen && (
<AppTooltip
anchorSelect={`#${dropdownId}`}
content={tooltip}
noArrow
place="right"
positionStrategy="fixed"
delay={TooltipDelay.mediumDelay}
/>
)}
</StyledButton> </StyledButton>
</div> </div>
); );

View File

@ -2,6 +2,7 @@ import { useDropdown } from '@/dropdown/hooks/useDropdown';
import { RecordBoardColumnHeaderAggregateDropdownContext } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext'; import { RecordBoardColumnHeaderAggregateDropdownContext } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext';
import { RecordBoardColumnHeaderAggregateDropdownFieldsContent } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent'; import { RecordBoardColumnHeaderAggregateDropdownFieldsContent } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent';
import { RecordBoardColumnHeaderAggregateDropdownMenuContent } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent'; import { RecordBoardColumnHeaderAggregateDropdownMenuContent } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent';
import { RecordBoardColumnHeaderAggregateDropdownMoreOptionsContent } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMoreOptionsContent';
export const AggregateDropdownContent = () => { export const AggregateDropdownContent = () => {
const { currentContentId } = useDropdown({ const { currentContentId } = useDropdown({
@ -9,6 +10,8 @@ export const AggregateDropdownContent = () => {
}); });
switch (currentContentId) { switch (currentContentId) {
case 'moreAggregateOperationOptions':
return <RecordBoardColumnHeaderAggregateDropdownMoreOptionsContent />;
case 'aggregateFields': case 'aggregateFields':
return <RecordBoardColumnHeaderAggregateDropdownFieldsContent />; return <RecordBoardColumnHeaderAggregateDropdownFieldsContent />;
default: default:

View File

@ -1,11 +1,11 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { AggregateContentId } from '@/object-record/record-board/types/AggregateContentId'; import { RecordBoardColumnHeaderAggregateContentId } from '@/object-record/record-board/types/RecordBoardColumnHeaderAggregateContentId';
import { createContext } from 'react'; import { createContext } from 'react';
export type RecordBoardColumnHeaderAggregateDropdownContextValue = { export type RecordBoardColumnHeaderAggregateDropdownContextValue = {
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
currentContentId: AggregateContentId | null; currentContentId: RecordBoardColumnHeaderAggregateContentId | null;
onContentChange: (key: AggregateContentId) => void; onContentChange: (key: RecordBoardColumnHeaderAggregateContentId) => void;
resetContent: () => void; resetContent: () => void;
dropdownId: string; dropdownId: string;
}; };

View File

@ -3,15 +3,23 @@ import { RecordBoardColumnHeaderAggregateDropdownContext } from '@/object-record
import { aggregateOperationComponentState } from '@/object-record/record-board/record-board-column/states/aggregateOperationComponentState'; import { aggregateOperationComponentState } from '@/object-record/record-board/record-board-column/states/aggregateOperationComponentState';
import { availableFieldIdsForAggregateOperationComponentState } from '@/object-record/record-board/record-board-column/states/availableFieldIdsForAggregateOperationComponentState'; import { availableFieldIdsForAggregateOperationComponentState } from '@/object-record/record-board/record-board-column/states/availableFieldIdsForAggregateOperationComponentState';
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel'; import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useUpdateViewAggregate } from '@/views/hooks/useUpdateViewAggregate'; import { useUpdateViewAggregate } from '@/views/hooks/useUpdateViewAggregate';
import { Icon123, IconChevronLeft, MenuItem, useIcons } from 'twenty-ui'; import { useRecoilValue } from 'recoil';
import {
Icon123,
IconCheck,
IconChevronLeft,
MenuItem,
useIcons,
} from 'twenty-ui';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => { export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => {
const { closeDropdown, resetContent, objectMetadataItem } = useDropdown({ const { closeDropdown, objectMetadataItem, onContentChange } = useDropdown({
context: RecordBoardColumnHeaderAggregateDropdownContext, context: RecordBoardColumnHeaderAggregateDropdownContext,
}); });
@ -27,11 +35,18 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => {
availableFieldIdsForAggregateOperationComponentState, availableFieldIdsForAggregateOperationComponentState,
); );
const recordIndexKanbanAggregateOperation = useRecoilValue(
recordIndexKanbanAggregateOperationState,
);
if (!isDefined(aggregateOperation)) return <></>; if (!isDefined(aggregateOperation)) return <></>;
return ( return (
<> <>
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetContent}> <DropdownMenuHeader
StartIcon={IconChevronLeft}
onClick={() => onContentChange('moreAggregateOperationOptions')}
>
{getAggregateOperationLabel(aggregateOperation)} {getAggregateOperationLabel(aggregateOperation)}
</DropdownMenuHeader> </DropdownMenuHeader>
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
@ -53,6 +68,14 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => {
}} }}
LeftIcon={getIcon(fieldMetadata.icon) ?? Icon123} LeftIcon={getIcon(fieldMetadata.icon) ?? Icon123}
text={fieldMetadata.label} text={fieldMetadata.label}
RightIcon={
recordIndexKanbanAggregateOperation?.fieldMetadataId ===
fieldId &&
recordIndexKanbanAggregateOperation?.operation ===
aggregateOperation
? IconCheck
: undefined
}
/> />
); );
})} })}

View File

@ -1,5 +1,5 @@
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { MenuItem } from 'twenty-ui'; import { IconCheck, MenuItem } from 'twenty-ui';
import { useDropdown } from '@/dropdown/hooks/useDropdown'; import { useDropdown } from '@/dropdown/hooks/useDropdown';
import { import {
@ -7,23 +7,18 @@ import {
RecordBoardColumnHeaderAggregateDropdownContextValue, RecordBoardColumnHeaderAggregateDropdownContextValue,
} from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext'; } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext';
import { RecordBoardColumnHeaderAggregateDropdownMenuItem } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuItem';
import { aggregateOperationComponentState } from '@/object-record/record-board/record-board-column/states/aggregateOperationComponentState';
import { availableFieldIdsForAggregateOperationComponentState } from '@/object-record/record-board/record-board-column/states/availableFieldIdsForAggregateOperationComponentState';
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel'; import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope'; import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation';
import { getAvailableFieldsIdsForAggregationFromObjectFields } from '@/object-record/utils/getAvailableFieldsIdsForAggregationFromObjectFields';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useUpdateViewAggregate } from '@/views/hooks/useUpdateViewAggregate'; import { useUpdateViewAggregate } from '@/views/hooks/useUpdateViewAggregate';
import isEmpty from 'lodash.isempty'; import { useRecoilValue } from 'recoil';
import { useMemo } from 'react'; import { isDefined } from '~/utils/isDefined';
export const RecordBoardColumnHeaderAggregateDropdownMenuContent = () => { export const RecordBoardColumnHeaderAggregateDropdownMenuContent = () => {
const { objectMetadataItem, onContentChange, closeDropdown } = const { onContentChange, closeDropdown } =
useDropdown<RecordBoardColumnHeaderAggregateDropdownContextValue>({ useDropdown<RecordBoardColumnHeaderAggregateDropdownContextValue>({
context: RecordBoardColumnHeaderAggregateDropdownContext, context: RecordBoardColumnHeaderAggregateDropdownContext,
}); });
@ -36,24 +31,12 @@ export const RecordBoardColumnHeaderAggregateDropdownMenuContent = () => {
TableOptionsHotkeyScope.Dropdown, TableOptionsHotkeyScope.Dropdown,
); );
const availableAggregations: AvailableFieldsForAggregateOperation = useMemo(
() =>
getAvailableFieldsIdsForAggregationFromObjectFields(
objectMetadataItem.fields,
),
[objectMetadataItem.fields],
);
const setAggregateOperation = useSetRecoilComponentStateV2(
aggregateOperationComponentState,
);
const setAvailableFieldsForAggregateOperation = useSetRecoilComponentStateV2(
availableFieldIdsForAggregateOperationComponentState,
);
const { updateViewAggregate } = useUpdateViewAggregate(); const { updateViewAggregate } = useUpdateViewAggregate();
const recordIndexKanbanAggregateOperation = useRecoilValue(
recordIndexKanbanAggregateOperationState,
);
return ( return (
<> <>
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
@ -63,35 +46,24 @@ export const RecordBoardColumnHeaderAggregateDropdownMenuContent = () => {
kanbanAggregateOperationFieldMetadataId: null, kanbanAggregateOperationFieldMetadataId: null,
kanbanAggregateOperation: AGGREGATE_OPERATIONS.count, kanbanAggregateOperation: AGGREGATE_OPERATIONS.count,
}); });
closeDropdown();
}} }}
text={getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)} text={getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)}
RightIcon={
!isDefined(recordIndexKanbanAggregateOperation?.operation) ||
recordIndexKanbanAggregateOperation?.operation ===
AGGREGATE_OPERATIONS.count
? IconCheck
: undefined
}
/>
<MenuItem
onClick={() => {
onContentChange('moreAggregateOperationOptions');
}}
text={'More options'}
hasSubMenu
/> />
{Object.entries(availableAggregations).map(
([
availableAggregationOperation,
availableAggregationFieldsIdsForOperation,
]) =>
isEmpty(availableAggregationFieldsIdsForOperation) ? (
<></>
) : (
<RecordBoardColumnHeaderAggregateDropdownMenuItem
key={`aggregate-dropdown-menu-content-${availableAggregationOperation}`}
onContentChange={() => {
setAggregateOperation(
availableAggregationOperation as AGGREGATE_OPERATIONS,
);
setAvailableFieldsForAggregateOperation(
availableAggregationFieldsIdsForOperation,
);
onContentChange('aggregateFields');
}}
text={getAggregateOperationLabel(
availableAggregationOperation as AGGREGATE_OPERATIONS,
)}
hasSubMenu
/>
),
)}
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
</> </>
); );

View File

@ -1,15 +1,22 @@
import { MenuItem } from 'twenty-ui'; import { IconComponent, MenuItem } from 'twenty-ui';
export const RecordBoardColumnHeaderAggregateDropdownMenuItem = ({ export const RecordBoardColumnHeaderAggregateDropdownMenuItem = ({
onContentChange, onContentChange,
text, text,
hasSubMenu, hasSubMenu,
RightIcon,
}: { }: {
onContentChange: () => void; onContentChange: () => void;
hasSubMenu: boolean; hasSubMenu: boolean;
text: string; text: string;
RightIcon?: IconComponent | null;
}) => { }) => {
return ( return (
<MenuItem onClick={onContentChange} text={text} hasSubMenu={hasSubMenu} /> <MenuItem
onClick={onContentChange}
text={text}
hasSubMenu={hasSubMenu}
RightIcon={RightIcon}
/>
); );
}; };

View File

@ -0,0 +1,89 @@
import { useDropdown } from '@/dropdown/hooks/useDropdown';
import {
RecordBoardColumnHeaderAggregateDropdownContext,
RecordBoardColumnHeaderAggregateDropdownContextValue,
} from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext';
import { RecordBoardColumnHeaderAggregateDropdownMenuItem } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuItem';
import { aggregateOperationComponentState } from '@/object-record/record-board/record-board-column/states/aggregateOperationComponentState';
import { availableFieldIdsForAggregateOperationComponentState } from '@/object-record/record-board/record-board-column/states/availableFieldIdsForAggregateOperationComponentState';
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation';
import { getAvailableFieldsIdsForAggregationFromObjectFields } from '@/object-record/utils/getAvailableFieldsIdsForAggregationFromObjectFields';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import isEmpty from 'lodash.isempty';
import { useMemo } from 'react';
import { Key } from 'ts-key-enum';
import { IconChevronLeft } from 'twenty-ui';
export const RecordBoardColumnHeaderAggregateDropdownMoreOptionsContent =
() => {
const { objectMetadataItem, onContentChange, closeDropdown, resetContent } =
useDropdown<RecordBoardColumnHeaderAggregateDropdownContextValue>({
context: RecordBoardColumnHeaderAggregateDropdownContext,
});
useScopedHotkeys(
[Key.Escape],
() => {
closeDropdown();
},
TableOptionsHotkeyScope.Dropdown,
);
const availableAggregations: AvailableFieldsForAggregateOperation = useMemo(
() =>
getAvailableFieldsIdsForAggregationFromObjectFields(
objectMetadataItem.fields,
),
[objectMetadataItem.fields],
);
const setAggregateOperation = useSetRecoilComponentStateV2(
aggregateOperationComponentState,
);
const setAvailableFieldsForAggregateOperation =
useSetRecoilComponentStateV2(
availableFieldIdsForAggregateOperationComponentState,
);
return (
<>
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetContent}>
More options
</DropdownMenuHeader>
<DropdownMenuItemsContainer>
{Object.entries(availableAggregations)
.filter(([, fields]) => !isEmpty(fields))
.map(
([
availableAggregationOperation,
availableAggregationFieldsIdsForOperation,
]) => (
<RecordBoardColumnHeaderAggregateDropdownMenuItem
key={`aggregate-dropdown-menu-content-${availableAggregationOperation}`}
onContentChange={() => {
setAggregateOperation(
availableAggregationOperation as AGGREGATE_OPERATIONS,
);
setAvailableFieldsForAggregateOperation(
availableAggregationFieldsIdsForOperation,
);
onContentChange('aggregateFields');
}}
text={getAggregateOperationLabel(
availableAggregationOperation as AGGREGATE_OPERATIONS,
)}
hasSubMenu
/>
),
)}
</DropdownMenuItemsContainer>
</>
);
};

View File

@ -141,6 +141,7 @@ describe('computeAggregateValueAndLabel', () => {
expect(result).toEqual({ expect(result).toEqual({
value: 42, value: 42,
label: 'Count', label: 'Count',
labelWithFieldName: 'Count',
}); });
}); });

View File

@ -35,6 +35,7 @@ export const computeAggregateValueAndLabel = ({
return { return {
value: data?.[fallbackFieldName]?.[AGGREGATE_OPERATIONS.count], value: data?.[fallbackFieldName]?.[AGGREGATE_OPERATIONS.count],
label: `${getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)}`, label: `${getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)}`,
labelWithFieldName: `${getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)}`,
}; };
} }

View File

@ -1 +0,0 @@
export type AggregateContentId = 'aggregateOperations' | 'aggregateFields';

View File

@ -0,0 +1,4 @@
export type RecordBoardColumnHeaderAggregateContentId =
| 'aggregateOperations'
| 'aggregateFields'
| 'moreAggregateOperationOptions';

View File

@ -30,7 +30,7 @@ import { useSetRecordGroup } from '@/object-record/record-group/hooks/useSetReco
import { RecordIndexFiltersToContextStoreEffect } from '@/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect'; import { RecordIndexFiltersToContextStoreEffect } from '@/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect';
import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState';
import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState'; import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState';
import { aggregateOperationForViewFieldState } from '@/object-record/record-table/record-table-footer/states/aggregateOperationForViewFieldState'; import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { ViewBar } from '@/views/components/ViewBar'; import { ViewBar } from '@/views/components/ViewBar';
import { ViewField } from '@/views/types/ViewField'; import { ViewField } from '@/views/types/ViewField';
@ -126,7 +126,7 @@ export const RecordIndexContainer = () => {
for (const viewField of viewFields) { for (const viewField of viewFields) {
const aggregateOperationForViewField = snapshot const aggregateOperationForViewField = snapshot
.getLoadable( .getLoadable(
aggregateOperationForViewFieldState({ viewFieldAggregateOperationState({
viewFieldId: viewField.id, viewFieldId: viewField.id,
}), }),
) )
@ -134,7 +134,7 @@ export const RecordIndexContainer = () => {
if (aggregateOperationForViewField !== viewField.aggregateOperation) { if (aggregateOperationForViewField !== viewField.aggregateOperation) {
set( set(
aggregateOperationForViewFieldState({ viewFieldAggregateOperationState({
viewFieldId: viewField.id, viewFieldId: viewField.id,
}), }),
viewField.aggregateOperation, viewField.aggregateOperation,

View File

@ -6,7 +6,7 @@ import { useRecordIndexContextOrThrow } from '@/object-record/record-index/conte
import { useHandleToggleColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleColumnFilter'; import { useHandleToggleColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleColumnFilter';
import { useHandleToggleColumnSort } from '@/object-record/record-index/hooks/useHandleToggleColumnSort'; import { useHandleToggleColumnSort } from '@/object-record/record-index/hooks/useHandleToggleColumnSort';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { aggregateOperationForViewFieldState } from '@/object-record/record-table/record-table-footer/states/aggregateOperationForViewFieldState'; import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView'; import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView';
import { ViewField } from '@/views/types/ViewField'; import { ViewField } from '@/views/types/ViewField';
@ -77,7 +77,7 @@ export const RecordIndexTableContainerEffect = () => {
(viewField: ViewField) => { (viewField: ViewField) => {
const aggregateOperationForViewField = snapshot const aggregateOperationForViewField = snapshot
.getLoadable( .getLoadable(
aggregateOperationForViewFieldState({ viewFieldAggregateOperationState({
viewFieldId: viewField.id, viewFieldId: viewField.id,
}), }),
) )
@ -85,7 +85,7 @@ export const RecordIndexTableContainerEffect = () => {
if (aggregateOperationForViewField !== viewField.aggregateOperation) { if (aggregateOperationForViewField !== viewField.aggregateOperation) {
set( set(
aggregateOperationForViewFieldState({ viewFieldAggregateOperationState({
viewFieldId: viewField.id, viewFieldId: viewField.id,
}), }),
viewField.aggregateOperation, viewField.aggregateOperation,

View File

@ -96,9 +96,12 @@ export const RecordTable = () => {
<RecordTableRecordGroupsBody /> <RecordTableRecordGroupsBody />
)} )}
<RecordTableStickyEffect /> <RecordTableStickyEffect />
{isAggregateQueryEnabled && !hasRecordGroups && ( {isAggregateQueryEnabled &&
<RecordTableAggregateFooter endOfTableSticky /> !hasRecordGroups &&
)} !isRecordTableInitialLoading &&
allRecordIds.length > 0 && (
<RecordTableAggregateFooter endOfTableSticky />
)}
</StyledTable> </StyledTable>
<DragSelect <DragSelect
dragSelectable={tableBodyRef} dragSelectable={tableBodyRef}

View File

@ -0,0 +1,60 @@
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
import { useViewFieldAggregateOperation } from '@/object-record/record-table/record-table-footer/hooks/useViewFieldAggregateOperation';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { ReactNode, useContext } from 'react';
import { IconCheck, isDefined, MenuItem } from 'twenty-ui';
export const RecordTableColumnAggregateFooterAggregateOperationMenuItems = ({
aggregateOperations,
children,
}: {
aggregateOperations: AGGREGATE_OPERATIONS[];
children?: ReactNode;
}) => {
const {
updateViewFieldAggregateOperation,
currentViewFieldAggregateOperation,
} = useViewFieldAggregateOperation();
const { dropdownId, resetContent } = useContext(
RecordTableColumnAggregateFooterDropdownContext,
);
const { closeDropdown } = useDropdown(dropdownId);
return (
<DropdownMenuItemsContainer>
{aggregateOperations.map((operation) => (
<MenuItem
key={operation}
onClick={() => {
updateViewFieldAggregateOperation(operation);
closeDropdown();
}}
text={getAggregateOperationLabel(operation)}
RightIcon={
currentViewFieldAggregateOperation === operation
? IconCheck
: undefined
}
aria-selected={currentViewFieldAggregateOperation === operation}
/>
))}
{children}
<MenuItem
key={'none'}
onClick={() => {
updateViewFieldAggregateOperation(null);
resetContent();
closeDropdown();
}}
text={'None'}
RightIcon={
!isDefined(currentViewFieldAggregateOperation) ? IconCheck : undefined
}
aria-selected={!isDefined(currentViewFieldAggregateOperation)}
/>
</DropdownMenuItemsContainer>
);
};

View File

@ -1,79 +0,0 @@
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { getAvailableAggregateOperationsForFieldMetadataType } from '@/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { usePersistViewFieldRecords } from '@/views/hooks/internal/usePersistViewFieldRecords';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useMemo } from 'react';
import { Key } from 'ts-key-enum';
import { MenuItem } from 'twenty-ui';
export const RecordTableColumnAggregateFooterDropdown = ({
column,
dropdownId,
}: {
column: ColumnDefinition<FieldMetadata>;
dropdownId: string;
}) => {
const { closeDropdown } = useDropdown(dropdownId);
const { objectMetadataItem } = useRecordTableContextOrThrow();
const { currentViewWithSavedFiltersAndSorts } = useGetCurrentView();
const currentViewField =
currentViewWithSavedFiltersAndSorts?.viewFields?.find(
(viewField) => viewField.fieldMetadataId === column.fieldMetadataId,
);
useScopedHotkeys(
[Key.Escape],
() => {
closeDropdown();
},
TableOptionsHotkeyScope.Dropdown,
);
const availableAggregateOperations = useMemo(
() =>
getAvailableAggregateOperationsForFieldMetadataType({
fieldMetadataType: objectMetadataItem.fields.find(
(field) => field.id === column.fieldMetadataId,
)?.type,
}),
[column.fieldMetadataId, objectMetadataItem.fields],
);
const { updateViewFieldRecords } = usePersistViewFieldRecords();
const handleAggregationChange = (
aggregateOperation: AGGREGATE_OPERATIONS,
) => {
if (!currentViewField) {
throw new Error('ViewField not found');
}
updateViewFieldRecords([
{ ...currentViewField, aggregateOperation: aggregateOperation },
]);
};
return (
<>
<DropdownMenuItemsContainer>
{availableAggregateOperations.map((aggregation) => (
<MenuItem
key={aggregation}
onClick={() => {
handleAggregationChange(aggregation);
closeDropdown();
}}
text={getAggregateOperationLabel(aggregation)}
/>
))}
</DropdownMenuItemsContainer>
</>
);
};

View File

@ -0,0 +1,17 @@
import { useDropdown } from '@/dropdown/hooks/useDropdown';
import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
import { RecordTableColumnAggregateFooterDropdownMoreOptionsContent } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownMoreOptionsContent';
import { RecordTableColumnAggregateFooterMenuContent } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterMenuContent';
export const RecordTableColumnAggregateFooterDropdownContent = () => {
const { currentContentId } = useDropdown({
context: RecordTableColumnAggregateFooterDropdownContext,
});
switch (currentContentId) {
case 'moreAggregateOperationOptions':
return <RecordTableColumnAggregateFooterDropdownMoreOptionsContent />;
default:
return <RecordTableColumnAggregateFooterMenuContent />;
}
};

View File

@ -0,0 +1,15 @@
import { RecordTableFooterAggregateContentId } from '@/object-record/record-table/record-table-footer/types/RecordTableFooterAggregateContentId';
import { createContext } from 'react';
export type RecordTableColumnAggregateFooterDropdownContextValue = {
currentContentId: RecordTableFooterAggregateContentId | null;
onContentChange: (key: RecordTableFooterAggregateContentId) => void;
resetContent: () => void;
dropdownId: string;
fieldMetadataId: string;
};
export const RecordTableColumnAggregateFooterDropdownContext =
createContext<RecordTableColumnAggregateFooterDropdownContextValue>(
{} as RecordTableColumnAggregateFooterDropdownContextValue,
);

View File

@ -0,0 +1,57 @@
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableColumnAggregateFooterAggregateOperationMenuItems } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterAggregateOperationMenuItems';
import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
import { STANDARD_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/standardAggregateOperationOptions';
import { getAvailableAggregateOperationsForFieldMetadataType } from '@/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useContext, useMemo } from 'react';
import { Key } from 'ts-key-enum';
import { IconChevronLeft } from 'twenty-ui';
export const RecordTableColumnAggregateFooterDropdownMoreOptionsContent =
() => {
const { fieldMetadataId, dropdownId, resetContent } = useContext(
RecordTableColumnAggregateFooterDropdownContext,
);
const { closeDropdown } = useDropdown(dropdownId);
const { objectMetadataItem } = useRecordTableContextOrThrow();
useScopedHotkeys(
[Key.Escape],
() => {
resetContent();
closeDropdown();
},
TableOptionsHotkeyScope.Dropdown,
);
const availableAggregateOperations = useMemo(
() =>
getAvailableAggregateOperationsForFieldMetadataType({
fieldMetadataType: objectMetadataItem.fields.find(
(field) => field.id === fieldMetadataId,
)?.type,
}).filter(
(aggregateOperation) =>
!STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation),
),
[fieldMetadataId, objectMetadataItem.fields],
);
return (
<>
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetContent}>
More options
</DropdownMenuHeader>
<DropdownMenuItemsContainer>
<RecordTableColumnAggregateFooterAggregateOperationMenuItems
aggregateOperations={availableAggregateOperations}
/>
</DropdownMenuItemsContainer>
</>
);
};

View File

@ -0,0 +1,68 @@
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableColumnAggregateFooterAggregateOperationMenuItems } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterAggregateOperationMenuItems';
import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
import { STANDARD_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/standardAggregateOperationOptions';
import { getAvailableAggregateOperationsForFieldMetadataType } from '@/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useContext, useMemo } from 'react';
import { Key } from 'ts-key-enum';
import { MenuItem } from 'twenty-ui';
export const RecordTableColumnAggregateFooterMenuContent = () => {
const { fieldMetadataId, dropdownId, onContentChange } = useContext(
RecordTableColumnAggregateFooterDropdownContext,
);
const { closeDropdown } = useDropdown(dropdownId);
const { objectMetadataItem } = useRecordTableContextOrThrow();
useScopedHotkeys(
[Key.Escape],
() => {
closeDropdown();
},
TableOptionsHotkeyScope.Dropdown,
);
const availableAggregateOperation = useMemo(
() =>
getAvailableAggregateOperationsForFieldMetadataType({
fieldMetadataType: objectMetadataItem.fields.find(
(field) => field.id === fieldMetadataId,
)?.type,
}),
[fieldMetadataId, objectMetadataItem.fields],
);
const standardAvailableAggregateOperation =
availableAggregateOperation.filter((aggregateOperation) =>
STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation),
);
const otherAvailableAggregateOperation = availableAggregateOperation.filter(
(aggregateOperation) =>
!STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation),
);
return (
<>
<DropdownMenuItemsContainer>
<RecordTableColumnAggregateFooterAggregateOperationMenuItems
aggregateOperations={standardAvailableAggregateOperation}
>
{otherAvailableAggregateOperation.length > 0 ? (
<MenuItem
key={'more-options'}
onClick={() => {
onContentChange('moreAggregateOperationOptions');
}}
text={'More options'}
hasSubMenu
/>
) : null}
</RecordTableColumnAggregateFooterAggregateOperationMenuItems>
</DropdownMenuItemsContainer>
</>
);
};

View File

@ -1,3 +1,4 @@
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useState } from 'react'; import { useState } from 'react';
@ -74,8 +75,11 @@ export const RecordTableColumnAggregateFooterValue = ({
aggregateLabel?: string; aggregateLabel?: string;
}) => { }) => {
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const { isDropdownOpen } = useDropdown(dropdownId);
const sanitizedId = `tooltip-${dropdownId.replace(/[^a-zA-Z0-9-_]/g, '-')}`; const sanitizedId = `tooltip-${dropdownId.replace(/[^a-zA-Z0-9-_]/g, '-')}`;
const theme = useTheme(); const theme = useTheme();
const shouldShowValue =
isHovered || isDropdownOpen || isDefined(aggregateValue) || isFirstCell;
return ( return (
<div <div
onMouseEnter={() => { onMouseEnter={() => {
@ -84,7 +88,7 @@ export const RecordTableColumnAggregateFooterValue = ({
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
> >
<StyledCell> <StyledCell>
{isHovered || isDefined(aggregateValue) || isFirstCell ? ( {shouldShowValue ? (
<> <>
{isDefined(aggregateValue) ? ( {isDefined(aggregateValue) ? (
<StyledValueContainer> <StyledValueContainer>

View File

@ -1,7 +1,10 @@
import { useCurrentContentId } from '@/dropdown/hooks/useCurrentContentId';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { RecordTableColumnAggregateFooterDropdown } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdown'; import { RecordTableColumnAggregateFooterDropdownContent } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContent';
import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
import { RecordTableColumnAggregateFooterValue } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValue'; import { RecordTableColumnAggregateFooterValue } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValue';
import { useAggregateRecordsForRecordTableColumnFooter } from '@/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter'; import { useAggregateRecordsForRecordTableColumnFooter } from '@/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter';
import { RecordTableFooterAggregateContentId } from '@/object-record/record-table/record-table-footer/types/RecordTableFooterAggregateContentId';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useToggleScrollWrapper } from '@/ui/utilities/scroll/hooks/useToggleScrollWrapper'; import { useToggleScrollWrapper } from '@/ui/utilities/scroll/hooks/useToggleScrollWrapper';
@ -18,6 +21,9 @@ export const RecordTableColumnFooterWithDropdown = ({
currentRecordGroupId, currentRecordGroupId,
isFirstCell, isFirstCell,
}: RecordTableColumnFooterWithDropdownProps) => { }: RecordTableColumnFooterWithDropdownProps) => {
const { currentContentId, handleContentChange, handleResetContent } =
useCurrentContentId<RecordTableFooterAggregateContentId>();
const { toggleScrollXWrapper, toggleScrollYWrapper } = const { toggleScrollXWrapper, toggleScrollYWrapper } =
useToggleScrollWrapper(); useToggleScrollWrapper();
@ -27,9 +33,10 @@ export const RecordTableColumnFooterWithDropdown = ({
}, [toggleScrollXWrapper, toggleScrollYWrapper]); }, [toggleScrollXWrapper, toggleScrollYWrapper]);
const handleDropdownClose = useCallback(() => { const handleDropdownClose = useCallback(() => {
handleResetContent();
toggleScrollXWrapper(true); toggleScrollXWrapper(true);
toggleScrollYWrapper(true); toggleScrollYWrapper(true);
}, [toggleScrollXWrapper, toggleScrollYWrapper]); }, [handleResetContent, toggleScrollXWrapper, toggleScrollYWrapper]);
const { aggregateValue, aggregateLabel } = const { aggregateValue, aggregateLabel } =
useAggregateRecordsForRecordTableColumnFooter(column.fieldMetadataId); useAggregateRecordsForRecordTableColumnFooter(column.fieldMetadataId);
@ -52,10 +59,17 @@ export const RecordTableColumnFooterWithDropdown = ({
/> />
} }
dropdownComponents={ dropdownComponents={
<RecordTableColumnAggregateFooterDropdown <RecordTableColumnAggregateFooterDropdownContext.Provider
column={column} value={{
dropdownId={dropdownId} currentContentId,
/> onContentChange: handleContentChange,
resetContent: handleResetContent,
dropdownId: dropdownId,
fieldMetadataId: column.fieldMetadataId,
}}
>
<RecordTableColumnAggregateFooterDropdownContent />
</RecordTableColumnAggregateFooterDropdownContext.Provider>
} }
dropdownOffset={{ x: -1 }} dropdownOffset={{ x: -1 }}
dropdownPlacement="bottom-start" dropdownPlacement="bottom-start"

View File

@ -0,0 +1,5 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
export const STANDARD_AGGREGATE_OPERATION_OPTIONS = [
AGGREGATE_OPERATIONS.count,
];

View File

@ -6,7 +6,7 @@ import { useRecordGroupFilter } from '@/object-record/record-group/hooks/useReco
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState'; import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { aggregateOperationForViewFieldState } from '@/object-record/record-table/record-table-footer/states/aggregateOperationForViewFieldState'; import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
@ -43,7 +43,7 @@ export const useAggregateRecordsForRecordTableColumnFooter = (
)?.id ?? ''; )?.id ?? '';
const aggregateOperationForViewField = useRecoilValue( const aggregateOperationForViewField = useRecoilValue(
aggregateOperationForViewFieldState({ viewFieldId: viewFieldId }), viewFieldAggregateOperationState({ viewFieldId: viewFieldId }),
); );
const fieldName = objectMetadataItem.fields.find( const fieldName = objectMetadataItem.fields.find(

View File

@ -0,0 +1,41 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
import { usePersistViewFieldRecords } from '@/views/hooks/internal/usePersistViewFieldRecords';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
export const useViewFieldAggregateOperation = () => {
const { fieldMetadataId } = useContext(
RecordTableColumnAggregateFooterDropdownContext,
);
const { currentViewWithSavedFiltersAndSorts } = useGetCurrentView();
const currentViewField =
currentViewWithSavedFiltersAndSorts?.viewFields?.find(
(viewField) => viewField.fieldMetadataId === fieldMetadataId,
);
const { updateViewFieldRecords } = usePersistViewFieldRecords();
const updateViewFieldAggregateOperation = (
aggregateOperation: AGGREGATE_OPERATIONS | null,
) => {
if (!currentViewField) {
throw new Error('ViewField not found');
}
updateViewFieldRecords([
{ ...currentViewField, aggregateOperation: aggregateOperation },
]);
};
const currentViewFieldAggregateOperation = useRecoilValue(
viewFieldAggregateOperationState({
viewFieldId: currentViewField?.id ?? '',
}),
);
return {
updateViewFieldAggregateOperation,
currentViewFieldAggregateOperation,
};
};

View File

@ -1,10 +1,10 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState'; import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
export const aggregateOperationForViewFieldState = createFamilyState< export const viewFieldAggregateOperationState = createFamilyState<
AGGREGATE_OPERATIONS | null | undefined, AGGREGATE_OPERATIONS | null | undefined,
{ viewFieldId: string } { viewFieldId: string }
>({ >({
key: 'aggregateOperationForViewFieldState', key: 'viewFieldAggregateOperationState',
defaultValue: null, defaultValue: null,
}); });

View File

@ -0,0 +1,2 @@
export type RecordTableFooterAggregateContentId =
'moreAggregateOperationOptions';

View File

@ -13,7 +13,7 @@ export const useUpdateViewAggregate = () => {
kanbanAggregateOperation, kanbanAggregateOperation,
}: { }: {
kanbanAggregateOperationFieldMetadataId: string | null; kanbanAggregateOperationFieldMetadataId: string | null;
kanbanAggregateOperation: AGGREGATE_OPERATIONS; kanbanAggregateOperation: AGGREGATE_OPERATIONS | null;
}) => }) =>
updateView({ updateView({
id: currentViewId, id: currentViewId,

View File

@ -25,6 +25,7 @@ export type MenuItemProps = {
isIconDisplayedOnHoverOnly?: boolean; isIconDisplayedOnHoverOnly?: boolean;
isTooltipOpen?: boolean; isTooltipOpen?: boolean;
LeftIcon?: IconComponent | null; LeftIcon?: IconComponent | null;
RightIcon?: IconComponent | null;
onClick?: (event: MouseEvent<HTMLDivElement>) => void; onClick?: (event: MouseEvent<HTMLDivElement>) => void;
onMouseEnter?: (event: MouseEvent<HTMLDivElement>) => void; onMouseEnter?: (event: MouseEvent<HTMLDivElement>) => void;
onMouseLeave?: (event: MouseEvent<HTMLDivElement>) => void; onMouseLeave?: (event: MouseEvent<HTMLDivElement>) => void;
@ -40,6 +41,7 @@ export const MenuItem = ({
iconButtons, iconButtons,
isIconDisplayedOnHoverOnly = true, isIconDisplayedOnHoverOnly = true,
LeftIcon, LeftIcon,
RightIcon,
onClick, onClick,
onMouseEnter, onMouseEnter,
onMouseLeave, onMouseLeave,
@ -81,6 +83,9 @@ export const MenuItem = ({
<LightIconButtonGroup iconButtons={iconButtons} size="small" /> <LightIconButtonGroup iconButtons={iconButtons} size="small" />
)} )}
</div> </div>
{RightIcon && (
<RightIcon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
)}
{hasSubMenu && ( {hasSubMenu && (
<IconChevronRight <IconChevronRight
size={theme.icon.size.sm} size={theme.icon.size.sm}