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:
Marie 2024-12-19 15:36:14 +01:00 committed by GitHub
parent 7d8f895ae9
commit ed56a68b7c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 268 additions and 114 deletions

View File

@ -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}

View File

@ -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,
};
};

View File

@ -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',
});
});
});

View File

@ -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,
};
};

View File

@ -4,6 +4,6 @@ import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewCompon
export const recordIndexRecordGroupHideComponentState =
createComponentStateV2<boolean>({
key: 'recordIndexRecordGroupHideComponentState',
defaultValue: true,
defaultValue: false,
componentInstanceContext: ViewComponentInstanceContext,
});

View File

@ -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

View File

@ -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 />
</>
);

View File

@ -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}

View File

@ -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>
</>
);
};

View File

@ -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}

View File

@ -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,

View File

@ -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 },
]);

View File

@ -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} />
</>
) : (
<></>

View File

@ -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}
/>

View File

@ -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 }),

View File

@ -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};
}

View File

@ -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 />

View File

@ -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 <></>;
};

View File

@ -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:

View File

@ -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;
}