mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-22 19:41:53 +03:00
Improve aggregate footer cell display (#9124)
Co-authored-by: Jérémy Magrin <jeremy.magrin@gmail.com> Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
parent
7d8f895ae9
commit
ed56a68b7c
@ -1,7 +1,6 @@
|
||||
import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton';
|
||||
import styled from '@emotion/styled';
|
||||
import { AppTooltip, Tag, TooltipDelay } from 'twenty-ui';
|
||||
import { formatNumber } from '~/utils/format/number';
|
||||
|
||||
const StyledButton = styled(StyledHeaderDropdownButton)`
|
||||
padding: 0;
|
||||
@ -19,10 +18,7 @@ export const RecordBoardColumnHeaderAggregateDropdownButton = ({
|
||||
return (
|
||||
<div id={dropdownId}>
|
||||
<StyledButton>
|
||||
<Tag
|
||||
text={value ? formatNumber(Number(value)) : '-'}
|
||||
color={'transparent'}
|
||||
/>
|
||||
<Tag text={value ? value.toString() : '-'} color={'transparent'} />
|
||||
<AppTooltip
|
||||
anchorSelect={`#${dropdownId}`}
|
||||
content={tooltip}
|
||||
|
@ -74,7 +74,7 @@ export const useAggregateRecordsForRecordBoardColumn = () => {
|
||||
skip: !isAggregateQueryEnabled,
|
||||
});
|
||||
|
||||
const { value, label } = computeAggregateValueAndLabel({
|
||||
const { value, labelWithFieldName } = computeAggregateValueAndLabel({
|
||||
data,
|
||||
objectMetadataItem,
|
||||
fieldMetadataId: recordIndexKanbanAggregateOperation?.fieldMetadataId,
|
||||
@ -84,6 +84,6 @@ export const useAggregateRecordsForRecordBoardColumn = () => {
|
||||
|
||||
return {
|
||||
aggregateValue: isAggregateQueryEnabled ? value : recordCount,
|
||||
aggregateLabel: isDefined(value) ? label : undefined,
|
||||
aggregateLabel: isDefined(value) ? labelWithFieldName : undefined,
|
||||
};
|
||||
};
|
||||
|
@ -47,8 +47,81 @@ describe('computeAggregateValueAndLabel', () => {
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
value: 2,
|
||||
label: 'Sum of amount',
|
||||
value: '2',
|
||||
label: 'Sum',
|
||||
labelWithFieldName: 'Sum of amount',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle number field as percentage', () => {
|
||||
const mockObjectMetadataWithPercentageField: ObjectMetadataItem = {
|
||||
id: '123',
|
||||
fields: [
|
||||
{
|
||||
id: MOCK_FIELD_ID,
|
||||
name: 'percentage',
|
||||
type: FieldMetadataType.Number,
|
||||
settings: {
|
||||
type: 'percentage',
|
||||
},
|
||||
} as FieldMetadataItem,
|
||||
],
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
const mockData = {
|
||||
percentage: {
|
||||
[AGGREGATE_OPERATIONS.avg]: 0.3,
|
||||
},
|
||||
};
|
||||
|
||||
const result = computeAggregateValueAndLabel({
|
||||
data: mockData,
|
||||
objectMetadataItem: mockObjectMetadataWithPercentageField,
|
||||
fieldMetadataId: MOCK_FIELD_ID,
|
||||
aggregateOperation: AGGREGATE_OPERATIONS.avg,
|
||||
fallbackFieldName: MOCK_KANBAN_FIELD_NAME,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
value: '30%',
|
||||
label: 'Average',
|
||||
labelWithFieldName: 'Average of percentage',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle number field with decimals', () => {
|
||||
const mockObjectMetadataWithDecimalsField: ObjectMetadataItem = {
|
||||
id: '123',
|
||||
fields: [
|
||||
{
|
||||
id: MOCK_FIELD_ID,
|
||||
name: 'decimals',
|
||||
type: FieldMetadataType.Number,
|
||||
settings: {
|
||||
decimals: 2,
|
||||
},
|
||||
} as FieldMetadataItem,
|
||||
],
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
const mockData = {
|
||||
decimals: {
|
||||
[AGGREGATE_OPERATIONS.sum]: 0.009,
|
||||
},
|
||||
};
|
||||
|
||||
const result = computeAggregateValueAndLabel({
|
||||
data: mockData,
|
||||
objectMetadataItem: mockObjectMetadataWithDecimalsField,
|
||||
fieldMetadataId: MOCK_FIELD_ID,
|
||||
aggregateOperation: AGGREGATE_OPERATIONS.sum,
|
||||
fallbackFieldName: MOCK_KANBAN_FIELD_NAME,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
value: '0.01',
|
||||
label: 'Sum',
|
||||
labelWithFieldName: 'Sum of decimals',
|
||||
});
|
||||
});
|
||||
|
||||
@ -86,8 +159,9 @@ describe('computeAggregateValueAndLabel', () => {
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
value: undefined,
|
||||
label: 'Sum of amount',
|
||||
value: '-',
|
||||
label: 'Sum',
|
||||
labelWithFieldName: 'Sum of amount',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -4,6 +4,8 @@ import { getAggregateOperationLabel } from '@/object-record/record-board/record-
|
||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { formatAmount } from '~/utils/format/formatAmount';
|
||||
import { formatNumber } from '~/utils/format/number';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const computeAggregateValueAndLabel = ({
|
||||
@ -42,12 +44,33 @@ export const computeAggregateValueAndLabel = ({
|
||||
|
||||
const aggregateValue = data[field.name]?.[aggregateOperation];
|
||||
|
||||
const value =
|
||||
isDefined(aggregateValue) && field.type === FieldMetadataType.Currency
|
||||
? Number(aggregateValue) / 1_000_000
|
||||
: aggregateValue;
|
||||
let value;
|
||||
|
||||
const label =
|
||||
if (aggregateOperation === AGGREGATE_OPERATIONS.count) {
|
||||
value = aggregateValue;
|
||||
} else if (!isDefined(aggregateValue)) {
|
||||
value = '-';
|
||||
} else {
|
||||
value = Number(aggregateValue);
|
||||
|
||||
switch (field.type) {
|
||||
case FieldMetadataType.Currency: {
|
||||
value = formatAmount(value / 1_000_000);
|
||||
break;
|
||||
}
|
||||
|
||||
case FieldMetadataType.Number: {
|
||||
const { decimals, type } = field.settings ?? {};
|
||||
value =
|
||||
type === 'percentage'
|
||||
? `${formatNumber(value * 100, decimals)}%`
|
||||
: formatNumber(value, decimals);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const label = getAggregateOperationLabel(aggregateOperation);
|
||||
const labelWithFieldName =
|
||||
aggregateOperation === AGGREGATE_OPERATIONS.count
|
||||
? `${getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)}`
|
||||
: `${getAggregateOperationLabel(aggregateOperation)} of ${field.name}`;
|
||||
@ -55,5 +78,6 @@ export const computeAggregateValueAndLabel = ({
|
||||
return {
|
||||
value,
|
||||
label,
|
||||
labelWithFieldName,
|
||||
};
|
||||
};
|
||||
|
@ -4,6 +4,6 @@ import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewCompon
|
||||
export const recordIndexRecordGroupHideComponentState =
|
||||
createComponentStateV2<boolean>({
|
||||
key: 'recordIndexRecordGroupHideComponentState',
|
||||
defaultValue: true,
|
||||
defaultValue: false,
|
||||
componentInstanceContext: ViewComponentInstanceContext,
|
||||
});
|
||||
|
@ -13,7 +13,7 @@ import { RecordTableNoRecordGroupBody } from '@/object-record/record-table/recor
|
||||
import { RecordTableNoRecordGroupBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect';
|
||||
import { RecordTableRecordGroupBodyEffects } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects';
|
||||
import { RecordTableRecordGroupsBody } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody';
|
||||
import { RecordTableFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableFooter';
|
||||
import { RecordTableAggregateFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter';
|
||||
import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader';
|
||||
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
|
||||
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
|
||||
@ -88,7 +88,7 @@ export const RecordTable = () => {
|
||||
<RecordTableEmptyState />
|
||||
) : (
|
||||
<>
|
||||
<StyledTable className="entity-table-cell" ref={tableBodyRef}>
|
||||
<StyledTable ref={tableBodyRef}>
|
||||
<RecordTableHeader />
|
||||
{!hasRecordGroups ? (
|
||||
<RecordTableNoRecordGroupBody />
|
||||
@ -97,7 +97,7 @@ export const RecordTable = () => {
|
||||
)}
|
||||
<RecordTableStickyEffect />
|
||||
{isAggregateQueryEnabled && !hasRecordGroups && (
|
||||
<RecordTableFooter />
|
||||
<RecordTableAggregateFooter endOfTableSticky />
|
||||
)}
|
||||
</StyledTable>
|
||||
<DragSelect
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
|
||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { RecordTableFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableFooter';
|
||||
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 { RecordTableRecordGroupSectionAddNew } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew';
|
||||
@ -9,14 +8,10 @@ import { RecordTableRecordGroupSectionLoadMore } from '@/object-record/record-ta
|
||||
import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState';
|
||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useMemo } from 'react';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const RecordTableRecordGroupRows = () => {
|
||||
const isAggregateQueryEnabled = useIsFeatureEnabled(
|
||||
'IS_AGGREGATE_QUERY_ENABLED',
|
||||
);
|
||||
const currentRecordGroupId = useCurrentRecordGroupId();
|
||||
|
||||
const allRecordIds = useRecoilComponentValueV2(
|
||||
@ -63,12 +58,6 @@ export const RecordTableRecordGroupRows = () => {
|
||||
})}
|
||||
<RecordTablePendingRecordGroupRow />
|
||||
<RecordTableRecordGroupSectionAddNew />
|
||||
{isAggregateQueryEnabled && (
|
||||
<RecordTableFooter
|
||||
key={currentRecordGroupId}
|
||||
currentRecordGroupId={currentRecordGroupId}
|
||||
/>
|
||||
)}
|
||||
<RecordTableRecordGroupSectionLoadMore />
|
||||
</>
|
||||
);
|
||||
|
@ -15,6 +15,7 @@ export const RecordTableBodyDroppable = ({
|
||||
isDropDisabled,
|
||||
}: RecordTableBodyDroppableProps) => {
|
||||
const [v4Persistable] = useState(v4());
|
||||
const recordTableBodyId = `record-table-body${recordGroupId ? '-' + recordGroupId : ''}`;
|
||||
|
||||
return (
|
||||
<Droppable
|
||||
@ -23,7 +24,7 @@ export const RecordTableBodyDroppable = ({
|
||||
>
|
||||
{(provided) => (
|
||||
<RecordTableBody
|
||||
id="record-table-body"
|
||||
id={recordTableBodyId}
|
||||
ref={provided.innerRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.droppableProps}
|
||||
|
@ -6,12 +6,17 @@ import { RecordTableRecordGroupRows } from '@/object-record/record-table/compone
|
||||
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 { RecordTableBodyRecordGroupDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider';
|
||||
import { RecordTableAggregateFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter';
|
||||
import { RecordTableRecordGroupEmptyRow } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupEmptyRow';
|
||||
import { RecordTableRecordGroupSection } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection';
|
||||
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
|
||||
export const RecordTableRecordGroupsBody = () => {
|
||||
const isAggregateQueryEnabled = useIsFeatureEnabled(
|
||||
'IS_AGGREGATE_QUERY_ENABLED',
|
||||
);
|
||||
const allRecordIds = useRecoilComponentValueV2(
|
||||
recordIndexAllRecordIdsComponentSelector,
|
||||
);
|
||||
@ -29,21 +34,29 @@ export const RecordTableRecordGroupsBody = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<RecordTableBodyRecordGroupDragDropContextProvider>
|
||||
{visibleRecordGroupIds.map((recordGroupId, index) => (
|
||||
<RecordTableRecordGroupBodyContextProvider
|
||||
key={recordGroupId}
|
||||
recordGroupId={recordGroupId}
|
||||
>
|
||||
<RecordGroupContext.Provider value={{ recordGroupId }}>
|
||||
<RecordTableBodyDroppable recordGroupId={recordGroupId}>
|
||||
{index > 0 && <RecordTableRecordGroupEmptyRow />}
|
||||
<RecordTableRecordGroupSection />
|
||||
<RecordTableRecordGroupRows />
|
||||
</RecordTableBodyDroppable>
|
||||
</RecordGroupContext.Provider>
|
||||
</RecordTableRecordGroupBodyContextProvider>
|
||||
))}
|
||||
</RecordTableBodyRecordGroupDragDropContextProvider>
|
||||
<>
|
||||
<RecordTableBodyRecordGroupDragDropContextProvider>
|
||||
{visibleRecordGroupIds.map((recordGroupId, index) => (
|
||||
<RecordTableRecordGroupBodyContextProvider
|
||||
key={recordGroupId}
|
||||
recordGroupId={recordGroupId}
|
||||
>
|
||||
<RecordGroupContext.Provider value={{ recordGroupId }}>
|
||||
<RecordTableBodyDroppable recordGroupId={recordGroupId}>
|
||||
{index > 0 && <RecordTableRecordGroupEmptyRow />}
|
||||
<RecordTableRecordGroupSection />
|
||||
<RecordTableRecordGroupRows />
|
||||
</RecordTableBodyDroppable>
|
||||
{isAggregateQueryEnabled && (
|
||||
<RecordTableAggregateFooter
|
||||
key={recordGroupId}
|
||||
currentRecordGroupId={recordGroupId}
|
||||
/>
|
||||
)}
|
||||
</RecordGroupContext.Provider>
|
||||
</RecordTableRecordGroupBodyContextProvider>
|
||||
))}
|
||||
</RecordTableBodyRecordGroupDragDropContextProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,17 +1,16 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { MOBILE_VIEWPORT } from 'twenty-ui';
|
||||
|
||||
import { TABLE_CELL_CHECKBOX_MIN_WIDTH } from '@/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox';
|
||||
import { TABLE_CELL_GRIP_WIDTH } from '@/object-record/record-table/record-table-cell/components/RecordTableCellGrip';
|
||||
import { RecordTableFooterCell } from '@/object-record/record-table/record-table-footer/components/RecordTableFooterCell';
|
||||
import { RecordTableAggregateFooterCell } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell';
|
||||
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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
|
||||
const StyledTableFoot = styled.thead`
|
||||
const StyledTableFoot = styled.thead<{ endOfTableSticky?: boolean }>`
|
||||
cursor: pointer;
|
||||
|
||||
th:nth-of-type(1) {
|
||||
width: 9px;
|
||||
width: ${FIRST_TH_WIDTH};
|
||||
left: 0;
|
||||
border-right-color: ${({ theme }) => theme.background.primary};
|
||||
}
|
||||
@ -59,31 +58,23 @@ const StyledTableFoot = styled.thead`
|
||||
}
|
||||
}
|
||||
|
||||
&.header-sticky {
|
||||
th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
}
|
||||
}
|
||||
|
||||
&.header-sticky.first-columns-sticky {
|
||||
th:nth-of-type(1),
|
||||
th:nth-of-type(2),
|
||||
th:nth-of-type(3) {
|
||||
z-index: 10;
|
||||
}
|
||||
tr {
|
||||
position: sticky;
|
||||
z-index: 5;
|
||||
${({ endOfTableSticky }) => endOfTableSticky && `bottom: 0;`}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
width: calc(${TABLE_CELL_GRIP_WIDTH} + ${TABLE_CELL_CHECKBOX_MIN_WIDTH});
|
||||
const StyledTh = styled.th`
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
`;
|
||||
|
||||
export const RecordTableFooter = ({
|
||||
export const RecordTableAggregateFooter = ({
|
||||
currentRecordGroupId,
|
||||
endOfTableSticky,
|
||||
}: {
|
||||
currentRecordGroupId?: string;
|
||||
endOfTableSticky?: boolean;
|
||||
}) => {
|
||||
const visibleTableColumns = useRecoilComponentValueV2(
|
||||
visibleTableColumnsComponentSelector,
|
||||
@ -93,12 +84,13 @@ export const RecordTableFooter = ({
|
||||
<StyledTableFoot
|
||||
id={`record-table-footer${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`}
|
||||
data-select-disable
|
||||
endOfTableSticky={endOfTableSticky}
|
||||
>
|
||||
<tr>
|
||||
<th />
|
||||
<StyledDiv />
|
||||
<StyledTh />
|
||||
<StyledTh />
|
||||
{visibleTableColumns.map((column, index) => (
|
||||
<RecordTableFooterCell
|
||||
<RecordTableAggregateFooterCell
|
||||
key={`${column.fieldMetadataId}${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`}
|
||||
column={column}
|
||||
currentRecordGroupId={currentRecordGroupId}
|
@ -2,7 +2,7 @@ import styled from '@emotion/styled';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { RecordTableColumnFooterWithDropdown } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnFooterWithDropdown';
|
||||
import { RecordTableColumnFooterWithDropdown } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterWithDropdown';
|
||||
import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
@ -61,7 +61,7 @@ const StyledColumnFootContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const RecordTableFooterCell = ({
|
||||
export const RecordTableAggregateFooterCell = ({
|
||||
column,
|
||||
isFirstCell = false,
|
||||
currentRecordGroupId,
|
@ -14,7 +14,7 @@ import { useMemo } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { MenuItem } from 'twenty-ui';
|
||||
|
||||
export const RecordTableColumnFooterDropdown = ({
|
||||
export const RecordTableColumnAggregateFooterDropdown = ({
|
||||
column,
|
||||
dropdownId,
|
||||
}: {
|
||||
@ -30,10 +30,6 @@ export const RecordTableColumnFooterDropdown = ({
|
||||
(viewField) => viewField.fieldMetadataId === column.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!currentViewField) {
|
||||
throw new Error('ViewField not found');
|
||||
}
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Escape],
|
||||
() => {
|
||||
@ -56,6 +52,9 @@ export const RecordTableColumnFooterDropdown = ({
|
||||
const handleAggregationChange = (
|
||||
aggregateOperation: AGGREGATE_OPERATIONS,
|
||||
) => {
|
||||
if (!currentViewField) {
|
||||
throw new Error('ViewField not found');
|
||||
}
|
||||
updateViewFieldRecords([
|
||||
{ ...currentViewField, aggregateOperation: aggregateOperation },
|
||||
]);
|
@ -1,12 +1,7 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
AppTooltip,
|
||||
IconChevronDown,
|
||||
isDefined,
|
||||
TooltipDelay,
|
||||
} from 'twenty-ui';
|
||||
import { IconChevronDown, isDefined } from 'twenty-ui';
|
||||
|
||||
const StyledCell = styled.div`
|
||||
align-items: center;
|
||||
@ -37,6 +32,27 @@ const StyledText = styled.span`
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const StyledValueContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 1 0 0;
|
||||
gap: 4px;
|
||||
height: 32px;
|
||||
justify-content: flex-end;
|
||||
padding: 8px;
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
`;
|
||||
|
||||
const StyledValue = styled.div`
|
||||
color: ${({ theme }) => theme.color.gray60};
|
||||
flex: 1 0 0;
|
||||
`;
|
||||
|
||||
const StyledIcon = styled(IconChevronDown)`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
@ -46,7 +62,7 @@ const StyledIcon = styled(IconChevronDown)`
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const RecordTableColumnFooterAggregateValue = ({
|
||||
export const RecordTableColumnAggregateFooterValue = ({
|
||||
dropdownId,
|
||||
aggregateValue,
|
||||
aggregateLabel,
|
||||
@ -70,20 +86,15 @@ export const RecordTableColumnFooterAggregateValue = ({
|
||||
<StyledCell>
|
||||
{isHovered || isDefined(aggregateValue) || isFirstCell ? (
|
||||
<>
|
||||
<StyledText id={sanitizedId}>
|
||||
{aggregateValue ?? 'Calculate'}
|
||||
</StyledText>
|
||||
<StyledIcon fontWeight={'light'} size={theme.icon.size.sm} />
|
||||
{isDefined(aggregateValue) && isDefined(aggregateLabel) && (
|
||||
<AppTooltip
|
||||
anchorSelect={`#${sanitizedId}`}
|
||||
content={aggregateLabel}
|
||||
noArrow
|
||||
place="top-start"
|
||||
positionStrategy="fixed"
|
||||
delay={TooltipDelay.shortDelay}
|
||||
/>
|
||||
{isDefined(aggregateValue) ? (
|
||||
<StyledValueContainer>
|
||||
<StyledLabel>{aggregateLabel}</StyledLabel>
|
||||
<StyledValue>{aggregateValue}</StyledValue>
|
||||
</StyledValueContainer>
|
||||
) : (
|
||||
<StyledText id={sanitizedId}>Calculate</StyledText>
|
||||
)}
|
||||
<StyledIcon fontWeight={'light'} size={theme.icon.size.sm} />
|
||||
</>
|
||||
) : (
|
||||
<></>
|
@ -1,6 +1,6 @@
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { RecordTableColumnFooterAggregateValue } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnFooterAggregateValue';
|
||||
import { RecordTableColumnFooterDropdown } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnFooterDropdown';
|
||||
import { RecordTableColumnAggregateFooterDropdown } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdown';
|
||||
import { RecordTableColumnAggregateFooterValue } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValue';
|
||||
import { useAggregateRecordsForRecordTableColumnFooter } from '@/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
@ -44,7 +44,7 @@ export const RecordTableColumnFooterWithDropdown = ({
|
||||
onClose={handleDropdownClose}
|
||||
dropdownId={dropdownId}
|
||||
clickableComponent={
|
||||
<RecordTableColumnFooterAggregateValue
|
||||
<RecordTableColumnAggregateFooterValue
|
||||
aggregateLabel={aggregateLabel}
|
||||
aggregateValue={aggregateValue}
|
||||
dropdownId={dropdownId}
|
||||
@ -52,7 +52,7 @@ export const RecordTableColumnFooterWithDropdown = ({
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<RecordTableColumnFooterDropdown
|
||||
<RecordTableColumnAggregateFooterDropdown
|
||||
column={column}
|
||||
dropdownId={dropdownId}
|
||||
/>
|
@ -32,13 +32,10 @@ export const useAggregateRecordsForRecordTableColumnFooter = (
|
||||
recordIndexViewFilterGroups,
|
||||
);
|
||||
|
||||
const viewFieldId = currentViewWithSavedFiltersAndSorts?.viewFields?.find(
|
||||
(viewField) => viewField.fieldMetadataId === fieldMetadataId,
|
||||
)?.id;
|
||||
|
||||
if (!viewFieldId) {
|
||||
throw new Error('ViewField not found');
|
||||
}
|
||||
const viewFieldId =
|
||||
currentViewWithSavedFiltersAndSorts?.viewFields?.find(
|
||||
(viewField) => viewField.fieldMetadataId === fieldMetadataId,
|
||||
)?.id ?? '';
|
||||
|
||||
const aggregateOperationForViewField = useRecoilValue(
|
||||
aggregateOperationForViewFieldState({ viewFieldId: viewFieldId }),
|
||||
|
@ -7,11 +7,13 @@ import { RecordTableHeaderCheckboxColumn } from '@/object-record/record-table/re
|
||||
import { RecordTableHeaderDragDropColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn';
|
||||
import { RecordTableHeaderLastColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn';
|
||||
|
||||
export const FIRST_TH_WIDTH = '9px';
|
||||
|
||||
const StyledTableHead = styled.thead`
|
||||
cursor: pointer;
|
||||
|
||||
th:nth-of-type(1) {
|
||||
width: 9px;
|
||||
width: ${FIRST_TH_WIDTH};
|
||||
left: 0;
|
||||
border-right-color: ${({ theme }) => theme.background.primary};
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/s
|
||||
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
|
||||
import { RecordTableRecordGroupStickyEffect } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupStickyEffect';
|
||||
import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState';
|
||||
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
|
||||
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
|
||||
@ -107,6 +108,7 @@ export const RecordTableRecordGroupSection = () => {
|
||||
weight="medium"
|
||||
/>
|
||||
<StyledTotalRow>{recordIdsByGroup.length}</StyledTotalRow>
|
||||
<RecordTableRecordGroupStickyEffect />
|
||||
</StyledRecordGroupSection>
|
||||
<StyledEmptyTd colSpan={visibleColumns.length - 1} />
|
||||
<StyledEmptyTd />
|
||||
|
@ -0,0 +1,48 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
|
||||
import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState';
|
||||
import { scrollWrapperScrollLeftComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollLeftComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
|
||||
export const RecordTableRecordGroupStickyEffect = () => {
|
||||
const scrollLeft = useRecoilComponentValueV2(
|
||||
scrollWrapperScrollLeftComponentState,
|
||||
);
|
||||
|
||||
const setIsRecordTableScrolledLeft = useSetRecoilComponentStateV2(
|
||||
isRecordTableScrolledLeftComponentState,
|
||||
);
|
||||
|
||||
const currentRecordGroupId = useCurrentRecordGroupId();
|
||||
|
||||
useEffect(() => {
|
||||
setIsRecordTableScrolledLeft(scrollLeft === 0);
|
||||
if (scrollLeft > 0) {
|
||||
document
|
||||
.getElementById(
|
||||
`record-table-footer${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`,
|
||||
)
|
||||
?.classList.add('first-columns-sticky');
|
||||
document
|
||||
.getElementById(
|
||||
`record-table-body${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`,
|
||||
)
|
||||
?.classList.add('first-columns-sticky');
|
||||
} else {
|
||||
document
|
||||
.getElementById(
|
||||
`record-table-footer${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`,
|
||||
)
|
||||
?.classList.remove('first-columns-sticky');
|
||||
document
|
||||
.getElementById(
|
||||
`record-table-body${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`,
|
||||
)
|
||||
?.classList.remove('first-columns-sticky');
|
||||
}
|
||||
}, [currentRecordGroupId, scrollLeft, setIsRecordTableScrolledLeft]);
|
||||
|
||||
return <></>;
|
||||
};
|
@ -19,6 +19,7 @@ export type ContextProviderName =
|
||||
| 'test'
|
||||
| 'showPageActivityContainer'
|
||||
| 'navigationDrawer'
|
||||
| 'aggregateFooterCell'
|
||||
| 'modalContent';
|
||||
|
||||
const createScrollWrapperContext = (id: string) =>
|
||||
@ -52,6 +53,8 @@ export const ShowPageActivityContainerScrollWrapperContext =
|
||||
export const NavigationDrawerScrollWrapperContext =
|
||||
createScrollWrapperContext('navigationDrawer');
|
||||
export const TestScrollWrapperContext = createScrollWrapperContext('test');
|
||||
export const AggregateFooterCellScrollWrapperContext =
|
||||
createScrollWrapperContext('aggregateFooterCell');
|
||||
export const ModalContentScrollWrapperContext =
|
||||
createScrollWrapperContext('modalContent');
|
||||
|
||||
@ -85,6 +88,8 @@ export const getContextByProviderName = (
|
||||
return ShowPageActivityContainerScrollWrapperContext;
|
||||
case 'navigationDrawer':
|
||||
return NavigationDrawerScrollWrapperContext;
|
||||
case 'aggregateFooterCell':
|
||||
return AggregateFooterCellScrollWrapperContext;
|
||||
case 'modalContent':
|
||||
return ModalContentScrollWrapperContext;
|
||||
default:
|
||||
|
@ -20,6 +20,7 @@ import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-met
|
||||
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
|
||||
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
|
||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
import { isPlainObject } from 'src/utils/is-plain-object';
|
||||
|
||||
export class ObjectRecordsToGraphqlConnectionHelper {
|
||||
@ -95,7 +96,7 @@ export class ObjectRecordsToGraphqlConnectionHelper {
|
||||
selectedAggregatedFields: Record<string, AggregationField[]>;
|
||||
objectRecordsAggregatedValues: Record<string, any>;
|
||||
}) => {
|
||||
if (!objectRecordsAggregatedValues) {
|
||||
if (!isDefined(objectRecordsAggregatedValues)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@ -104,7 +105,7 @@ export class ObjectRecordsToGraphqlConnectionHelper {
|
||||
const aggregatedFieldValue =
|
||||
objectRecordsAggregatedValues[aggregatedFieldName];
|
||||
|
||||
if (!aggregatedFieldValue) {
|
||||
if (!isDefined(aggregatedFieldValue)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user