Aggregate follow-up (#9547)
In this PR - fix [some UI regressions](https://discord.com/channels/1130383047699738754/1327189577575956514/1327189577575956514) introduced by work on view groups - address some follow-ups: 1. [Menu should keep selected when the menu is open](https://discord.com/channels/1130383047699738754/1326607851824877639/1326607851824877639) 2. [Cropping](https://discord.com/channels/1130383047699738754/1326610578869063800/1326610578869063800) 3. [Put earliest date / latest date in a separate "Date" submenu](https://discord.com/channels/1130383047699738754/1326856023985618966/1326856023985618966) - Refactor around date aggregate operations
This commit is contained in:
@ -1,3 +1,6 @@
|
|||||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||||
|
|
||||||
export type RecordGqlFieldsAggregate = Record<string, AGGREGATE_OPERATIONS[]>;
|
export type RecordGqlFieldsAggregate = Record<
|
||||||
|
string,
|
||||||
|
ExtendedAggregateOperations[]
|
||||||
|
>;
|
||||||
|
|||||||
@ -5,13 +5,13 @@ import { RecordGqlFieldsAggregate } from '@/object-record/graphql/types/RecordGq
|
|||||||
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
||||||
import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult';
|
import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult';
|
||||||
import { useAggregateRecordsQuery } from '@/object-record/hooks/useAggregateRecordsQuery';
|
import { useAggregateRecordsQuery } from '@/object-record/hooks/useAggregateRecordsQuery';
|
||||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||||
import isEmpty from 'lodash.isempty';
|
import isEmpty from 'lodash.isempty';
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
export type AggregateRecordsData = {
|
export type AggregateRecordsData = {
|
||||||
[fieldName: string]: {
|
[fieldName: string]: {
|
||||||
[operation in AGGREGATE_OPERATIONS]?: string | number | undefined;
|
[operation in ExtendedAggregateOperations]?: string | number | undefined;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
|
import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
|
||||||
import { RecordGqlFieldsAggregate } from '@/object-record/graphql/types/RecordGqlFieldsAggregate';
|
import { RecordGqlFieldsAggregate } from '@/object-record/graphql/types/RecordGqlFieldsAggregate';
|
||||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||||
import { generateAggregateQuery } from '@/object-record/utils/generateAggregateQuery';
|
import { generateAggregateQuery } from '@/object-record/utils/generateAggregateQuery';
|
||||||
import { getAvailableAggregationsFromObjectFields } from '@/object-record/utils/getAvailableAggregationsFromObjectFields';
|
import { getAvailableAggregationsFromObjectFields } from '@/object-record/utils/getAvailableAggregationsFromObjectFields';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
@ -10,7 +10,7 @@ import { isDefined } from 'twenty-ui';
|
|||||||
export type GqlFieldToFieldMap = {
|
export type GqlFieldToFieldMap = {
|
||||||
[gqlField: string]: [
|
[gqlField: string]: [
|
||||||
fieldName: string,
|
fieldName: string,
|
||||||
aggregateOperation: AGGREGATE_OPERATIONS,
|
aggregateOperation: ExtendedAggregateOperations,
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -6,9 +6,7 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
|||||||
export const MultipleFiltersButton = () => {
|
export const MultipleFiltersButton = () => {
|
||||||
const { resetFilterDropdown } = useResetFilterDropdown();
|
const { resetFilterDropdown } = useResetFilterDropdown();
|
||||||
|
|
||||||
const { isDropdownOpen, toggleDropdown } = useDropdown(
|
const { toggleDropdown } = useDropdown(OBJECT_FILTER_DROPDOWN_ID);
|
||||||
OBJECT_FILTER_DROPDOWN_ID,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
toggleDropdown();
|
toggleDropdown();
|
||||||
@ -16,10 +14,7 @@ export const MultipleFiltersButton = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledHeaderDropdownButton
|
<StyledHeaderDropdownButton onClick={handleClick}>
|
||||||
isUnfolded={isDropdownOpen}
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
Filter
|
Filter
|
||||||
</StyledHeaderDropdownButton>
|
</StyledHeaderDropdownButton>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenu
|
|||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
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 { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { SORT_DIRECTIONS } from '../types/SortDirection';
|
import { SORT_DIRECTIONS } from '../types/SortDirection';
|
||||||
@ -78,8 +77,6 @@ export const ObjectSortDropdownButton = ({
|
|||||||
|
|
||||||
const { recordIndexId } = useRecordIndexContextOrThrow();
|
const { recordIndexId } = useRecordIndexContextOrThrow();
|
||||||
|
|
||||||
const { isDropdownOpen } = useDropdown(OBJECT_SORT_DROPDOWN_ID);
|
|
||||||
|
|
||||||
const handleButtonClick = () => {
|
const handleButtonClick = () => {
|
||||||
toggleSortDropdown();
|
toggleSortDropdown();
|
||||||
};
|
};
|
||||||
@ -141,10 +138,7 @@ export const ObjectSortDropdownButton = ({
|
|||||||
dropdownHotkeyScope={hotkeyScope}
|
dropdownHotkeyScope={hotkeyScope}
|
||||||
dropdownOffset={{ y: 8 }}
|
dropdownOffset={{ y: 8 }}
|
||||||
clickableComponent={
|
clickableComponent={
|
||||||
<StyledHeaderDropdownButton
|
<StyledHeaderDropdownButton onClick={handleButtonClick}>
|
||||||
isUnfolded={isDropdownOpen}
|
|
||||||
onClick={handleButtonClick}
|
|
||||||
>
|
|
||||||
Sort
|
Sort
|
||||||
</StyledHeaderDropdownButton>
|
</StyledHeaderDropdownButton>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,10 @@ import { RecordBoardColumnHeaderAggregateDropdownContext } from '@/object-record
|
|||||||
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 { RecordBoardColumnHeaderAggregateDropdownOptionsContent } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownOptionsContent';
|
import { RecordBoardColumnHeaderAggregateDropdownOptionsContent } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownOptionsContent';
|
||||||
|
import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/DateAggregateOperations';
|
||||||
import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions';
|
import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions';
|
||||||
import { NON_STANDARD_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/nonStandardAggregateOperationsOptions';
|
import { NON_STANDARD_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/nonStandardAggregateOperationsOptions';
|
||||||
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOption';
|
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOptions';
|
||||||
import { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation';
|
import { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation';
|
||||||
import { getAvailableFieldsIdsForAggregationFromObjectFields } from '@/object-record/utils/getAvailableFieldsIdsForAggregationFromObjectFields';
|
import { getAvailableFieldsIdsForAggregationFromObjectFields } from '@/object-record/utils/getAvailableFieldsIdsForAggregationFromObjectFields';
|
||||||
|
|
||||||
@ -41,15 +42,31 @@ export const AggregateDropdownContent = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
case 'datesAggregateOperationOptions': {
|
||||||
|
const datesAvailableAggregations: AvailableFieldsForAggregateOperation =
|
||||||
|
getAvailableFieldsIdsForAggregationFromObjectFields(
|
||||||
|
objectMetadataItem.fields,
|
||||||
|
[
|
||||||
|
DATE_AGGREGATE_OPERATIONS.earliest,
|
||||||
|
DATE_AGGREGATE_OPERATIONS.latest,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<RecordBoardColumnHeaderAggregateDropdownOptionsContent
|
||||||
|
availableAggregations={datesAvailableAggregations}
|
||||||
|
title="Dates"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
case 'moreAggregateOperationOptions': {
|
case 'moreAggregateOperationOptions': {
|
||||||
const availableAggregations: AvailableFieldsForAggregateOperation =
|
const availableAggregationsWithoutDates: AvailableFieldsForAggregateOperation =
|
||||||
getAvailableFieldsIdsForAggregationFromObjectFields(
|
getAvailableFieldsIdsForAggregationFromObjectFields(
|
||||||
objectMetadataItem.fields,
|
objectMetadataItem.fields,
|
||||||
NON_STANDARD_AGGREGATE_OPERATION_OPTIONS,
|
NON_STANDARD_AGGREGATE_OPERATION_OPTIONS,
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<RecordBoardColumnHeaderAggregateDropdownOptionsContent
|
<RecordBoardColumnHeaderAggregateDropdownOptionsContent
|
||||||
availableAggregations={availableAggregations}
|
availableAggregations={availableAggregationsWithoutDates}
|
||||||
title="More options"
|
title="More options"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { aggregateOperationComponentState } from '@/object-record/record-board/r
|
|||||||
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 { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState';
|
||||||
import { convertExtendedAggregateOperationToAggregateOperation } from '@/object-record/utils/convertExtendedAggregateOperationToAggregateOperation';
|
|
||||||
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';
|
||||||
@ -47,9 +46,6 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!isDefined(aggregateOperation)) return <></>;
|
if (!isDefined(aggregateOperation)) return <></>;
|
||||||
|
|
||||||
const convertedAggregateOperation =
|
|
||||||
convertExtendedAggregateOperationToAggregateOperation(aggregateOperation);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuHeader
|
<DropdownMenuHeader
|
||||||
@ -75,7 +71,7 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateViewAggregate({
|
updateViewAggregate({
|
||||||
kanbanAggregateOperationFieldMetadataId: fieldId,
|
kanbanAggregateOperationFieldMetadataId: fieldId,
|
||||||
kanbanAggregateOperation: convertedAggregateOperation,
|
kanbanAggregateOperation: aggregateOperation,
|
||||||
});
|
});
|
||||||
closeDropdown();
|
closeDropdown();
|
||||||
}}
|
}}
|
||||||
@ -85,7 +81,7 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => {
|
|||||||
recordIndexKanbanAggregateOperation?.fieldMetadataId ===
|
recordIndexKanbanAggregateOperation?.fieldMetadataId ===
|
||||||
fieldId &&
|
fieldId &&
|
||||||
recordIndexKanbanAggregateOperation?.operation ===
|
recordIndexKanbanAggregateOperation?.operation ===
|
||||||
convertedAggregateOperation
|
aggregateOperation
|
||||||
? IconCheck
|
? IconCheck
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,6 +42,13 @@ export const RecordBoardColumnHeaderAggregateDropdownMenuContent = () => {
|
|||||||
text={'Percent'}
|
text={'Percent'}
|
||||||
hasSubMenu
|
hasSubMenu
|
||||||
/>
|
/>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
onContentChange('datesAggregateOperationOptions');
|
||||||
|
}}
|
||||||
|
text={'Dates'}
|
||||||
|
hasSubMenu
|
||||||
|
/>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onContentChange('moreAggregateOperationOptions');
|
onContentChange('moreAggregateOperationOptions');
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|||||||
import { AggregateRecordsData } from '@/object-record/hooks/useAggregateRecords';
|
import { AggregateRecordsData } from '@/object-record/hooks/useAggregateRecords';
|
||||||
import { computeAggregateValueAndLabel } from '@/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel';
|
import { computeAggregateValueAndLabel } from '@/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel';
|
||||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
||||||
|
import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/DateAggregateOperations';
|
||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { FieldMetadataType } from '~/generated/graphql';
|
||||||
|
|
||||||
const MOCK_FIELD_ID = '7d2d7b5e-7b3e-4b4a-8b0a-7b3e4b4a8b0a';
|
const MOCK_FIELD_ID = '7d2d7b5e-7b3e-4b4a-8b0a-7b3e4b4a8b0a';
|
||||||
@ -154,17 +155,17 @@ describe('computeAggregateValueAndLabel', () => {
|
|||||||
],
|
],
|
||||||
} as ObjectMetadataItem;
|
} as ObjectMetadataItem;
|
||||||
|
|
||||||
const mockData = {
|
const mockFormattedData = {
|
||||||
createdAt: {
|
createdAt: {
|
||||||
[AGGREGATE_OPERATIONS.min]: '2023-01-01T12:00:00Z',
|
[DATE_AGGREGATE_OPERATIONS.earliest]: '2023-01-01T12:00:00Z',
|
||||||
},
|
},
|
||||||
} as AggregateRecordsData;
|
} as AggregateRecordsData;
|
||||||
|
|
||||||
const result = computeAggregateValueAndLabel({
|
const result = computeAggregateValueAndLabel({
|
||||||
data: mockData,
|
data: mockFormattedData,
|
||||||
objectMetadataItem: mockObjectMetadataWithDatetimeField,
|
objectMetadataItem: mockObjectMetadataWithDatetimeField,
|
||||||
fieldMetadataId: MOCK_FIELD_ID,
|
fieldMetadataId: MOCK_FIELD_ID,
|
||||||
aggregateOperation: AGGREGATE_OPERATIONS.min,
|
aggregateOperation: DATE_AGGREGATE_OPERATIONS.earliest,
|
||||||
fallbackFieldName: MOCK_KANBAN_FIELD_NAME,
|
fallbackFieldName: MOCK_KANBAN_FIELD_NAME,
|
||||||
...defaultParams,
|
...defaultParams,
|
||||||
});
|
});
|
||||||
@ -189,17 +190,17 @@ describe('computeAggregateValueAndLabel', () => {
|
|||||||
],
|
],
|
||||||
} as ObjectMetadataItem;
|
} as ObjectMetadataItem;
|
||||||
|
|
||||||
const mockData = {
|
const mockFormattedData = {
|
||||||
updatedAt: {
|
updatedAt: {
|
||||||
[AGGREGATE_OPERATIONS.max]: '2023-12-31T23:59:59Z',
|
[DATE_AGGREGATE_OPERATIONS.latest]: '2023-12-31T23:59:59Z',
|
||||||
},
|
},
|
||||||
} as AggregateRecordsData;
|
} as AggregateRecordsData;
|
||||||
|
|
||||||
const result = computeAggregateValueAndLabel({
|
const result = computeAggregateValueAndLabel({
|
||||||
data: mockData,
|
data: mockFormattedData,
|
||||||
objectMetadataItem: mockObjectMetadataWithDatetimeField,
|
objectMetadataItem: mockObjectMetadataWithDatetimeField,
|
||||||
fieldMetadataId: MOCK_FIELD_ID,
|
fieldMetadataId: MOCK_FIELD_ID,
|
||||||
aggregateOperation: AGGREGATE_OPERATIONS.max,
|
aggregateOperation: DATE_AGGREGATE_OPERATIONS.latest,
|
||||||
fallbackFieldName: MOCK_KANBAN_FIELD_NAME,
|
fallbackFieldName: MOCK_KANBAN_FIELD_NAME,
|
||||||
...defaultParams,
|
...defaultParams,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
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 { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
||||||
|
import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/DateAggregateOperations';
|
||||||
import { expect } from '@storybook/test';
|
import { expect } from '@storybook/test';
|
||||||
|
|
||||||
describe('getAggregateOperationLabel', () => {
|
describe('getAggregateOperationLabel', () => {
|
||||||
@ -9,8 +10,12 @@ describe('getAggregateOperationLabel', () => {
|
|||||||
expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.avg)).toBe(
|
expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.avg)).toBe(
|
||||||
'Average',
|
'Average',
|
||||||
);
|
);
|
||||||
expect(getAggregateOperationLabel('EARLIEST')).toBe('Earliest date');
|
expect(getAggregateOperationLabel(DATE_AGGREGATE_OPERATIONS.earliest)).toBe(
|
||||||
expect(getAggregateOperationLabel('LATEST')).toBe('Latest date');
|
'Earliest date',
|
||||||
|
);
|
||||||
|
expect(getAggregateOperationLabel(DATE_AGGREGATE_OPERATIONS.latest)).toBe(
|
||||||
|
'Latest date',
|
||||||
|
);
|
||||||
expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.sum)).toBe('Sum');
|
expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.sum)).toBe('Sum');
|
||||||
expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)).toBe(
|
expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)).toBe(
|
||||||
'Count all',
|
'Count all',
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import { AggregateRecordsData } from '@/object-record/hooks/useAggregateRecords'
|
|||||||
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 { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
||||||
import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions';
|
import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions';
|
||||||
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOption';
|
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOptions';
|
||||||
import { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation';
|
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||||
import isEmpty from 'lodash.isempty';
|
import isEmpty from 'lodash.isempty';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
import { formatAmount } from '~/utils/format/formatAmount';
|
import { formatAmount } from '~/utils/format/formatAmount';
|
||||||
@ -28,7 +28,7 @@ export const computeAggregateValueAndLabel = ({
|
|||||||
data: AggregateRecordsData;
|
data: AggregateRecordsData;
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
fieldMetadataId?: string | null;
|
fieldMetadataId?: string | null;
|
||||||
aggregateOperation?: AGGREGATE_OPERATIONS | null;
|
aggregateOperation?: ExtendedAggregateOperations | null;
|
||||||
fallbackFieldName?: string;
|
fallbackFieldName?: string;
|
||||||
dateFormat: DateFormat;
|
dateFormat: DateFormat;
|
||||||
timeFormat: TimeFormat;
|
timeFormat: TimeFormat;
|
||||||
@ -62,11 +62,19 @@ export const computeAggregateValueAndLabel = ({
|
|||||||
|
|
||||||
const displayAsRelativeDate = field?.settings?.displayAsRelativeDate;
|
const displayAsRelativeDate = field?.settings?.displayAsRelativeDate;
|
||||||
|
|
||||||
if (COUNT_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation)) {
|
if (
|
||||||
|
COUNT_AGGREGATE_OPERATION_OPTIONS.includes(
|
||||||
|
aggregateOperation as AGGREGATE_OPERATIONS,
|
||||||
|
)
|
||||||
|
) {
|
||||||
value = aggregateValue;
|
value = aggregateValue;
|
||||||
} else if (!isDefined(aggregateValue)) {
|
} else if (!isDefined(aggregateValue)) {
|
||||||
value = '-';
|
value = '-';
|
||||||
} else if (PERCENT_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation)) {
|
} else if (
|
||||||
|
PERCENT_AGGREGATE_OPERATION_OPTIONS.includes(
|
||||||
|
aggregateOperation as AGGREGATE_OPERATIONS,
|
||||||
|
)
|
||||||
|
) {
|
||||||
value = `${formatNumber(Number(aggregateValue) * 100)}%`;
|
value = `${formatNumber(Number(aggregateValue) * 100)}%`;
|
||||||
} else {
|
} else {
|
||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
@ -110,16 +118,11 @@ export const computeAggregateValueAndLabel = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const convertedAggregateOperation =
|
const label = getAggregateOperationLabel(aggregateOperation);
|
||||||
convertAggregateOperationToExtendedAggregateOperation(
|
|
||||||
aggregateOperation,
|
|
||||||
field.type,
|
|
||||||
);
|
|
||||||
const label = getAggregateOperationLabel(convertedAggregateOperation);
|
|
||||||
const labelWithFieldName =
|
const labelWithFieldName =
|
||||||
aggregateOperation === AGGREGATE_OPERATIONS.count
|
aggregateOperation === AGGREGATE_OPERATIONS.count
|
||||||
? `${getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)}`
|
? `${getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)}`
|
||||||
: `${getAggregateOperationLabel(convertedAggregateOperation)} of ${field.label}`;
|
: `${getAggregateOperationLabel(aggregateOperation)} of ${field.label}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value,
|
value,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
||||||
|
import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/DateAggregateOperations';
|
||||||
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||||
|
|
||||||
export const getAggregateOperationLabel = (
|
export const getAggregateOperationLabel = (
|
||||||
@ -25,9 +26,9 @@ export const getAggregateOperationLabel = (
|
|||||||
return 'Percent empty';
|
return 'Percent empty';
|
||||||
case AGGREGATE_OPERATIONS.percentageNotEmpty:
|
case AGGREGATE_OPERATIONS.percentageNotEmpty:
|
||||||
return 'Percent not empty';
|
return 'Percent not empty';
|
||||||
case 'EARLIEST':
|
case DATE_AGGREGATE_OPERATIONS.earliest:
|
||||||
return 'Earliest date';
|
return 'Earliest date';
|
||||||
case 'LATEST':
|
case DATE_AGGREGATE_OPERATIONS.latest:
|
||||||
return 'Latest date';
|
return 'Latest date';
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown aggregate operation: ${operation}`);
|
throw new Error(`Unknown aggregate operation: ${operation}`);
|
||||||
|
|||||||
@ -3,4 +3,5 @@ export type RecordBoardColumnHeaderAggregateContentId =
|
|||||||
| 'aggregateFields'
|
| 'aggregateFields'
|
||||||
| 'countAggregateOperationsOptions'
|
| 'countAggregateOperationsOptions'
|
||||||
| 'percentAggregateOperationsOptions'
|
| 'percentAggregateOperationsOptions'
|
||||||
|
| 'datesAggregateOperationOptions'
|
||||||
| 'moreAggregateOperationOptions';
|
| 'moreAggregateOperationOptions';
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
|
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
|
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
|
||||||
import { ObjectOptionsDropdown } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdown';
|
import { ObjectOptionsDropdown } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdown';
|
||||||
@ -31,6 +32,7 @@ import { RecordIndexTableContainerEffect } from '@/object-record/record-index/co
|
|||||||
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 { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
|
import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
|
||||||
|
import { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation';
|
||||||
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';
|
||||||
@ -125,6 +127,9 @@ export const RecordIndexContainer = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const viewField of viewFields) {
|
for (const viewField of viewFields) {
|
||||||
|
const viewFieldMetadataType = objectMetadataItem.fields?.find(
|
||||||
|
(field) => field.id === viewField.fieldMetadataId,
|
||||||
|
)?.type;
|
||||||
const aggregateOperationForViewField = snapshot
|
const aggregateOperationForViewField = snapshot
|
||||||
.getLoadable(
|
.getLoadable(
|
||||||
viewFieldAggregateOperationState({
|
viewFieldAggregateOperationState({
|
||||||
@ -133,17 +138,29 @@ export const RecordIndexContainer = () => {
|
|||||||
)
|
)
|
||||||
.getValue();
|
.getValue();
|
||||||
|
|
||||||
if (aggregateOperationForViewField !== viewField.aggregateOperation) {
|
const convertedViewFieldAggregateOperation = isDefined(
|
||||||
|
viewField.aggregateOperation,
|
||||||
|
)
|
||||||
|
? convertAggregateOperationToExtendedAggregateOperation(
|
||||||
|
viewField.aggregateOperation,
|
||||||
|
viewFieldMetadataType,
|
||||||
|
)
|
||||||
|
: viewField.aggregateOperation;
|
||||||
|
|
||||||
|
if (
|
||||||
|
aggregateOperationForViewField !==
|
||||||
|
convertedViewFieldAggregateOperation
|
||||||
|
) {
|
||||||
set(
|
set(
|
||||||
viewFieldAggregateOperationState({
|
viewFieldAggregateOperationState({
|
||||||
viewFieldId: viewField.id,
|
viewFieldId: viewField.id,
|
||||||
}),
|
}),
|
||||||
viewField.aggregateOperation,
|
convertedViewFieldAggregateOperation,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[columnDefinitions, setTableColumns],
|
[columnDefinitions, objectMetadataItem.fields, setTableColumns],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onViewGroupsChange = useCallback(
|
const onViewGroupsChange = useCallback(
|
||||||
@ -220,8 +237,18 @@ export const RecordIndexContainer = () => {
|
|||||||
setRecordIndexViewKanbanFieldMetadataIdState(
|
setRecordIndexViewKanbanFieldMetadataIdState(
|
||||||
view.kanbanFieldMetadataId,
|
view.kanbanFieldMetadataId,
|
||||||
);
|
);
|
||||||
|
const kanbanAggregateOperationFieldMetadataType =
|
||||||
|
objectMetadataItem.fields?.find(
|
||||||
|
(field) =>
|
||||||
|
field.id === view.kanbanAggregateOperationFieldMetadataId,
|
||||||
|
)?.type;
|
||||||
setRecordIndexViewKanbanAggregateOperationState({
|
setRecordIndexViewKanbanAggregateOperationState({
|
||||||
operation: view.kanbanAggregateOperation,
|
operation: isDefined(view.kanbanAggregateOperation)
|
||||||
|
? convertAggregateOperationToExtendedAggregateOperation(
|
||||||
|
view.kanbanAggregateOperation,
|
||||||
|
kanbanAggregateOperationFieldMetadataType,
|
||||||
|
)
|
||||||
|
: view.kanbanAggregateOperation,
|
||||||
fieldMetadataId: view.kanbanAggregateOperationFieldMetadataId,
|
fieldMetadataId: view.kanbanAggregateOperationFieldMetadataId,
|
||||||
});
|
});
|
||||||
setRecordIndexIsCompactModeActive(view.isCompact);
|
setRecordIndexIsCompactModeActive(view.isCompact);
|
||||||
|
|||||||
@ -8,9 +8,11 @@ import { useHandleToggleColumnSort } from '@/object-record/record-index/hooks/us
|
|||||||
import { useSetRecordIndexEntityCount } from '@/object-record/record-index/hooks/useSetRecordIndexEntityCount';
|
import { useSetRecordIndexEntityCount } from '@/object-record/record-index/hooks/useSetRecordIndexEntityCount';
|
||||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||||
import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
|
import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
|
||||||
|
import { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation';
|
||||||
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
||||||
import { ViewField } from '@/views/types/ViewField';
|
import { ViewField } from '@/views/types/ViewField';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
export const RecordIndexTableContainerEffect = () => {
|
export const RecordIndexTableContainerEffect = () => {
|
||||||
const { recordIndexId, objectNameSingular } = useRecordIndexContextOrThrow();
|
const { recordIndexId, objectNameSingular } = useRecordIndexContextOrThrow();
|
||||||
@ -83,16 +85,33 @@ export const RecordIndexTableContainerEffect = () => {
|
|||||||
)
|
)
|
||||||
.getValue();
|
.getValue();
|
||||||
|
|
||||||
if (aggregateOperationForViewField !== viewField.aggregateOperation) {
|
const viewFieldMetadataType = columnDefinitions.find(
|
||||||
|
(columnDefinition) =>
|
||||||
|
columnDefinition.fieldMetadataId === viewField.fieldMetadataId,
|
||||||
|
)?.type;
|
||||||
|
|
||||||
|
const convertedViewFieldAggregateOperation = isDefined(
|
||||||
|
viewField.aggregateOperation,
|
||||||
|
)
|
||||||
|
? convertAggregateOperationToExtendedAggregateOperation(
|
||||||
|
viewField.aggregateOperation,
|
||||||
|
viewFieldMetadataType,
|
||||||
|
)
|
||||||
|
: viewField.aggregateOperation;
|
||||||
|
|
||||||
|
if (
|
||||||
|
aggregateOperationForViewField !==
|
||||||
|
convertedViewFieldAggregateOperation
|
||||||
|
) {
|
||||||
set(
|
set(
|
||||||
viewFieldAggregateOperationState({
|
viewFieldAggregateOperationState({
|
||||||
viewFieldId: viewField.id,
|
viewFieldId: viewField.id,
|
||||||
}),
|
}),
|
||||||
viewField.aggregateOperation,
|
convertedViewFieldAggregateOperation,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[],
|
[columnDefinitions],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||||
import { createState } from '@ui/utilities/state/utils/createState';
|
import { createState } from '@ui/utilities/state/utils/createState';
|
||||||
|
|
||||||
export type KanbanAggregateOperation = {
|
export type KanbanAggregateOperation = {
|
||||||
operation?: AGGREGATE_OPERATIONS | null;
|
operation?: ExtendedAggregateOperations | null;
|
||||||
fieldMetadataId?: string | null;
|
fieldMetadataId?: string | null;
|
||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
|
|||||||
@ -100,7 +100,9 @@ export const RecordTable = () => {
|
|||||||
{isAggregateQueryEnabled &&
|
{isAggregateQueryEnabled &&
|
||||||
!hasRecordGroups &&
|
!hasRecordGroups &&
|
||||||
!isRecordTableInitialLoading &&
|
!isRecordTableInitialLoading &&
|
||||||
allRecordIds.length > 0 && <RecordTableAggregateFooter />}
|
allRecordIds.length > 0 && (
|
||||||
|
<RecordTableAggregateFooter endOfTableSticky />
|
||||||
|
)}
|
||||||
</StyledTable>
|
</StyledTable>
|
||||||
<DragSelect
|
<DragSelect
|
||||||
dragSelectable={tableBodyRef}
|
dragSelectable={tableBodyRef}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
|
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
|
||||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||||
import { RecordTableAggregateFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter';
|
|
||||||
import { RecordTablePendingRecordGroupRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRecordGroupRow';
|
import { RecordTablePendingRecordGroupRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRecordGroupRow';
|
||||||
import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow';
|
import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow';
|
||||||
import { RecordTableRecordGroupSectionAddNew } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew';
|
import { RecordTableRecordGroupSectionAddNew } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew';
|
||||||
@ -9,16 +8,10 @@ import { RecordTableRecordGroupSectionLoadMore } from '@/object-record/record-ta
|
|||||||
import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState';
|
import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState';
|
||||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { FeatureFlagKey } from '~/generated/graphql';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
export const RecordTableRecordGroupRows = () => {
|
export const RecordTableRecordGroupRows = () => {
|
||||||
const isAggregateQueryEnabled = useIsFeatureEnabled(
|
|
||||||
FeatureFlagKey.IsAggregateQueryEnabled,
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentRecordGroupId = useCurrentRecordGroupId();
|
const currentRecordGroupId = useCurrentRecordGroupId();
|
||||||
|
|
||||||
const allRecordIds = useRecoilComponentValueV2(
|
const allRecordIds = useRecoilComponentValueV2(
|
||||||
@ -66,12 +59,6 @@ export const RecordTableRecordGroupRows = () => {
|
|||||||
<RecordTablePendingRecordGroupRow />
|
<RecordTablePendingRecordGroupRow />
|
||||||
<RecordTableRecordGroupSectionAddNew />
|
<RecordTableRecordGroupSectionAddNew />
|
||||||
<RecordTableRecordGroupSectionLoadMore />
|
<RecordTableRecordGroupSectionLoadMore />
|
||||||
{isAggregateQueryEnabled && (
|
|
||||||
<RecordTableAggregateFooter
|
|
||||||
key={currentRecordGroupId}
|
|
||||||
currentRecordGroupId={currentRecordGroupId}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,4 @@
|
|||||||
|
export enum DATE_AGGREGATE_OPERATIONS {
|
||||||
|
earliest = 'EARLIEST',
|
||||||
|
latest = 'LATEST',
|
||||||
|
}
|
||||||
@ -1,18 +1,15 @@
|
|||||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
||||||
|
import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/DateAggregateOperations';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
export const FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION = {
|
export const FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION = {
|
||||||
[AGGREGATE_OPERATIONS.min]: [
|
[AGGREGATE_OPERATIONS.min]: [
|
||||||
FieldMetadataType.Number,
|
FieldMetadataType.Number,
|
||||||
FieldMetadataType.Currency,
|
FieldMetadataType.Currency,
|
||||||
FieldMetadataType.DateTime,
|
|
||||||
FieldMetadataType.Date,
|
|
||||||
],
|
],
|
||||||
[AGGREGATE_OPERATIONS.max]: [
|
[AGGREGATE_OPERATIONS.max]: [
|
||||||
FieldMetadataType.Number,
|
FieldMetadataType.Number,
|
||||||
FieldMetadataType.Currency,
|
FieldMetadataType.Currency,
|
||||||
FieldMetadataType.DateTime,
|
|
||||||
FieldMetadataType.Date,
|
|
||||||
],
|
],
|
||||||
[AGGREGATE_OPERATIONS.avg]: [
|
[AGGREGATE_OPERATIONS.avg]: [
|
||||||
FieldMetadataType.Number,
|
FieldMetadataType.Number,
|
||||||
@ -22,4 +19,12 @@ export const FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION = {
|
|||||||
FieldMetadataType.Number,
|
FieldMetadataType.Number,
|
||||||
FieldMetadataType.Currency,
|
FieldMetadataType.Currency,
|
||||||
],
|
],
|
||||||
|
[DATE_AGGREGATE_OPERATIONS.earliest]: [
|
||||||
|
FieldMetadataType.DateTime,
|
||||||
|
FieldMetadataType.Date,
|
||||||
|
],
|
||||||
|
[DATE_AGGREGATE_OPERATIONS.latest]: [
|
||||||
|
FieldMetadataType.DateTime,
|
||||||
|
FieldMetadataType.Date,
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,11 +6,14 @@ import { RecordTableRecordGroupRows } from '@/object-record/record-table/compone
|
|||||||
import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable';
|
import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable';
|
||||||
import { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading';
|
import { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading';
|
||||||
import { RecordTableBodyRecordGroupDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider';
|
import { RecordTableBodyRecordGroupDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider';
|
||||||
|
import { RecordTableAggregateFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter';
|
||||||
import { RecordTableRecordGroupSection } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection';
|
import { RecordTableRecordGroupSection } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection';
|
||||||
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
|
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
|
||||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { ViewType } from '@/views/types/ViewType';
|
import { ViewType } from '@/views/types/ViewType';
|
||||||
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
|
import { FeatureFlagKey } from '~/generated/graphql';
|
||||||
|
|
||||||
export const RecordTableRecordGroupsBody = () => {
|
export const RecordTableRecordGroupsBody = () => {
|
||||||
const allRecordIds = useRecoilComponentValueV2(
|
const allRecordIds = useRecoilComponentValueV2(
|
||||||
@ -26,6 +29,10 @@ export const RecordTableRecordGroupsBody = () => {
|
|||||||
ViewType.Table,
|
ViewType.Table,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isAggregateQueryEnabled = useIsFeatureEnabled(
|
||||||
|
FeatureFlagKey.IsAggregateQueryEnabled,
|
||||||
|
);
|
||||||
|
|
||||||
if (isRecordTableInitialLoading && allRecordIds.length === 0) {
|
if (isRecordTableInitialLoading && allRecordIds.length === 0) {
|
||||||
return <RecordTableBodyLoading />;
|
return <RecordTableBodyLoading />;
|
||||||
}
|
}
|
||||||
@ -43,6 +50,12 @@ export const RecordTableRecordGroupsBody = () => {
|
|||||||
<RecordTableRecordGroupSection />
|
<RecordTableRecordGroupSection />
|
||||||
<RecordTableRecordGroupRows />
|
<RecordTableRecordGroupRows />
|
||||||
</RecordTableBodyDroppable>
|
</RecordTableBodyDroppable>
|
||||||
|
{isAggregateQueryEnabled && (
|
||||||
|
<RecordTableAggregateFooter
|
||||||
|
key={recordGroupId}
|
||||||
|
currentRecordGroupId={recordGroupId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</RecordGroupContext.Provider>
|
</RecordGroupContext.Provider>
|
||||||
</RecordTableRecordGroupBodyContextProvider>
|
</RecordTableRecordGroupBodyContextProvider>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -2,40 +2,119 @@ import styled from '@emotion/styled';
|
|||||||
|
|
||||||
import { RecordTableAggregateFooterCell } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell';
|
import { RecordTableAggregateFooterCell } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell';
|
||||||
import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext';
|
import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext';
|
||||||
|
import { FIRST_TH_WIDTH } from '@/object-record/record-table/record-table-header/components/RecordTableHeader';
|
||||||
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
|
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
import { MOBILE_VIEWPORT } from 'twenty-ui';
|
||||||
|
|
||||||
const StyledTh = styled.th`
|
const StyledTh = styled.th`
|
||||||
background-color: ${({ theme }) => theme.background.primary};
|
background-color: ${({ theme }) => theme.background.primary};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledTableFoot = styled.thead<{ endOfTableSticky?: boolean }>`
|
||||||
|
cursor: pointer;
|
||||||
|
th:nth-of-type(1) {
|
||||||
|
width: ${FIRST_TH_WIDTH};
|
||||||
|
left: 0;
|
||||||
|
border-right-color: ${({ theme }) => theme.background.primary};
|
||||||
|
}
|
||||||
|
th:nth-of-type(2) {
|
||||||
|
border-right-color: ${({ theme }) => theme.background.primary};
|
||||||
|
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
}
|
||||||
|
&.first-columns-sticky {
|
||||||
|
th:nth-of-type(1) {
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
z-index: 5;
|
||||||
|
transition: 0.3s ease;
|
||||||
|
}
|
||||||
|
th:nth-of-type(2) {
|
||||||
|
position: sticky;
|
||||||
|
left: 11px;
|
||||||
|
z-index: 5;
|
||||||
|
transition: 0.3s ease;
|
||||||
|
}
|
||||||
|
th:nth-of-type(3) {
|
||||||
|
position: sticky;
|
||||||
|
left: 43px;
|
||||||
|
z-index: 5;
|
||||||
|
transition: 0.3s ease;
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -1px;
|
||||||
|
height: calc(100% + 2px);
|
||||||
|
width: 4px;
|
||||||
|
right: 0px;
|
||||||
|
box-shadow: ${({ theme }) => theme.boxShadow.light};
|
||||||
|
clip-path: inset(0px -4px 0px 0px);
|
||||||
|
}
|
||||||
|
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||||
|
width: 34px;
|
||||||
|
max-width: 34px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
position: sticky;
|
||||||
|
z-index: 5;
|
||||||
|
background: ${({ theme }) => theme.background.primary};
|
||||||
|
${({ endOfTableSticky }) =>
|
||||||
|
endOfTableSticky &&
|
||||||
|
`
|
||||||
|
bottom: 10px;
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -10px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 10px;
|
||||||
|
background: inherit;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export const RecordTableAggregateFooter = ({
|
export const RecordTableAggregateFooter = ({
|
||||||
currentRecordGroupId,
|
currentRecordGroupId,
|
||||||
|
endOfTableSticky,
|
||||||
}: {
|
}: {
|
||||||
currentRecordGroupId?: string;
|
currentRecordGroupId?: string;
|
||||||
|
|
||||||
|
endOfTableSticky?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const visibleTableColumns = useRecoilComponentValueV2(
|
const visibleTableColumns = useRecoilComponentValueV2(
|
||||||
visibleTableColumnsComponentSelector,
|
visibleTableColumnsComponentSelector,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr>
|
<StyledTableFoot
|
||||||
<StyledTh />
|
id={`record-table-footer${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`}
|
||||||
<StyledTh />
|
data-select-disable
|
||||||
{visibleTableColumns.map((column, index) => (
|
endOfTableSticky={endOfTableSticky}
|
||||||
<RecordTableColumnAggregateFooterCellContext.Provider
|
>
|
||||||
key={`${column.fieldMetadataId}${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`}
|
<tr>
|
||||||
value={{
|
<StyledTh />
|
||||||
viewFieldId: column.viewFieldId || '',
|
<StyledTh />
|
||||||
fieldMetadataId: column.fieldMetadataId,
|
{visibleTableColumns.map((column, index) => {
|
||||||
}}
|
return (
|
||||||
>
|
<RecordTableColumnAggregateFooterCellContext.Provider
|
||||||
<RecordTableAggregateFooterCell
|
key={`${column.fieldMetadataId}${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`}
|
||||||
currentRecordGroupId={currentRecordGroupId}
|
value={{
|
||||||
isFirstCell={index === 0}
|
viewFieldId: column.viewFieldId || '',
|
||||||
/>
|
fieldMetadataId: column.fieldMetadataId,
|
||||||
</RecordTableColumnAggregateFooterCellContext.Provider>
|
}}
|
||||||
))}
|
>
|
||||||
</tr>
|
<RecordTableAggregateFooterCell
|
||||||
|
currentRecordGroupId={currentRecordGroupId}
|
||||||
|
isFirstCell={index === 0}
|
||||||
|
/>
|
||||||
|
</RecordTableColumnAggregateFooterCellContext.Provider>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
</StyledTableFoot>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -43,6 +43,7 @@ const StyledColumnFooterCell = styled.th<{
|
|||||||
*::-webkit-scrollbar {
|
*::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledColumnFootContainer = styled.div`
|
const StyledColumnFootContainer = styled.div`
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
|
||||||
import { RecordTableColumnAggregateFooterAggregateOperationMenuItems } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterAggregateOperationMenuItems';
|
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 { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
|
||||||
|
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||||
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
|
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
|
||||||
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';
|
||||||
@ -14,7 +14,7 @@ export const RecordTableColumnAggregateFooterDropdownSubmenuContent = ({
|
|||||||
aggregateOperations,
|
aggregateOperations,
|
||||||
title,
|
title,
|
||||||
}: {
|
}: {
|
||||||
aggregateOperations: AGGREGATE_OPERATIONS[];
|
aggregateOperations: ExtendedAggregateOperations[];
|
||||||
title: string;
|
title: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { dropdownId, resetContent } = useContext(
|
const { dropdownId, resetContent } = useContext(
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
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 { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
|
||||||
import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
|
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 { useViewFieldAggregateOperation } from '@/object-record/record-table/record-table-footer/hooks/useViewFieldAggregateOperation';
|
||||||
import { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation';
|
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { ReactNode, useContext } from 'react';
|
import { ReactNode, useContext } from 'react';
|
||||||
import { IconCheck, isDefined, MenuItem } from 'twenty-ui';
|
import { IconCheck, isDefined, MenuItem } from 'twenty-ui';
|
||||||
@ -11,7 +10,7 @@ export const RecordTableColumnAggregateFooterAggregateOperationMenuItems = ({
|
|||||||
aggregateOperations,
|
aggregateOperations,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
aggregateOperations: AGGREGATE_OPERATIONS[];
|
aggregateOperations: ExtendedAggregateOperations[];
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
@ -19,7 +18,7 @@ export const RecordTableColumnAggregateFooterAggregateOperationMenuItems = ({
|
|||||||
currentViewFieldAggregateOperation,
|
currentViewFieldAggregateOperation,
|
||||||
} = useViewFieldAggregateOperation();
|
} = useViewFieldAggregateOperation();
|
||||||
|
|
||||||
const { dropdownId, resetContent, fieldMetadataType } = useContext(
|
const { dropdownId, resetContent } = useContext(
|
||||||
RecordTableColumnAggregateFooterDropdownContext,
|
RecordTableColumnAggregateFooterDropdownContext,
|
||||||
);
|
);
|
||||||
const { closeDropdown } = useDropdown(dropdownId);
|
const { closeDropdown } = useDropdown(dropdownId);
|
||||||
@ -32,12 +31,7 @@ export const RecordTableColumnAggregateFooterAggregateOperationMenuItems = ({
|
|||||||
updateViewFieldAggregateOperation(operation);
|
updateViewFieldAggregateOperation(operation);
|
||||||
closeDropdown();
|
closeDropdown();
|
||||||
}}
|
}}
|
||||||
text={getAggregateOperationLabel(
|
text={getAggregateOperationLabel(operation)}
|
||||||
convertAggregateOperationToExtendedAggregateOperation(
|
|
||||||
operation,
|
|
||||||
fieldMetadataType,
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
RightIcon={
|
RightIcon={
|
||||||
currentViewFieldAggregateOperation === operation
|
currentViewFieldAggregateOperation === operation
|
||||||
? IconCheck
|
? IconCheck
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
import { useDropdown } from '@/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/dropdown/hooks/useDropdown';
|
||||||
|
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
||||||
|
import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/DateAggregateOperations';
|
||||||
import { RecordTableColumnAggregateFooterDropdownSubmenuContent } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateDropdownSubmenuContent';
|
import { RecordTableColumnAggregateFooterDropdownSubmenuContent } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateDropdownSubmenuContent';
|
||||||
import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
|
import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
|
||||||
import { RecordTableColumnAggregateFooterMenuContent } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterMenuContent';
|
import { RecordTableColumnAggregateFooterMenuContent } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterMenuContent';
|
||||||
import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions';
|
import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions';
|
||||||
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOption';
|
import { DATE_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/dateAggregateOperationOptions';
|
||||||
|
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOptions';
|
||||||
import { STANDARD_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/standardAggregateOperationOptions';
|
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 { getAvailableAggregateOperationsForFieldMetadataType } from '@/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType';
|
||||||
|
|
||||||
@ -21,7 +24,9 @@ export const RecordTableColumnAggregateFooterDropdownContent = () => {
|
|||||||
case 'moreAggregateOperationOptions': {
|
case 'moreAggregateOperationOptions': {
|
||||||
const aggregateOperations = availableAggregateOperations.filter(
|
const aggregateOperations = availableAggregateOperations.filter(
|
||||||
(aggregateOperation) =>
|
(aggregateOperation) =>
|
||||||
!STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation),
|
!STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(
|
||||||
|
aggregateOperation as AGGREGATE_OPERATIONS,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -34,7 +39,9 @@ export const RecordTableColumnAggregateFooterDropdownContent = () => {
|
|||||||
case 'countAggregateOperationsOptions': {
|
case 'countAggregateOperationsOptions': {
|
||||||
const aggregateOperations = availableAggregateOperations.filter(
|
const aggregateOperations = availableAggregateOperations.filter(
|
||||||
(aggregateOperation) =>
|
(aggregateOperation) =>
|
||||||
COUNT_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation),
|
COUNT_AGGREGATE_OPERATION_OPTIONS.includes(
|
||||||
|
aggregateOperation as AGGREGATE_OPERATIONS,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<RecordTableColumnAggregateFooterDropdownSubmenuContent
|
<RecordTableColumnAggregateFooterDropdownSubmenuContent
|
||||||
@ -46,7 +53,9 @@ export const RecordTableColumnAggregateFooterDropdownContent = () => {
|
|||||||
case 'percentAggregateOperationsOptions': {
|
case 'percentAggregateOperationsOptions': {
|
||||||
const aggregateOperations = availableAggregateOperations.filter(
|
const aggregateOperations = availableAggregateOperations.filter(
|
||||||
(aggregateOperation) =>
|
(aggregateOperation) =>
|
||||||
PERCENT_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation),
|
PERCENT_AGGREGATE_OPERATION_OPTIONS.includes(
|
||||||
|
aggregateOperation as AGGREGATE_OPERATIONS,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<RecordTableColumnAggregateFooterDropdownSubmenuContent
|
<RecordTableColumnAggregateFooterDropdownSubmenuContent
|
||||||
@ -55,6 +64,20 @@ export const RecordTableColumnAggregateFooterDropdownContent = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
case 'datesAggregateOperationsOptions': {
|
||||||
|
const aggregateOperations = availableAggregateOperations.filter(
|
||||||
|
(aggregateOperation) =>
|
||||||
|
DATE_AGGREGATE_OPERATION_OPTIONS.includes(
|
||||||
|
aggregateOperation as DATE_AGGREGATE_OPERATIONS,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<RecordTableColumnAggregateFooterDropdownSubmenuContent
|
||||||
|
aggregateOperations={aggregateOperations}
|
||||||
|
title="Dates"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return <RecordTableColumnAggregateFooterMenuContent />;
|
return <RecordTableColumnAggregateFooterMenuContent />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
|
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
||||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||||
import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
|
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 { NON_STANDARD_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/nonStandardAggregateOperationsOptions';
|
||||||
|
import { useViewFieldAggregateOperation } from '@/object-record/record-table/record-table-footer/hooks/useViewFieldAggregateOperation';
|
||||||
import { getAvailableAggregateOperationsForFieldMetadataType } from '@/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType';
|
import { getAvailableAggregateOperationsForFieldMetadataType } from '@/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType';
|
||||||
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
|
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
@ -8,13 +10,18 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
|||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { useContext, useMemo } from 'react';
|
import { useContext, useMemo } from 'react';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
import { MenuItem } from 'twenty-ui';
|
import { isFieldMetadataDateKind } from 'twenty-shared';
|
||||||
|
import { IconCheck, isDefined, MenuItem } from 'twenty-ui';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
export const RecordTableColumnAggregateFooterMenuContent = () => {
|
export const RecordTableColumnAggregateFooterMenuContent = () => {
|
||||||
const { fieldMetadataId, dropdownId, onContentChange } = useContext(
|
const {
|
||||||
RecordTableColumnAggregateFooterDropdownContext,
|
fieldMetadataId,
|
||||||
);
|
dropdownId,
|
||||||
|
onContentChange,
|
||||||
|
fieldMetadataType,
|
||||||
|
resetContent,
|
||||||
|
} = useContext(RecordTableColumnAggregateFooterDropdownContext);
|
||||||
const { closeDropdown } = useDropdown(dropdownId);
|
const { closeDropdown } = useDropdown(dropdownId);
|
||||||
const { objectMetadataItem } = useRecordTableContextOrThrow();
|
const { objectMetadataItem } = useRecordTableContextOrThrow();
|
||||||
|
|
||||||
@ -36,16 +43,24 @@ export const RecordTableColumnAggregateFooterMenuContent = () => {
|
|||||||
[fieldMetadataId, objectMetadataItem.fields],
|
[fieldMetadataId, objectMetadataItem.fields],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const fieldIsDateKind = isFieldMetadataDateKind(fieldMetadataType);
|
||||||
|
|
||||||
const nonStandardAvailableAggregateOperation =
|
const nonStandardAvailableAggregateOperation =
|
||||||
availableAggregateOperation.filter(
|
availableAggregateOperation.filter((aggregateOperation) =>
|
||||||
(aggregateOperation) =>
|
NON_STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(
|
||||||
!STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation),
|
aggregateOperation as AGGREGATE_OPERATIONS,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const fieldIsRelation =
|
const fieldIsRelation =
|
||||||
objectMetadataItem.fields.find((field) => field.id === fieldMetadataId)
|
objectMetadataItem.fields.find((field) => field.id === fieldMetadataId)
|
||||||
?.type === FieldMetadataType.Relation;
|
?.type === FieldMetadataType.Relation;
|
||||||
|
|
||||||
|
const {
|
||||||
|
updateViewFieldAggregateOperation,
|
||||||
|
currentViewFieldAggregateOperation,
|
||||||
|
} = useViewFieldAggregateOperation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuItemsContainer>
|
<DropdownMenuItemsContainer>
|
||||||
@ -65,6 +80,15 @@ export const RecordTableColumnAggregateFooterMenuContent = () => {
|
|||||||
hasSubMenu
|
hasSubMenu
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{fieldIsDateKind && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
onContentChange('datesAggregateOperationsOptions');
|
||||||
|
}}
|
||||||
|
text={'Dates'}
|
||||||
|
hasSubMenu
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{nonStandardAvailableAggregateOperation.length > 0 ? (
|
{nonStandardAvailableAggregateOperation.length > 0 ? (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -74,6 +98,21 @@ export const RecordTableColumnAggregateFooterMenuContent = () => {
|
|||||||
hasSubMenu
|
hasSubMenu
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
<MenuItem
|
||||||
|
key={'none'}
|
||||||
|
onClick={() => {
|
||||||
|
updateViewFieldAggregateOperation(null);
|
||||||
|
resetContent();
|
||||||
|
closeDropdown();
|
||||||
|
}}
|
||||||
|
text={'None'}
|
||||||
|
RightIcon={
|
||||||
|
!isDefined(currentViewFieldAggregateOperation)
|
||||||
|
? IconCheck
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
aria-selected={!isDefined(currentViewFieldAggregateOperation)}
|
||||||
|
/>
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -36,8 +36,9 @@ const StyledValueContainer = styled(StyledScrollableContainer)`
|
|||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledValue = styled(StyledScrollableContainer)`
|
const StyledValue = styled.div`
|
||||||
color: ${({ theme }) => theme.color.gray60};
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
max-width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const RecordTableColumnAggregateFooterValue = ({
|
export const RecordTableColumnAggregateFooterValue = ({
|
||||||
|
|||||||
@ -76,7 +76,9 @@ export const RecordTableColumnAggregateFooterValueCell = ({
|
|||||||
fieldMetadataId={fieldMetadataId}
|
fieldMetadataId={fieldMetadataId}
|
||||||
dropdownId={dropdownId}
|
dropdownId={dropdownId}
|
||||||
/>
|
/>
|
||||||
<StyledIcon fontWeight={'light'} size={theme.icon.size.sm} />
|
{!hasAggregateOperationForViewField && (
|
||||||
|
<StyledIcon fontWeight={'light'} size={theme.icon.size.sm} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/DateAggregateOperations';
|
||||||
|
|
||||||
|
export const DATE_AGGREGATE_OPERATION_OPTIONS = [
|
||||||
|
DATE_AGGREGATE_OPERATIONS.earliest,
|
||||||
|
DATE_AGGREGATE_OPERATIONS.latest,
|
||||||
|
];
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions';
|
import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions';
|
||||||
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOption';
|
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOptions';
|
||||||
|
|
||||||
export const STANDARD_AGGREGATE_OPERATION_OPTIONS = [
|
export const STANDARD_AGGREGATE_OPERATION_OPTIONS = [
|
||||||
...COUNT_AGGREGATE_OPERATION_OPTIONS,
|
...COUNT_AGGREGATE_OPERATION_OPTIONS,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
|
||||||
import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
|
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 { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
|
||||||
|
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||||
|
import { convertExtendedAggregateOperationToAggregateOperation } from '@/object-record/utils/convertExtendedAggregateOperationToAggregateOperation';
|
||||||
import { usePersistViewFieldRecords } from '@/views/hooks/internal/usePersistViewFieldRecords';
|
import { usePersistViewFieldRecords } from '@/views/hooks/internal/usePersistViewFieldRecords';
|
||||||
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
@ -18,13 +19,19 @@ export const useViewFieldAggregateOperation = () => {
|
|||||||
);
|
);
|
||||||
const { updateViewFieldRecords } = usePersistViewFieldRecords();
|
const { updateViewFieldRecords } = usePersistViewFieldRecords();
|
||||||
const updateViewFieldAggregateOperation = (
|
const updateViewFieldAggregateOperation = (
|
||||||
aggregateOperation: AGGREGATE_OPERATIONS | null,
|
aggregateOperation: ExtendedAggregateOperations | null,
|
||||||
) => {
|
) => {
|
||||||
if (!currentViewField) {
|
if (!currentViewField) {
|
||||||
throw new Error('ViewField not found');
|
throw new Error('ViewField not found');
|
||||||
}
|
}
|
||||||
updateViewFieldRecords([
|
updateViewFieldRecords([
|
||||||
{ ...currentViewField, aggregateOperation: aggregateOperation },
|
{
|
||||||
|
...currentViewField,
|
||||||
|
aggregateOperation:
|
||||||
|
convertExtendedAggregateOperationToAggregateOperation(
|
||||||
|
aggregateOperation,
|
||||||
|
),
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||||
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
|
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
|
||||||
|
|
||||||
export const viewFieldAggregateOperationState = createFamilyState<
|
export const viewFieldAggregateOperationState = createFamilyState<
|
||||||
AGGREGATE_OPERATIONS | null | undefined,
|
ExtendedAggregateOperations | null | undefined,
|
||||||
{ viewFieldId: string }
|
{ viewFieldId: string }
|
||||||
>({
|
>({
|
||||||
key: 'viewFieldAggregateOperationState',
|
key: 'viewFieldAggregateOperationState',
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
export type RecordTableFooterAggregateContentId =
|
export type RecordTableFooterAggregateContentId =
|
||||||
| 'moreAggregateOperationOptions'
|
| 'moreAggregateOperationOptions'
|
||||||
| 'countAggregateOperationsOptions'
|
| 'countAggregateOperationsOptions'
|
||||||
| 'percentAggregateOperationsOptions';
|
| 'percentAggregateOperationsOptions'
|
||||||
|
| 'datesAggregateOperationsOptions';
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
||||||
import { FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION } from '@/object-record/record-table/constants/FieldTypesAvailableForNonStandardAggregateOperation';
|
import { FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION } from '@/object-record/record-table/constants/FieldTypesAvailableForNonStandardAggregateOperation';
|
||||||
|
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||||
import { AggregateOperationsOmittingStandardOperations } from '@/object-record/types/AggregateOperationsOmittingStandardOperations';
|
import { AggregateOperationsOmittingStandardOperations } from '@/object-record/types/AggregateOperationsOmittingStandardOperations';
|
||||||
import { isFieldTypeValidForAggregateOperation } from '@/object-record/utils/isFieldTypeValidForAggregateOperation';
|
import { isFieldTypeValidForAggregateOperation } from '@/object-record/utils/isFieldTypeValidForAggregateOperation';
|
||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { FieldMetadataType } from '~/generated/graphql';
|
||||||
@ -14,7 +15,7 @@ export const getAvailableAggregateOperationsForFieldMetadataType = ({
|
|||||||
return [AGGREGATE_OPERATIONS.count];
|
return [AGGREGATE_OPERATIONS.count];
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableAggregateOperations = new Set<AGGREGATE_OPERATIONS>([
|
const availableAggregateOperations = new Set<ExtendedAggregateOperations>([
|
||||||
AGGREGATE_OPERATIONS.count,
|
AGGREGATE_OPERATIONS.count,
|
||||||
AGGREGATE_OPERATIONS.countEmpty,
|
AGGREGATE_OPERATIONS.countEmpty,
|
||||||
AGGREGATE_OPERATIONS.countNotEmpty,
|
AGGREGATE_OPERATIONS.countNotEmpty,
|
||||||
@ -35,7 +36,9 @@ export const getAvailableAggregateOperationsForFieldMetadataType = ({
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.forEach((operation) =>
|
.forEach((operation) =>
|
||||||
availableAggregateOperations.add(operation as AGGREGATE_OPERATIONS),
|
availableAggregateOperations.add(
|
||||||
|
operation as ExtendedAggregateOperations,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return Array.from(availableAggregateOperations);
|
return Array.from(availableAggregateOperations);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
||||||
|
import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/DateAggregateOperations';
|
||||||
|
|
||||||
export type ExtendedAggregateOperations =
|
export type ExtendedAggregateOperations =
|
||||||
| AGGREGATE_OPERATIONS
|
| AGGREGATE_OPERATIONS
|
||||||
| 'EARLIEST'
|
| DATE_AGGREGATE_OPERATIONS;
|
||||||
| 'LATEST';
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
|||||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
||||||
import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions';
|
import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions';
|
||||||
import { NON_STANDARD_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/nonStandardAggregateOperationsOptions';
|
import { NON_STANDARD_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/nonStandardAggregateOperationsOptions';
|
||||||
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOption';
|
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOptions';
|
||||||
import { getAvailableFieldsIdsForAggregationFromObjectFields } from '@/object-record/utils/getAvailableFieldsIdsForAggregationFromObjectFields';
|
import { getAvailableFieldsIdsForAggregationFromObjectFields } from '@/object-record/utils/getAvailableFieldsIdsForAggregationFromObjectFields';
|
||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { FieldMetadataType } from '~/generated/graphql';
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
||||||
import { FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION } from '@/object-record/record-table/constants/FieldTypesAvailableForNonStandardAggregateOperation';
|
import { FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION } from '@/object-record/record-table/constants/FieldTypesAvailableForNonStandardAggregateOperation';
|
||||||
import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions';
|
import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions';
|
||||||
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOption';
|
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOptions';
|
||||||
import { AggregateOperationsOmittingStandardOperations } from '@/object-record/types/AggregateOperationsOmittingStandardOperations';
|
import { AggregateOperationsOmittingStandardOperations } from '@/object-record/types/AggregateOperationsOmittingStandardOperations';
|
||||||
import { initializeAvailableFieldsForAggregateOperationMap } from '@/object-record/utils/initializeAvailableFieldsForAggregateOperationMap';
|
import { initializeAvailableFieldsForAggregateOperationMap } from '@/object-record/utils/initializeAvailableFieldsForAggregateOperationMap';
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
||||||
|
import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/DateAggregateOperations';
|
||||||
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||||
import { isFieldMetadataDateKind } from 'twenty-shared';
|
import { isFieldMetadataDateKind } from 'twenty-shared';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
@ -9,10 +10,10 @@ export const convertAggregateOperationToExtendedAggregateOperation = (
|
|||||||
): ExtendedAggregateOperations => {
|
): ExtendedAggregateOperations => {
|
||||||
if (isFieldMetadataDateKind(fieldType) === true) {
|
if (isFieldMetadataDateKind(fieldType) === true) {
|
||||||
if (aggregateOperation === AGGREGATE_OPERATIONS.min) {
|
if (aggregateOperation === AGGREGATE_OPERATIONS.min) {
|
||||||
return 'EARLIEST';
|
return DATE_AGGREGATE_OPERATIONS.earliest;
|
||||||
}
|
}
|
||||||
if (aggregateOperation === AGGREGATE_OPERATIONS.max) {
|
if (aggregateOperation === AGGREGATE_OPERATIONS.max) {
|
||||||
return 'LATEST';
|
return DATE_AGGREGATE_OPERATIONS.latest;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return aggregateOperation;
|
return aggregateOperation;
|
||||||
|
|||||||
@ -1,14 +1,15 @@
|
|||||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
||||||
|
import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/DateAggregateOperations';
|
||||||
|
|
||||||
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||||
|
|
||||||
export const convertExtendedAggregateOperationToAggregateOperation = (
|
export const convertExtendedAggregateOperationToAggregateOperation = (
|
||||||
extendedAggregateOperation: ExtendedAggregateOperations,
|
extendedAggregateOperation: ExtendedAggregateOperations | null,
|
||||||
) => {
|
) => {
|
||||||
if (extendedAggregateOperation === 'EARLIEST') {
|
if (extendedAggregateOperation === DATE_AGGREGATE_OPERATIONS.earliest) {
|
||||||
return AGGREGATE_OPERATIONS.min;
|
return AGGREGATE_OPERATIONS.min;
|
||||||
}
|
}
|
||||||
if (extendedAggregateOperation === 'LATEST') {
|
if (extendedAggregateOperation === DATE_AGGREGATE_OPERATIONS.latest) {
|
||||||
return AGGREGATE_OPERATIONS.max;
|
return AGGREGATE_OPERATIONS.max;
|
||||||
}
|
}
|
||||||
return extendedAggregateOperation;
|
return extendedAggregateOperation;
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
||||||
|
import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/DateAggregateOperations';
|
||||||
|
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||||
import { capitalize, isFieldMetadataDateKind } from 'twenty-shared';
|
import { capitalize, isFieldMetadataDateKind } from 'twenty-shared';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
type NameForAggregation = {
|
type NameForAggregation = {
|
||||||
[T in AGGREGATE_OPERATIONS]?: string;
|
[T in ExtendedAggregateOperations]?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Aggregations = {
|
type Aggregations = {
|
||||||
@ -58,8 +60,8 @@ export const getAvailableAggregationsFromObjectFields = (
|
|||||||
if (isFieldMetadataDateKind(field.type) === true) {
|
if (isFieldMetadataDateKind(field.type) === true) {
|
||||||
acc[field.name] = {
|
acc[field.name] = {
|
||||||
...acc[field.name],
|
...acc[field.name],
|
||||||
[AGGREGATE_OPERATIONS.min]: `min${capitalize(field.name)}`,
|
[DATE_AGGREGATE_OPERATIONS.earliest]: `min${capitalize(field.name)}`,
|
||||||
[AGGREGATE_OPERATIONS.max]: `max${capitalize(field.name)}`,
|
[DATE_AGGREGATE_OPERATIONS.latest]: `max${capitalize(field.name)}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,13 @@
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
|
||||||
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||||
import { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation';
|
import { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation';
|
||||||
import { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation';
|
|
||||||
import { getAvailableAggregationsFromObjectFields } from '@/object-record/utils/getAvailableAggregationsFromObjectFields';
|
import { getAvailableAggregationsFromObjectFields } from '@/object-record/utils/getAvailableAggregationsFromObjectFields';
|
||||||
import { initializeAvailableFieldsForAggregateOperationMap } from '@/object-record/utils/initializeAvailableFieldsForAggregateOperationMap';
|
import { initializeAvailableFieldsForAggregateOperationMap } from '@/object-record/utils/initializeAvailableFieldsForAggregateOperationMap';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
export const getAvailableFieldsIdsForAggregationFromObjectFields = (
|
export const getAvailableFieldsIdsForAggregationFromObjectFields = (
|
||||||
fields: FieldMetadataItem[],
|
fields: FieldMetadataItem[],
|
||||||
targetAggregateOperations: AGGREGATE_OPERATIONS[],
|
targetAggregateOperations: ExtendedAggregateOperations[],
|
||||||
): AvailableFieldsForAggregateOperation => {
|
): AvailableFieldsForAggregateOperation => {
|
||||||
const aggregationMap = initializeAvailableFieldsForAggregateOperationMap(
|
const aggregationMap = initializeAvailableFieldsForAggregateOperationMap(
|
||||||
targetAggregateOperations,
|
targetAggregateOperations,
|
||||||
@ -20,20 +18,12 @@ export const getAvailableFieldsIdsForAggregationFromObjectFields = (
|
|||||||
return fields.reduce((acc, field) => {
|
return fields.reduce((acc, field) => {
|
||||||
if (isDefined(allAggregations[field.name])) {
|
if (isDefined(allAggregations[field.name])) {
|
||||||
Object.keys(allAggregations[field.name]).forEach((aggregation) => {
|
Object.keys(allAggregations[field.name]).forEach((aggregation) => {
|
||||||
if (
|
const typedAggregation = aggregation as ExtendedAggregateOperations;
|
||||||
targetAggregateOperations.includes(
|
if (targetAggregateOperations.includes(typedAggregation)) {
|
||||||
aggregation as AGGREGATE_OPERATIONS,
|
if (!isDefined(acc[typedAggregation])) {
|
||||||
)
|
acc[typedAggregation] = [];
|
||||||
) {
|
|
||||||
const convertedAggregateOperation: ExtendedAggregateOperations =
|
|
||||||
convertAggregateOperationToExtendedAggregateOperation(
|
|
||||||
aggregation as AGGREGATE_OPERATIONS,
|
|
||||||
field.type,
|
|
||||||
);
|
|
||||||
if (!isDefined(acc[convertedAggregateOperation])) {
|
|
||||||
acc[convertedAggregateOperation] = [];
|
|
||||||
}
|
}
|
||||||
(acc[convertedAggregateOperation] as string[]).push(field.id);
|
(acc[typedAggregation] as string[]).push(field.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||||
import { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation';
|
import { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation';
|
||||||
import { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation';
|
|
||||||
|
|
||||||
export const initializeAvailableFieldsForAggregateOperationMap = (
|
export const initializeAvailableFieldsForAggregateOperationMap = (
|
||||||
aggregateOperations: AGGREGATE_OPERATIONS[],
|
aggregateOperations: ExtendedAggregateOperations[],
|
||||||
): AvailableFieldsForAggregateOperation => {
|
): AvailableFieldsForAggregateOperation => {
|
||||||
return aggregateOperations.reduce(
|
return aggregateOperations.reduce(
|
||||||
(acc, operation) => ({
|
(acc, operation) => ({
|
||||||
...acc,
|
...acc,
|
||||||
[convertAggregateOperationToExtendedAggregateOperation(operation)]: [],
|
[operation]: [],
|
||||||
}),
|
}),
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,30 +1,28 @@
|
|||||||
|
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||||
|
import { DropdownOnToggleEffect } from '@/ui/layout/dropdown/components/DropdownOnToggleEffect';
|
||||||
|
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponeInstanceContext';
|
||||||
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||||
|
import { dropdownHotkeyComponentState } from '@/ui/layout/dropdown/states/dropdownHotkeyComponentState';
|
||||||
|
import { dropdownMaxHeightComponentStateV2 } from '@/ui/layout/dropdown/states/dropdownMaxHeightComponentStateV2';
|
||||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||||
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
||||||
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
import {
|
import {
|
||||||
|
Placement,
|
||||||
autoUpdate,
|
autoUpdate,
|
||||||
flip,
|
flip,
|
||||||
offset,
|
offset,
|
||||||
Placement,
|
|
||||||
size,
|
size,
|
||||||
useFloating,
|
useFloating,
|
||||||
} from '@floating-ui/react';
|
} from '@floating-ui/react';
|
||||||
import { MouseEvent, ReactNode } from 'react';
|
import { MouseEvent, ReactNode } from 'react';
|
||||||
import { Keys } from 'react-hotkeys-hook';
|
|
||||||
|
|
||||||
import { useDropdown } from '../hooks/useDropdown';
|
|
||||||
|
|
||||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
|
||||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponeInstanceContext';
|
|
||||||
import { dropdownHotkeyComponentState } from '@/ui/layout/dropdown/states/dropdownHotkeyComponentState';
|
|
||||||
import { dropdownMaxHeightComponentStateV2 } from '@/ui/layout/dropdown/states/dropdownMaxHeightComponentStateV2';
|
|
||||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { flushSync } from 'react-dom';
|
import { flushSync } from 'react-dom';
|
||||||
|
import { Keys } from 'react-hotkeys-hook';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
import { sleep } from '~/utils/sleep';
|
import { sleep } from '~/utils/sleep';
|
||||||
import { DropdownOnToggleEffect } from './DropdownOnToggleEffect';
|
import { useDropdown } from '../hooks/useDropdown';
|
||||||
|
|
||||||
const StyledDropdownFallbackAnchor = styled.div`
|
const StyledDropdownFallbackAnchor = styled.div`
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -32,6 +30,26 @@ const StyledDropdownFallbackAnchor = styled.div`
|
|||||||
top: 0;
|
top: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
type StyledHeaderDivProps = {
|
||||||
|
isUnfolded?: boolean;
|
||||||
|
isActive?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledHeaderDiv = styled.div<StyledHeaderDivProps>`
|
||||||
|
& button,
|
||||||
|
& > * {
|
||||||
|
background: ${({ theme, isUnfolded }) =>
|
||||||
|
isUnfolded ? theme.background.transparent.light : 'none'};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${({ theme, isUnfolded }) =>
|
||||||
|
isUnfolded
|
||||||
|
? theme.background.transparent.medium
|
||||||
|
: theme.background.transparent.light};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
type DropdownProps = {
|
type DropdownProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
clickableComponent?: ReactNode;
|
clickableComponent?: ReactNode;
|
||||||
@ -133,16 +151,17 @@ export const Dropdown = ({
|
|||||||
<DropdownScope dropdownScopeId={getScopeIdFromComponentId(dropdownId)}>
|
<DropdownScope dropdownScopeId={getScopeIdFromComponentId(dropdownId)}>
|
||||||
<>
|
<>
|
||||||
{isDefined(clickableComponent) ? (
|
{isDefined(clickableComponent) ? (
|
||||||
<div
|
<StyledHeaderDiv
|
||||||
ref={refs.setReference}
|
ref={refs.setReference}
|
||||||
onClick={handleClickableComponentClick}
|
onClick={handleClickableComponentClick}
|
||||||
aria-controls={`${dropdownId}-options`}
|
aria-controls={`${dropdownId}-options`}
|
||||||
aria-expanded={isDropdownOpen}
|
aria-expanded={isDropdownOpen}
|
||||||
aria-haspopup={true}
|
aria-haspopup={true}
|
||||||
role="button"
|
role="button"
|
||||||
|
isUnfolded={isDropdownOpen}
|
||||||
>
|
>
|
||||||
{clickableComponent}
|
{clickableComponent}
|
||||||
</div>
|
</StyledHeaderDiv>
|
||||||
) : (
|
) : (
|
||||||
<StyledDropdownFallbackAnchor ref={refs.setReference} />
|
<StyledDropdownFallbackAnchor ref={refs.setReference} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -8,8 +8,6 @@ type StyledDropdownButtonProps = {
|
|||||||
export const StyledHeaderDropdownButton = styled.button<StyledDropdownButtonProps>`
|
export const StyledHeaderDropdownButton = styled.button<StyledDropdownButtonProps>`
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: ${({ theme, isUnfolded }) =>
|
|
||||||
isUnfolded ? theme.background.transparent.light : theme.background.primary};
|
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
color: ${({ isActive, theme }) =>
|
color: ${({ isActive, theme }) =>
|
||||||
@ -22,11 +20,4 @@ export const StyledHeaderDropdownButton = styled.button<StyledDropdownButtonProp
|
|||||||
|
|
||||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: ${({ theme, isUnfolded }) =>
|
|
||||||
isUnfolded
|
|
||||||
? theme.background.transparent.medium
|
|
||||||
: theme.background.transparent.light};
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||||
|
import { convertExtendedAggregateOperationToAggregateOperation } from '@/object-record/utils/convertExtendedAggregateOperationToAggregateOperation';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { useUpdateView } from '@/views/hooks/useUpdateView';
|
import { useUpdateView } from '@/views/hooks/useUpdateView';
|
||||||
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
|
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
|
||||||
@ -13,13 +14,18 @@ export const useUpdateViewAggregate = () => {
|
|||||||
kanbanAggregateOperation,
|
kanbanAggregateOperation,
|
||||||
}: {
|
}: {
|
||||||
kanbanAggregateOperationFieldMetadataId: string | null;
|
kanbanAggregateOperationFieldMetadataId: string | null;
|
||||||
kanbanAggregateOperation: AGGREGATE_OPERATIONS | null;
|
kanbanAggregateOperation: ExtendedAggregateOperations | null;
|
||||||
}) =>
|
}) => {
|
||||||
|
const convertedKanbanAggregateOperation =
|
||||||
|
convertExtendedAggregateOperationToAggregateOperation(
|
||||||
|
kanbanAggregateOperation,
|
||||||
|
);
|
||||||
updateView({
|
updateView({
|
||||||
id: currentViewId,
|
id: currentViewId,
|
||||||
kanbanAggregateOperationFieldMetadataId,
|
kanbanAggregateOperationFieldMetadataId,
|
||||||
kanbanAggregateOperation,
|
kanbanAggregateOperation: convertedKanbanAggregateOperation,
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
[currentViewId, updateView],
|
[currentViewId, updateView],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { FieldMetadataType } from 'src/types/FieldMetadataType';
|
import { FieldMetadataType } from 'src/types/FieldMetadataType';
|
||||||
|
|
||||||
export const isFieldMetadataDateKind = (
|
export const isFieldMetadataDateKind = (
|
||||||
fieldMetadataType: FieldMetadataType,
|
fieldMetadataType?: FieldMetadataType,
|
||||||
): fieldMetadataType is
|
): fieldMetadataType is
|
||||||
| FieldMetadataType.DATE
|
| FieldMetadataType.DATE
|
||||||
| FieldMetadataType.DATE_TIME => {
|
| FieldMetadataType.DATE_TIME => {
|
||||||
|
|||||||
Reference in New Issue
Block a user