Fix glitch at aggregate operation update (#9253)

Before:

https://github.com/user-attachments/assets/6e76b19c-b99c-4870-9c93-b75e7cf86103

After:

https://github.com/user-attachments/assets/b5827d3e-5891-4204-bf91-6fa4504f30d3

Isolated the value change in a separate component to avoid re-renders of
the parent component that has the down chevron.
Also added a context at foot cell-level to centralize viewFieldId and
fieldMetadataId that were queried in children components calling heavy
hooks.
This commit is contained in:
Marie
2024-12-27 14:36:24 +01:00
committed by GitHub
parent 546a793aed
commit 2bb71bb79a
8 changed files with 186 additions and 124 deletions

View File

@ -2,6 +2,7 @@ import styled from '@emotion/styled';
import { MOBILE_VIEWPORT } from 'twenty-ui';
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 { 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';
@ -90,12 +91,18 @@ export const RecordTableAggregateFooter = ({
<StyledTh />
<StyledTh />
{visibleTableColumns.map((column, index) => (
<RecordTableAggregateFooterCell
<RecordTableColumnAggregateFooterCellContext.Provider
key={`${column.fieldMetadataId}${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`}
column={column}
currentRecordGroupId={currentRecordGroupId}
isFirstCell={index === 0}
/>
value={{
viewFieldId: column.viewFieldId || '',
fieldMetadataId: column.fieldMetadataId,
}}
>
<RecordTableAggregateFooterCell
currentRecordGroupId={currentRecordGroupId}
isFirstCell={index === 0}
/>
</RecordTableColumnAggregateFooterCellContext.Provider>
))}
</tr>
</StyledTableFoot>

View File

@ -1,10 +1,9 @@
import styled from '@emotion/styled';
import { useMemo } from 'react';
import { useContext, useMemo } from 'react';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext';
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';
import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
@ -12,20 +11,19 @@ const COLUMN_MIN_WIDTH = 104;
const StyledColumnFooterCell = styled.th<{
columnWidth: number;
isResizing?: boolean;
}>`
color: ${({ theme }) => theme.font.color.tertiary};
padding: 0;
text-align: left;
transition: 0.3s ease;
background-color: ${({ theme }) => theme.background.primary};
color: ${({ theme }) => theme.font.color.tertiary};
overflow: hidden;
padding: 0;
position: relative;
${({ columnWidth }) => `
min-width: ${columnWidth}px;
width: ${columnWidth}px;
`}
position: relative;
user-select: none;
text-align: left;
transition: 0.3s ease;
${({ theme }) => {
return `
&:hover {
@ -36,23 +34,14 @@ const StyledColumnFooterCell = styled.th<{
};
`;
}};
${({ isResizing, theme }) => {
if (isResizing === true) {
return `&:after {
background-color: ${theme.color.blue};
bottom: 0;
content: '';
display: block;
position: absolute;
right: -1px;
top: 0;
width: 2px;
}`;
}
}};
// TODO: refactor this, each component should own its CSS
user-select: none;
overflow: auto;
scrollbar-width: none;
-ms-overflow-style: none;
*::-webkit-scrollbar {
display: none;
}
`;
const StyledColumnFootContainer = styled.div`
@ -62,11 +51,9 @@ const StyledColumnFootContainer = styled.div`
`;
export const RecordTableAggregateFooterCell = ({
column,
isFirstCell = false,
currentRecordGroupId,
}: {
column: ColumnDefinition<FieldMetadata>;
isFirstCell?: boolean;
currentRecordGroupId?: string;
}) => {
@ -76,18 +63,19 @@ export const RecordTableAggregateFooterCell = ({
mapArrayToObject(tableColumns, ({ fieldMetadataId }) => fieldMetadataId),
[tableColumns],
);
const { fieldMetadataId } = useContext(
RecordTableColumnAggregateFooterCellContext,
);
return (
<StyledColumnFooterCell
key={column.fieldMetadataId}
columnWidth={Math.max(
tableColumnsByKey[column.fieldMetadataId].size + 24,
tableColumnsByKey[fieldMetadataId].size + 24,
COLUMN_MIN_WIDTH,
)}
>
<StyledColumnFootContainer>
<RecordTableColumnFooterWithDropdown
column={column}
currentRecordGroupId={currentRecordGroupId}
isFirstCell={isFirstCell}
/>

View File

@ -0,0 +1,11 @@
import { createContext } from 'react';
export type RecordTableColumnAggregateFooterCellValue = {
viewFieldId: string;
fieldMetadataId: string;
};
export const RecordTableColumnAggregateFooterCellContext =
createContext<RecordTableColumnAggregateFooterCellValue>(
{} as RecordTableColumnAggregateFooterCellValue,
);

View File

@ -1,23 +1,6 @@
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useTheme } from '@emotion/react';
import { useAggregateRecordsForRecordTableColumnFooter } from '@/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter';
import styled from '@emotion/styled';
import { useState } from 'react';
import { IconChevronDown, isDefined } from 'twenty-ui';
const StyledCell = styled.div`
align-items: center;
display: flex;
flex-direction: row;
flex-shrink: 0;
font-weight: ${({ theme }) => theme.font.weight.medium};
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ theme }) => theme.spacing(7)};
justify-content: space-between;
min-width: ${({ theme }) => theme.spacing(7)};
flex-grow: 1;
width: 100%;
`;
import { isDefined } from 'twenty-ui';
const StyledText = styled.span`
overflow: hidden;
@ -54,56 +37,34 @@ const StyledValue = styled.div`
flex: 1 0 0;
`;
const StyledIcon = styled(IconChevronDown)`
align-items: center;
display: flex;
height: 20px;
justify-content: center;
flex-grow: 0;
padding-right: ${({ theme }) => theme.spacing(2)};
`;
export const RecordTableColumnAggregateFooterValue = ({
dropdownId,
aggregateValue,
aggregateLabel,
isFirstCell,
fieldMetadataId,
}: {
dropdownId: string;
isFirstCell: boolean;
aggregateValue?: string | number | null;
aggregateLabel?: string;
fieldMetadataId: string;
}) => {
const [isHovered, setIsHovered] = useState(false);
const { isDropdownOpen } = useDropdown(dropdownId);
const sanitizedId = `tooltip-${dropdownId.replace(/[^a-zA-Z0-9-_]/g, '-')}`;
const theme = useTheme();
const shouldShowValue =
isHovered || isDropdownOpen || isDefined(aggregateValue) || isFirstCell;
const { aggregateValue, aggregateLabel, isLoading } =
useAggregateRecordsForRecordTableColumnFooter(fieldMetadataId);
return (
<div
onMouseEnter={() => {
setIsHovered(true);
}}
onMouseLeave={() => setIsHovered(false)}
>
<StyledCell>
{shouldShowValue ? (
<>
{isDefined(aggregateValue) ? (
<StyledValueContainer>
<StyledLabel>{aggregateLabel}</StyledLabel>
<StyledValue>{aggregateValue}</StyledValue>
</StyledValueContainer>
) : (
<StyledText id={sanitizedId}>Calculate</StyledText>
)}
<StyledIcon fontWeight={'light'} size={theme.icon.size.sm} />
</>
) : (
<></>
)}
</StyledCell>
</div>
<>
{isDefined(aggregateValue) || isLoading ? (
<StyledValueContainer>
{isLoading ? (
<></>
) : (
<>
<StyledLabel>{aggregateLabel}</StyledLabel>
<StyledValue>{aggregateValue}</StyledValue>
</>
)}
</StyledValueContainer>
) : (
<StyledText id={sanitizedId}>Calculate</StyledText>
)}
</>
);
};

View File

@ -0,0 +1,80 @@
import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext';
import { RecordTableColumnAggregateFooterValue } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValue';
import { hasAggregateOperationForViewFieldFamilySelector } from '@/object-record/record-table/record-table-footer/states/hasAggregateOperationForViewFieldFamilySelector';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useContext, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { IconChevronDown } from 'twenty-ui';
const StyledCell = styled.div`
align-items: center;
display: flex;
flex-direction: row;
flex-shrink: 0;
font-weight: ${({ theme }) => theme.font.weight.medium};
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ theme }) => theme.spacing(7)};
justify-content: space-between;
min-width: ${({ theme }) => theme.spacing(7)};
flex-grow: 1;
width: 100%;
`;
const StyledIcon = styled(IconChevronDown)`
align-items: center;
display: flex;
height: 20px;
justify-content: center;
flex-grow: 0;
padding-right: ${({ theme }) => theme.spacing(2)};
`;
export const RecordTableColumnAggregateFooterValueCell = ({
dropdownId,
isFirstCell,
}: {
dropdownId: string;
isFirstCell: boolean;
}) => {
const [isHovered, setIsHovered] = useState(false);
const { isDropdownOpen } = useDropdown(dropdownId);
const theme = useTheme();
const { viewFieldId, fieldMetadataId } = useContext(
RecordTableColumnAggregateFooterCellContext,
);
const hasAggregateOperationForViewField = useRecoilValue(
hasAggregateOperationForViewFieldFamilySelector({
viewFieldId,
}),
);
return (
<div
onMouseEnter={() => {
setIsHovered(true);
}}
onMouseLeave={() => setIsHovered(false)}
>
<StyledCell>
{isHovered ||
isDropdownOpen ||
hasAggregateOperationForViewField ||
isFirstCell ? (
<>
<RecordTableColumnAggregateFooterValue
fieldMetadataId={fieldMetadataId}
dropdownId={dropdownId}
/>
<StyledIcon fontWeight={'light'} size={theme.icon.size.sm} />
</>
) : (
<></>
)}
</StyledCell>
</div>
);
};

View File

@ -1,29 +1,29 @@
import { useCurrentContentId } from '@/dropdown/hooks/useCurrentContentId';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext';
import { RecordTableColumnAggregateFooterDropdownContent } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContent';
import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
import { RecordTableColumnAggregateFooterValue } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValue';
import { useAggregateRecordsForRecordTableColumnFooter } from '@/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter';
import { RecordTableColumnAggregateFooterValueCell } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValueCell';
import { RecordTableFooterAggregateContentId } from '@/object-record/record-table/record-table-footer/types/RecordTableFooterAggregateContentId';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useToggleScrollWrapper } from '@/ui/utilities/scroll/hooks/useToggleScrollWrapper';
import { useCallback } from 'react';
import { useCallback, useContext } from 'react';
type RecordTableColumnFooterWithDropdownProps = {
column: ColumnDefinition<FieldMetadata>;
isFirstCell: boolean;
currentRecordGroupId?: string;
};
export const RecordTableColumnFooterWithDropdown = ({
column,
currentRecordGroupId,
isFirstCell,
}: RecordTableColumnFooterWithDropdownProps) => {
const { currentContentId, handleContentChange, handleResetContent } =
useCurrentContentId<RecordTableFooterAggregateContentId>();
const { fieldMetadataId } = useContext(
RecordTableColumnAggregateFooterCellContext,
);
const { toggleScrollXWrapper, toggleScrollYWrapper } =
useToggleScrollWrapper();
@ -38,12 +38,9 @@ export const RecordTableColumnFooterWithDropdown = ({
toggleScrollYWrapper(true);
}, [handleResetContent, toggleScrollXWrapper, toggleScrollYWrapper]);
const { aggregateValue, aggregateLabel } =
useAggregateRecordsForRecordTableColumnFooter(column.fieldMetadataId);
const dropdownId = currentRecordGroupId
? `${column.fieldMetadataId}-footer-${currentRecordGroupId}`
: `${column.fieldMetadataId}-footer`;
? `${fieldMetadataId}-footer-${currentRecordGroupId}`
: `${fieldMetadataId}-footer`;
return (
<Dropdown
@ -51,9 +48,7 @@ export const RecordTableColumnFooterWithDropdown = ({
onClose={handleDropdownClose}
dropdownId={dropdownId}
clickableComponent={
<RecordTableColumnAggregateFooterValue
aggregateLabel={aggregateLabel}
aggregateValue={aggregateValue}
<RecordTableColumnAggregateFooterValueCell
dropdownId={dropdownId}
isFirstCell={isFirstCell}
/>
@ -65,7 +60,7 @@ export const RecordTableColumnFooterWithDropdown = ({
onContentChange: handleContentChange,
resetContent: handleResetContent,
dropdownId: dropdownId,
fieldMetadataId: column.fieldMetadataId,
fieldMetadataId: fieldMetadataId,
}}
>
<RecordTableColumnAggregateFooterDropdownContent />

View File

@ -6,9 +6,10 @@ import { useRecordGroupFilter } from '@/object-record/record-group/hooks/useReco
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext';
import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from '~/utils/isDefined';
@ -21,7 +22,6 @@ export const useAggregateRecordsForRecordTableColumnFooter = (
const { objectMetadataItem } = useRecordTableContextOrThrow();
const { recordGroupFilter } = useRecordGroupFilter(objectMetadataItem.fields);
const { currentViewWithSavedFiltersAndSorts } = useGetCurrentView();
const recordIndexViewFilterGroups = useRecoilValue(
recordIndexViewFilterGroupsState,
);
@ -37,13 +37,11 @@ export const useAggregateRecordsForRecordTableColumnFooter = (
recordIndexViewFilterGroups,
);
const viewFieldId =
currentViewWithSavedFiltersAndSorts?.viewFields?.find(
(viewField) => viewField.fieldMetadataId === fieldMetadataId,
)?.id ?? '';
const { viewFieldId } = useContext(
RecordTableColumnAggregateFooterCellContext,
);
const aggregateOperationForViewField = useRecoilValue(
viewFieldAggregateOperationState({ viewFieldId: viewFieldId }),
viewFieldAggregateOperationState({ viewFieldId }),
);
const fieldName = objectMetadataItem.fields.find(
@ -57,7 +55,7 @@ export const useAggregateRecordsForRecordTableColumnFooter = (
}
: {};
const { data } = useAggregateRecords({
const { data, loading } = useAggregateRecords({
objectNameSingular: objectMetadataItem.nameSingular,
recordGqlFieldsAggregate,
filter: { ...requestFilters, ...recordGroupFilter },
@ -75,5 +73,6 @@ export const useAggregateRecordsForRecordTableColumnFooter = (
return {
aggregateValue: value,
aggregateLabel: isDefined(value) ? label : undefined,
isLoading: loading,
};
};

View File

@ -0,0 +1,21 @@
import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
import { selectorFamily } from 'recoil';
import { isDefined } from '~/utils/isDefined';
export const hasAggregateOperationForViewFieldFamilySelector = selectorFamily<
boolean,
{ viewFieldId: string }
>({
key: 'hasAggregateOperationForViewField',
get:
({ viewFieldId }) =>
({ get }) => {
const aggregateOperation = get(
viewFieldAggregateOperationState({
viewFieldId,
}),
);
return isDefined(aggregateOperation);
},
});