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:
@ -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>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export type RecordTableColumnAggregateFooterCellValue = {
|
||||
viewFieldId: string;
|
||||
fieldMetadataId: string;
|
||||
};
|
||||
|
||||
export const RecordTableColumnAggregateFooterCellContext =
|
||||
createContext<RecordTableColumnAggregateFooterCellValue>(
|
||||
{} as RecordTableColumnAggregateFooterCellValue,
|
||||
);
|
||||
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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 />
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@ -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);
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user