Refacto scroll + Aggregate queries for view groups (#9089)

Closes https://github.com/twentyhq/private-issues/issues/217.

Refactoring scroll not to cause table-wide re-render when opening a
dropdown (triggering a scroll lock) in the table.
This commit is contained in:
Marie
2024-12-16 17:58:57 +01:00
committed by GitHub
parent c90d2fd5cc
commit 311b5f64c4
47 changed files with 374 additions and 277 deletions

View File

@ -1,9 +1,10 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useLocation, useNavigation } from 'react-router-dom'; import { useNavigation } from 'react-router-dom';
import { useRecoilState, useRecoilValue } from 'recoil';
import { overlayScrollbarsState } from '@/ui/utilities/scroll/states/overlayScrollbarsState'; import { scrollWrapperInstanceComponentState } from '@/ui/utilities/scroll/states/scrollWrapperInstanceComponentState';
import { scrollPositionState } from '@/ui/utilities/scroll/states/scrollPositionState'; import { scrollWrapperScrollTopComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollTopComponentState';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
/** /**
@ -13,23 +14,24 @@ import { isDefined } from '~/utils/isDefined';
* not share the same scroll position. * not share the same scroll position.
*/ */
export const useScrollRestoration = (viewportHeight?: number) => { export const useScrollRestoration = (viewportHeight?: number) => {
const key = `scroll-position-${useLocation().key}`;
const { state } = useNavigation(); const { state } = useNavigation();
const [scrollPosition, setScrollPosition] = useRecoilState( const [scrollTop, setScrollTop] = useRecoilComponentStateV2(
scrollPositionState(key), scrollWrapperScrollTopComponentState,
); );
const overlayScrollbars = useRecoilValue(overlayScrollbarsState); const overlayScrollbars = useRecoilComponentValueV2(
scrollWrapperInstanceComponentState,
);
const scrollWrapper = overlayScrollbars?.elements().viewport; const scrollWrapper = overlayScrollbars?.elements().viewport;
const skip = isDefined(viewportHeight) && scrollPosition > viewportHeight; const skip = isDefined(viewportHeight) && scrollTop > viewportHeight;
useEffect(() => { useEffect(() => {
if (state === 'loading') { if (state === 'loading') {
setScrollPosition(scrollWrapper?.scrollTop ?? 0); setScrollTop(scrollWrapper?.scrollTop ?? 0);
} else if (state === 'idle' && isDefined(scrollWrapper) && !skip) { } else if (state === 'idle' && isDefined(scrollWrapper) && !skip) {
scrollWrapper.scrollTo({ top: scrollPosition }); scrollWrapper.scrollTo({ top: scrollTop });
} }
}, [key, state, scrollWrapper, skip, scrollPosition, setScrollPosition]); }, [state, scrollWrapper, skip, scrollTop, setScrollTop]);
}; };

View File

@ -1,19 +1,24 @@
import { overlayScrollbarsState } from '@/ui/utilities/scroll/states/overlayScrollbarsState'; import { scrollWrapperInstanceComponentState } from '@/ui/utilities/scroll/states/scrollWrapperInstanceComponentState';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
export const useScrollToPosition = () => { export const useScrollToPosition = () => {
const scrollWrapperInstanceState = useRecoilComponentCallbackStateV2(
scrollWrapperInstanceComponentState,
);
const scrollToPosition = useRecoilCallback( const scrollToPosition = useRecoilCallback(
({ snapshot }) => ({ snapshot }) =>
(scrollPositionInPx: number) => { (scrollPositionInPx: number) => {
const overlayScrollbars = snapshot const overlayScrollbars = snapshot
.getLoadable(overlayScrollbarsState) .getLoadable(scrollWrapperInstanceState)
.getValue(); .getValue();
const scrollWrapper = overlayScrollbars?.elements().viewport; const scrollWrapper = overlayScrollbars?.elements().viewport;
scrollWrapper?.scrollTo({ top: scrollPositionInPx }); scrollWrapper?.scrollTo({ top: scrollPositionInPx });
}, },
[], [scrollWrapperInstanceState],
); );
return { scrollToPosition }; return { scrollToPosition };

View File

@ -44,7 +44,10 @@ export const EventList = ({ events, targetableObject }: EventListProps) => {
const groupedEvents = groupEventsByMonth(filteredEvents); const groupedEvents = groupEventsByMonth(filteredEvents);
return ( return (
<ScrollWrapper contextProviderName="eventList"> <ScrollWrapper
contextProviderName="eventList"
componentInstanceId={`scroll-wrapper-event-list-${targetableObject.id}`}
>
<StyledTimelineContainer> <StyledTimelineContainer>
{groupedEvents.map((group, index) => ( {groupedEvents.map((group, index) => (
<EventsGroup <EventsGroup

View File

@ -610,7 +610,10 @@ export const CommandMenu = () => {
setCommandMenuSearch={setCommandMenuSearch} setCommandMenuSearch={setCommandMenuSearch}
/> />
<StyledList> <StyledList>
<ScrollWrapper contextProviderName="commandMenu"> <ScrollWrapper
contextProviderName="commandMenu"
componentInstanceId={`scroll-wrapper-command-menu`}
>
<StyledInnerList isMobile={isMobile}> <StyledInnerList isMobile={isMobile}>
<SelectableList <SelectableList
selectableListId="command-menu-list" selectableListId="command-menu-list"

View File

@ -58,7 +58,8 @@ export const MainNavigationDrawerItems = () => {
)} )}
<ScrollWrapper <ScrollWrapper
contextProviderName="navigationDrawer" contextProviderName="navigationDrawer"
enableXScroll={false} componentInstanceId={`scroll-wrapper-navigation-drawer`}
defaultEnableXScroll={false}
scrollHide={true} scrollHide={true}
> >
<NavigationDrawerOpenedSection /> <NavigationDrawerOpenedSection />

View File

@ -194,7 +194,10 @@ export const RecordBoard = () => {
<RecordBoardComponentInstanceContext.Provider <RecordBoardComponentInstanceContext.Provider
value={{ instanceId: recordBoardId }} value={{ instanceId: recordBoardId }}
> >
<ScrollWrapper contextProviderName="recordBoard"> <ScrollWrapper
contextProviderName="recordBoard"
componentInstanceId={`scroll-wrapper-record-board-${recordBoardId}`}
>
<RecordBoardStickyHeaderEffect /> <RecordBoardStickyHeaderEffect />
<StyledContainerContainer> <StyledContainerContainer>
<RecordBoardHeader /> <RecordBoardHeader />

View File

@ -1,9 +1,11 @@
import { scrollWrapperScrollTopComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollTopComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useScrollTopValue } from '@/ui/utilities/scroll/hooks/useScrollTopValue';
export const RecordBoardStickyHeaderEffect = () => { export const RecordBoardStickyHeaderEffect = () => {
const scrollTop = useScrollTopValue('recordBoard'); const scrollTop = useRecoilComponentValueV2(
scrollWrapperScrollTopComponentState,
);
// TODO: move this outside because it might cause way too many re-renders for other hooks // TODO: move this outside because it might cause way too many re-renders for other hooks
useEffect(() => { useEffect(() => {

View File

@ -0,0 +1,37 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { useCurrentRecordGroupDefinition } from '@/object-record/record-group/hooks/useCurrentRecordGroupDefinition';
import { useMemo } from 'react';
import { isDefined } from 'twenty-ui';
export const useRecordGroupFilter = (fields: FieldMetadataItem[]) => {
const currentRecordGroupDefinition = useCurrentRecordGroupDefinition();
const recordGroupFilter = useMemo(() => {
if (isDefined(currentRecordGroupDefinition)) {
const fieldMetadataItem = fields.find(
(fieldMetadataItem) =>
fieldMetadataItem.id === currentRecordGroupDefinition.fieldMetadataId,
);
if (!fieldMetadataItem) {
throw new Error(
`Field metadata item with id ${currentRecordGroupDefinition.fieldMetadataId} not found`,
);
}
if (!isDefined(currentRecordGroupDefinition.value)) {
return { [fieldMetadataItem.name]: { is: 'NULL' } };
}
return {
[fieldMetadataItem.name]: {
eq: currentRecordGroupDefinition.value,
},
};
}
return {};
}, [currentRecordGroupDefinition, fields]);
return { recordGroupFilter };
};

View File

@ -2,12 +2,11 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter';
import { useCurrentRecordGroupDefinition } from '@/object-record/record-group/hooks/useCurrentRecordGroupDefinition'; import { useCurrentRecordGroupDefinition } from '@/object-record/record-group/hooks/useCurrentRecordGroupDefinition';
import { useRecordGroupFilter } from '@/object-record/record-group/hooks/useRecordGroupFilter';
import { tableFiltersComponentState } from '@/object-record/record-table/states/tableFiltersComponentState'; import { tableFiltersComponentState } from '@/object-record/record-table/states/tableFiltersComponentState';
import { tableSortsComponentState } from '@/object-record/record-table/states/tableSortsComponentState'; import { tableSortsComponentState } from '@/object-record/record-table/states/tableSortsComponentState';
import { tableViewFilterGroupsComponentState } from '@/object-record/record-table/states/tableViewFilterGroupsComponentState'; import { tableViewFilterGroupsComponentState } from '@/object-record/record-table/states/tableViewFilterGroupsComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useMemo } from 'react';
import { isDefined } from 'twenty-ui';
export const useFindManyRecordIndexTableParams = ( export const useFindManyRecordIndexTableParams = (
objectNameSingular: string, objectNameSingular: string,
@ -17,6 +16,10 @@ export const useFindManyRecordIndexTableParams = (
objectNameSingular, objectNameSingular,
}); });
const { recordGroupFilter } = useRecordGroupFilter(
objectMetadataItem?.fields,
);
const currentRecordGroupDefinition = useCurrentRecordGroupDefinition(); const currentRecordGroupDefinition = useCurrentRecordGroupDefinition();
const tableViewFilterGroups = useRecoilComponentValueV2( const tableViewFilterGroups = useRecoilComponentValueV2(
@ -38,33 +41,6 @@ export const useFindManyRecordIndexTableParams = (
tableViewFilterGroups, tableViewFilterGroups,
); );
const recordGroupFilter = useMemo(() => {
if (isDefined(currentRecordGroupDefinition)) {
const fieldMetadataItem = objectMetadataItem?.fields.find(
(fieldMetadataItem) =>
fieldMetadataItem.id === currentRecordGroupDefinition.fieldMetadataId,
);
if (!fieldMetadataItem) {
throw new Error(
`Field metadata item with id ${currentRecordGroupDefinition.fieldMetadataId} not found`,
);
}
if (!isDefined(currentRecordGroupDefinition.value)) {
return { [fieldMetadataItem.name]: { is: 'NULL' } };
}
return {
[fieldMetadataItem.name]: {
eq: currentRecordGroupDefinition.value,
},
};
}
return {};
}, [objectMetadataItem.fields, currentRecordGroupDefinition]);
const orderBy = turnSortsIntoOrderBy(objectMetadataItem, tableSorts); const orderBy = turnSortsIntoOrderBy(objectMetadataItem, tableSorts);
return { return {

View File

@ -96,7 +96,9 @@ export const RecordTable = () => {
<RecordTableRecordGroupsBody /> <RecordTableRecordGroupsBody />
)} )}
<RecordTableStickyEffect /> <RecordTableStickyEffect />
{isAggregateQueryEnabled && <RecordTableFooter />} {isAggregateQueryEnabled && !hasRecordGroups && (
<RecordTableFooter />
)}
</StyledTable> </StyledTable>
<DragSelect <DragSelect
dragSelectable={tableBodyRef} dragSelectable={tableBodyRef}

View File

@ -1,6 +1,7 @@
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId'; import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { RecordTableFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableFooter';
import { RecordTablePendingRecordGroupRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRecordGroupRow'; import { RecordTablePendingRecordGroupRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRecordGroupRow';
import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow'; import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow';
import { RecordTableRecordGroupSectionAddNew } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew'; import { RecordTableRecordGroupSectionAddNew } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew';
@ -8,10 +9,14 @@ import { RecordTableRecordGroupSectionLoadMore } from '@/object-record/record-ta
import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState'; import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
export const RecordTableRecordGroupRows = () => { export const RecordTableRecordGroupRows = () => {
const isAggregateQueryEnabled = useIsFeatureEnabled(
'IS_AGGREGATE_QUERY_ENABLED',
);
const currentRecordGroupId = useCurrentRecordGroupId(); const currentRecordGroupId = useCurrentRecordGroupId();
const allRecordIds = useRecoilComponentValueV2( const allRecordIds = useRecoilComponentValueV2(
@ -57,8 +62,14 @@ export const RecordTableRecordGroupRows = () => {
); );
})} })}
<RecordTablePendingRecordGroupRow /> <RecordTablePendingRecordGroupRow />
<RecordTableRecordGroupSectionLoadMore />
<RecordTableRecordGroupSectionAddNew /> <RecordTableRecordGroupSectionAddNew />
{isAggregateQueryEnabled && (
<RecordTableFooter
key={currentRecordGroupId}
currentRecordGroupId={currentRecordGroupId}
/>
)}
<RecordTableRecordGroupSectionLoadMore />
</> </>
); );
}; };

View File

@ -1,12 +1,15 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState'; import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState';
import { useScrollLeftValue } from '@/ui/utilities/scroll/hooks/useScrollLeftValue'; import { scrollWrapperScrollLeftComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollLeftComponentState';
import { useScrollTopValue } from '@/ui/utilities/scroll/hooks/useScrollTopValue'; import { scrollWrapperScrollTopComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollTopComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
export const RecordTableStickyEffect = () => { export const RecordTableStickyEffect = () => {
const scrollTop = useScrollTopValue('recordTableWithWrappers'); const scrollTop = useRecoilComponentValueV2(
scrollWrapperScrollTopComponentState,
);
useEffect(() => { useEffect(() => {
if (scrollTop > 0) { if (scrollTop > 0) {
@ -20,7 +23,9 @@ export const RecordTableStickyEffect = () => {
} }
}, [scrollTop]); }, [scrollTop]);
const scrollLeft = useScrollLeftValue('recordTableWithWrappers'); const scrollLeft = useRecoilComponentValueV2(
scrollWrapperScrollLeftComponentState,
);
const setIsRecordTableScrolledLeft = useSetRecoilComponentStateV2( const setIsRecordTableScrolledLeft = useSetRecoilComponentStateV2(
isRecordTableScrolledLeftComponentState, isRecordTableScrolledLeftComponentState,

View File

@ -10,8 +10,6 @@ import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields'; import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields';
import { mapColumnDefinitionsToViewFields } from '@/views/utils/mapColumnDefinitionToViewField'; import { mapColumnDefinitionsToViewFields } from '@/views/utils/mapColumnDefinitionToViewField';
import { isScrollEnabledForRecordTableState } from '@/object-record/record-table/states/isScrollEnabledForRecordTableState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { RecordUpdateContext } from '../contexts/EntityUpdateMutationHookContext'; import { RecordUpdateContext } from '../contexts/EntityUpdateMutationHookContext';
import { useRecordTable } from '../hooks/useRecordTable'; import { useRecordTable } from '../hooks/useRecordTable';
@ -50,11 +48,6 @@ export const RecordTableWithWrappers = ({
recordTableId, recordTableId,
viewBarId, viewBarId,
}: RecordTableWithWrappersProps) => { }: RecordTableWithWrappersProps) => {
const isScrollEnabledForRecordTable = useRecoilComponentValueV2(
isScrollEnabledForRecordTableState,
recordTableId,
);
const { resetTableRowSelection, selectAllRows, setHasUserSelectedAllRows } = const { resetTableRowSelection, selectAllRows, setHasUserSelectedAllRows } =
useRecordTable({ useRecordTable({
recordTableId, recordTableId,
@ -109,9 +102,8 @@ export const RecordTableWithWrappers = ({
> >
<EntityDeleteContext.Provider value={deleteOneRecord}> <EntityDeleteContext.Provider value={deleteOneRecord}>
<ScrollWrapper <ScrollWrapper
enableXScroll={isScrollEnabledForRecordTable.enableXScroll}
enableYScroll={isScrollEnabledForRecordTable.enableYScroll}
contextProviderName="recordTableWithWrappers" contextProviderName="recordTableWithWrappers"
componentInstanceId={`record-table-scroll-${recordTableId}`}
> >
<RecordUpdateContext.Provider value={updateRecordMutation}> <RecordUpdateContext.Provider value={updateRecordMutation}>
<StyledTableWithHeader> <StyledTableWithHeader>

View File

@ -53,6 +53,11 @@ export const useRecordTable = (props?: useRecordTableProps) => {
recordTableId, recordTableId,
); );
const tableColumnsState = useRecoilComponentCallbackStateV2(
tableColumnsComponentState,
recordTableId,
);
const setAvailableTableColumns = useRecoilCallback( const setAvailableTableColumns = useRecoilCallback(
({ snapshot, set }) => ({ snapshot, set }) =>
(columns: ColumnDefinition<FieldMetadata>[]) => { (columns: ColumnDefinition<FieldMetadata>[]) => {
@ -69,6 +74,19 @@ export const useRecordTable = (props?: useRecordTableProps) => {
[availableTableColumnsState], [availableTableColumnsState],
); );
const setTableColumns = useRecoilCallback(
({ snapshot, set }) =>
(columns: ColumnDefinition<FieldMetadata>[]) => {
const tableColumns = getSnapshotValue(snapshot, tableColumnsState);
if (isDeeplyEqual(tableColumns, columns)) {
return;
}
set(tableColumnsState, columns);
},
[tableColumnsState],
);
const setOnEntityCountChange = useSetRecoilComponentStateV2( const setOnEntityCountChange = useSetRecoilComponentStateV2(
onEntityCountChangeComponentState, onEntityCountChangeComponentState,
recordTableId, recordTableId,
@ -89,11 +107,6 @@ export const useRecordTable = (props?: useRecordTableProps) => {
recordTableId, recordTableId,
); );
const setTableColumns = useSetRecoilComponentStateV2(
tableColumnsComponentState,
recordTableId,
);
const setOnColumnsChange = useSetRecoilComponentStateV2( const setOnColumnsChange = useSetRecoilComponentStateV2(
onColumnsChangeComponentState, onColumnsChangeComponentState,
recordTableId, recordTableId,

View File

@ -6,13 +6,15 @@ import { RecordTableTd } from '@/object-record/record-table/record-table-cell/co
import { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected'; import { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected';
import { Checkbox } from 'twenty-ui'; import { Checkbox } from 'twenty-ui';
export const TABLE_CELL_CHECKBOX_MIN_WIDTH = '24px';
const StyledContainer = styled.div` const StyledContainer = styled.div`
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
height: 32px; height: 32px;
justify-content: center; justify-content: center;
min-width: 24px; min-width: ${TABLE_CELL_CHECKBOX_MIN_WIDTH};
`; `;
export const RecordTableCellCheckbox = () => { export const RecordTableCellCheckbox = () => {

View File

@ -6,12 +6,14 @@ import { RecordTableTd } from '@/object-record/record-table/record-table-cell/co
import { css } from '@emotion/react'; import { css } from '@emotion/react';
import { IconListViewGrip } from 'twenty-ui'; import { IconListViewGrip } from 'twenty-ui';
export const TABLE_CELL_GRIP_WIDTH = '16px';
const StyledContainer = styled.div<{ isPendingRow?: boolean }>` const StyledContainer = styled.div<{ isPendingRow?: boolean }>`
height: 32px;
width: ${TABLE_CELL_GRIP_WIDTH};
border-color: transparent; border-color: transparent;
cursor: grab; cursor: grab;
display: flex; display: flex;
height: 32px;
width: 16px;
${({ isPendingRow }) => ${({ isPendingRow }) =>
!isPendingRow !isPendingRow
? css` ? css`

View File

@ -50,8 +50,10 @@ export const RecordTableColumnFooterAggregateValue = ({
dropdownId, dropdownId,
aggregateValue, aggregateValue,
aggregateLabel, aggregateLabel,
isFirstCell,
}: { }: {
dropdownId: string; dropdownId: string;
isFirstCell: boolean;
aggregateValue?: string | number | null; aggregateValue?: string | number | null;
aggregateLabel?: string; aggregateLabel?: string;
}) => { }) => {
@ -66,13 +68,13 @@ export const RecordTableColumnFooterAggregateValue = ({
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
> >
<StyledCell> <StyledCell>
{isHovered || isDefined(aggregateValue) ? ( {isHovered || isDefined(aggregateValue) || isFirstCell ? (
<> <>
<StyledText id={sanitizedId}> <StyledText id={sanitizedId}>
{aggregateValue ?? 'Calculate'} {aggregateValue ?? 'Calculate'}
</StyledText> </StyledText>
<StyledIcon fontWeight={'light'} size={theme.icon.size.sm} /> <StyledIcon fontWeight={'light'} size={theme.icon.size.sm} />
{aggregateValue && isDefined(aggregateLabel) && ( {isDefined(aggregateValue) && isDefined(aggregateLabel) && (
<AppTooltip <AppTooltip
anchorSelect={`#${sanitizedId}`} anchorSelect={`#${sanitizedId}`}
content={aggregateLabel} content={aggregateLabel}

View File

@ -16,10 +16,12 @@ import { MenuItem } from 'twenty-ui';
export const RecordTableColumnFooterDropdown = ({ export const RecordTableColumnFooterDropdown = ({
column, column,
dropdownId,
}: { }: {
column: ColumnDefinition<FieldMetadata>; column: ColumnDefinition<FieldMetadata>;
dropdownId: string;
}) => { }) => {
const { closeDropdown } = useDropdown(column.fieldMetadataId + '-footer'); const { closeDropdown } = useDropdown(dropdownId);
const { objectMetadataItem } = useRecordTableContextOrThrow(); const { objectMetadataItem } = useRecordTableContextOrThrow();
const { currentViewWithSavedFiltersAndSorts } = useGetCurrentView(); const { currentViewWithSavedFiltersAndSorts } = useGetCurrentView();
@ -67,6 +69,7 @@ export const RecordTableColumnFooterDropdown = ({
key={aggregation} key={aggregation}
onClick={() => { onClick={() => {
handleAggregationChange(aggregation); handleAggregationChange(aggregation);
closeDropdown();
}} }}
text={getAggregateOperationLabel(aggregation)} text={getAggregateOperationLabel(aggregation)}
/> />

View File

@ -2,53 +2,44 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'
import { RecordTableColumnFooterAggregateValue } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnFooterAggregateValue'; 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 { RecordTableColumnFooterDropdown } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnFooterDropdown';
import { useAggregateRecordsForRecordTableColumnFooter } from '@/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter'; import { useAggregateRecordsForRecordTableColumnFooter } from '@/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter';
import { isScrollEnabledForRecordTableState } from '@/object-record/record-table/states/isScrollEnabledForRecordTableState';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useToggleScrollWrapper } from '@/ui/utilities/scroll/hooks/useToggleScrollWrapper';
import styled from '@emotion/styled';
import { useCallback } from 'react'; import { useCallback } from 'react';
type RecordTableColumnFooterWithDropdownProps = { type RecordTableColumnFooterWithDropdownProps = {
column: ColumnDefinition<FieldMetadata>; column: ColumnDefinition<FieldMetadata>;
isFirstCell: boolean;
currentRecordGroupId?: string;
}; };
const StyledDropdown = styled(Dropdown)`
display: flex;
flex: 1;
z-index: ${({ theme }) => theme.lastLayerZIndex};
transition: opacity 150ms ease-in-out;
`;
export const RecordTableColumnFooterWithDropdown = ({ export const RecordTableColumnFooterWithDropdown = ({
column, column,
currentRecordGroupId,
isFirstCell,
}: RecordTableColumnFooterWithDropdownProps) => { }: RecordTableColumnFooterWithDropdownProps) => {
const setIsScrollEnabledForRecordTable = useSetRecoilComponentStateV2( const { toggleScrollXWrapper, toggleScrollYWrapper } =
isScrollEnabledForRecordTableState, useToggleScrollWrapper();
);
const handleDropdownOpen = useCallback(() => { const handleDropdownOpen = useCallback(() => {
setIsScrollEnabledForRecordTable({ toggleScrollXWrapper(false);
enableXScroll: false, toggleScrollYWrapper(false);
enableYScroll: false, }, [toggleScrollXWrapper, toggleScrollYWrapper]);
});
}, [setIsScrollEnabledForRecordTable]);
const handleDropdownClose = useCallback(() => { const handleDropdownClose = useCallback(() => {
setIsScrollEnabledForRecordTable({ toggleScrollXWrapper(true);
enableXScroll: true, toggleScrollYWrapper(true);
enableYScroll: true, }, [toggleScrollXWrapper, toggleScrollYWrapper]);
});
}, [setIsScrollEnabledForRecordTable]);
const { aggregateValue, aggregateLabel } = const { aggregateValue, aggregateLabel } =
useAggregateRecordsForRecordTableColumnFooter(column.fieldMetadataId); useAggregateRecordsForRecordTableColumnFooter(column.fieldMetadataId);
const dropdownId = column.fieldMetadataId + '-footer'; const dropdownId = currentRecordGroupId
? `${column.fieldMetadataId}-footer-${currentRecordGroupId}`
: `${column.fieldMetadataId}-footer`;
return ( return (
<StyledDropdown <Dropdown
onOpen={handleDropdownOpen} onOpen={handleDropdownOpen}
onClose={handleDropdownClose} onClose={handleDropdownClose}
dropdownId={dropdownId} dropdownId={dropdownId}
@ -57,9 +48,15 @@ export const RecordTableColumnFooterWithDropdown = ({
aggregateLabel={aggregateLabel} aggregateLabel={aggregateLabel}
aggregateValue={aggregateValue} aggregateValue={aggregateValue}
dropdownId={dropdownId} dropdownId={dropdownId}
isFirstCell={isFirstCell}
/>
}
dropdownComponents={
<RecordTableColumnFooterDropdown
column={column}
dropdownId={dropdownId}
/> />
} }
dropdownComponents={<RecordTableColumnFooterDropdown column={column} />}
dropdownOffset={{ x: -1 }} dropdownOffset={{ x: -1 }}
dropdownPlacement="bottom-start" dropdownPlacement="bottom-start"
dropdownHotkeyScope={{ scope: dropdownId }} dropdownHotkeyScope={{ scope: dropdownId }}

View File

@ -1,6 +1,8 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { MOBILE_VIEWPORT } from 'twenty-ui'; 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 { RecordTableFooterCell } from '@/object-record/record-table/record-table-footer/components/RecordTableFooterCell';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -75,21 +77,33 @@ const StyledTableFoot = styled.thead`
`; `;
const StyledDiv = styled.div` const StyledDiv = styled.div`
width: 30px; width: calc(${TABLE_CELL_GRIP_WIDTH} + ${TABLE_CELL_CHECKBOX_MIN_WIDTH});
`; `;
export const RecordTableFooter = () => { export const RecordTableFooter = ({
currentRecordGroupId,
}: {
currentRecordGroupId?: string;
}) => {
const visibleTableColumns = useRecoilComponentValueV2( const visibleTableColumns = useRecoilComponentValueV2(
visibleTableColumnsComponentSelector, visibleTableColumnsComponentSelector,
); );
return ( return (
<StyledTableFoot id="record-table-footer" data-select-disable> <StyledTableFoot
id={`record-table-footer${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`}
data-select-disable
>
<tr> <tr>
<th /> <th />
<StyledDiv /> <StyledDiv />
{visibleTableColumns.map((column) => ( {visibleTableColumns.map((column, index) => (
<RecordTableFooterCell key={column.fieldMetadataId} column={column} /> <RecordTableFooterCell
key={`${column.fieldMetadataId}${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`}
column={column}
currentRecordGroupId={currentRecordGroupId}
isFirstCell={index === 0}
/>
))} ))}
</tr> </tr>
</StyledTableFoot> </StyledTableFoot>

View File

@ -63,8 +63,12 @@ const StyledColumnFootContainer = styled.div`
export const RecordTableFooterCell = ({ export const RecordTableFooterCell = ({
column, column,
isFirstCell = false,
currentRecordGroupId,
}: { }: {
column: ColumnDefinition<FieldMetadata>; column: ColumnDefinition<FieldMetadata>;
isFirstCell?: boolean;
currentRecordGroupId?: string;
}) => { }) => {
const tableColumns = useRecoilComponentValueV2(tableColumnsComponentState); const tableColumns = useRecoilComponentValueV2(tableColumnsComponentState);
const tableColumnsByKey = useMemo( const tableColumnsByKey = useMemo(
@ -82,7 +86,11 @@ export const RecordTableFooterCell = ({
)} )}
> >
<StyledColumnFootContainer> <StyledColumnFootContainer>
<RecordTableColumnFooterWithDropdown column={column} /> <RecordTableColumnFooterWithDropdown
column={column}
currentRecordGroupId={currentRecordGroupId}
isFirstCell={isFirstCell}
/>
</StyledColumnFootContainer> </StyledColumnFootContainer>
</StyledColumnFooterCell> </StyledColumnFooterCell>
); );

View File

@ -1,6 +1,7 @@
import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords'; import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords';
import { computeAggregateValueAndLabel } from '@/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel'; import { computeAggregateValueAndLabel } from '@/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel';
import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter';
import { useRecordGroupFilter } from '@/object-record/record-group/hooks/useRecordGroupFilter';
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState'; import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
@ -16,8 +17,9 @@ export const useAggregateRecordsForRecordTableColumnFooter = (
const isAggregateQueryEnabled = useIsFeatureEnabled( const isAggregateQueryEnabled = useIsFeatureEnabled(
'IS_AGGREGATE_QUERY_ENABLED', 'IS_AGGREGATE_QUERY_ENABLED',
); );
const { objectMetadataItem } = useRecordTableContextOrThrow(); const { objectMetadataItem } = useRecordTableContextOrThrow();
const { recordGroupFilter } = useRecordGroupFilter(objectMetadataItem.fields);
const { currentViewWithSavedFiltersAndSorts } = useGetCurrentView(); const { currentViewWithSavedFiltersAndSorts } = useGetCurrentView();
const recordIndexViewFilterGroups = useRecoilValue( const recordIndexViewFilterGroups = useRecoilValue(
recordIndexViewFilterGroupsState, recordIndexViewFilterGroupsState,
@ -56,7 +58,7 @@ export const useAggregateRecordsForRecordTableColumnFooter = (
const { data } = useAggregateRecords({ const { data } = useAggregateRecords({
objectNameSingular: objectMetadataItem.nameSingular, objectNameSingular: objectMetadataItem.nameSingular,
recordGqlFieldsAggregate, recordGqlFieldsAggregate,
filter: { ...requestFilters }, filter: { ...requestFilters, ...recordGroupFilter },
skip: skip:
!isAggregateQueryEnabled || !isDefined(aggregateOperationForViewField), !isAggregateQueryEnabled || !isDefined(aggregateOperationForViewField),
}); });

View File

@ -1,8 +1,7 @@
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { isScrollEnabledForRecordTableState } from '@/object-record/record-table/states/isScrollEnabledForRecordTableState';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useToggleScrollWrapper } from '@/ui/utilities/scroll/hooks/useToggleScrollWrapper';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { RecordTableColumnHead } from './RecordTableColumnHead'; import { RecordTableColumnHead } from './RecordTableColumnHead';
@ -21,23 +20,18 @@ const StyledDropdown = styled(Dropdown)`
export const RecordTableColumnHeadWithDropdown = ({ export const RecordTableColumnHeadWithDropdown = ({
column, column,
}: RecordTableColumnHeadWithDropdownProps) => { }: RecordTableColumnHeadWithDropdownProps) => {
const setIsScrollEnabledForRecordTable = useSetRecoilComponentStateV2( const { toggleScrollXWrapper, toggleScrollYWrapper } =
isScrollEnabledForRecordTableState, useToggleScrollWrapper();
);
const handleDropdownOpen = useCallback(() => { const handleDropdownOpen = useCallback(() => {
setIsScrollEnabledForRecordTable({ toggleScrollXWrapper(false);
enableXScroll: false, toggleScrollYWrapper(false);
enableYScroll: false, }, [toggleScrollXWrapper, toggleScrollYWrapper]);
});
}, [setIsScrollEnabledForRecordTable]);
const handleDropdownClose = useCallback(() => { const handleDropdownClose = useCallback(() => {
setIsScrollEnabledForRecordTable({ toggleScrollXWrapper(true);
enableXScroll: true, toggleScrollYWrapper(true);
enableYScroll: true, }, [toggleScrollXWrapper, toggleScrollYWrapper]);
});
}, [setIsScrollEnabledForRecordTable]);
return ( return (
<StyledDropdown <StyledDropdown

View File

@ -1,17 +0,0 @@
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export type ScrollEnabled = {
enableXScroll: boolean;
enableYScroll: boolean;
};
export const isScrollEnabledForRecordTableState =
createComponentStateV2<ScrollEnabled>({
key: 'isScrollEnabledForRecordTableState',
defaultValue: {
enableXScroll: true,
enableYScroll: true,
},
componentInstanceContext: RecordTableComponentInstanceContext,
});

View File

@ -1,6 +1,5 @@
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
export type ColumnDefinition<T extends FieldMetadata> = FieldDefinition<T> & { export type ColumnDefinition<T extends FieldMetadata> = FieldDefinition<T> & {
size: number; size: number;
@ -10,5 +9,4 @@ export type ColumnDefinition<T extends FieldMetadata> = FieldDefinition<T> & {
viewFieldId?: string; viewFieldId?: string;
isFilterable?: boolean; isFilterable?: boolean;
isSortable?: boolean; isSortable?: boolean;
aggregateOperation?: AGGREGATE_OPERATIONS | null;
}; };

View File

@ -30,7 +30,10 @@ export const SettingsPageContainer = ({
}: { }: {
children: ReactNode; children: ReactNode;
}) => ( }) => (
<ScrollWrapper contextProviderName="settingsPageContainer"> <ScrollWrapper
contextProviderName="settingsPageContainer"
componentInstanceId={'scroll-wrapper-settings-page-container'}
>
<StyledSettingsPageContainer>{children}</StyledSettingsPageContainer> <StyledSettingsPageContainer>{children}</StyledSettingsPageContainer>
</ScrollWrapper> </ScrollWrapper>
); );

View File

@ -1,5 +1,6 @@
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useId } from 'react';
const StyledDropdownMenuItemsExternalContainer = styled.div<{ const StyledDropdownMenuItemsExternalContainer = styled.div<{
hasMaxHeight?: boolean; hasMaxHeight?: boolean;
@ -44,13 +45,18 @@ export const DropdownMenuItemsContainer = ({
className?: string; className?: string;
withoutScrollWrapper?: boolean; withoutScrollWrapper?: boolean;
}) => { }) => {
const id = useId();
return withoutScrollWrapper === true ? ( return withoutScrollWrapper === true ? (
<StyledDropdownMenuItemsExternalContainer <StyledDropdownMenuItemsExternalContainer
hasMaxHeight={hasMaxHeight} hasMaxHeight={hasMaxHeight}
className={className} className={className}
> >
{hasMaxHeight ? ( {hasMaxHeight ? (
<StyledScrollWrapper contextProviderName="dropdownMenuItemsContainer"> <StyledScrollWrapper
contextProviderName="dropdownMenuItemsContainer"
componentInstanceId={`scroll-wrapper-dropdown-menu-${id}`}
>
<StyledDropdownMenuItemsInternalContainer> <StyledDropdownMenuItemsInternalContainer>
{children} {children}
</StyledDropdownMenuItemsInternalContainer> </StyledDropdownMenuItemsInternalContainer>
@ -62,7 +68,10 @@ export const DropdownMenuItemsContainer = ({
)} )}
</StyledDropdownMenuItemsExternalContainer> </StyledDropdownMenuItemsExternalContainer>
) : ( ) : (
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer"> <ScrollWrapper
contextProviderName="dropdownMenuItemsContainer"
componentInstanceId={`scroll-wrapper-dropdown-menu-${id}`}
>
<StyledDropdownMenuItemsExternalContainer <StyledDropdownMenuItemsExternalContainer
hasMaxHeight={hasMaxHeight} hasMaxHeight={hasMaxHeight}
className={className} className={className}

View File

@ -31,7 +31,10 @@ export const ShowPageContainer = ({ children }: ShowPageContainerProps) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
return isMobile ? ( return isMobile ? (
<StyledOuterContainer> <StyledOuterContainer>
<StyledScrollWrapper contextProviderName="showPageContainer"> <StyledScrollWrapper
contextProviderName="showPageContainer"
componentInstanceId={'scroll-wrapper-show-page-container'}
>
<StyledInnerContainer>{children}</StyledInnerContainer> <StyledInnerContainer>{children}</StyledInnerContainer>
</StyledScrollWrapper> </StyledScrollWrapper>
</StyledOuterContainer> </StyledOuterContainer>

View File

@ -23,7 +23,10 @@ export const ShowPageActivityContainer = ({
); );
return !isNewViewableRecordLoading ? ( return !isNewViewableRecordLoading ? (
<ScrollWrapper contextProviderName="showPageActivityContainer"> <ScrollWrapper
contextProviderName="showPageActivityContainer"
componentInstanceId={`scroll-wrapper-tab-list-${targetableObject.id}`}
>
<StyledShowPageActivityContainer> <StyledShowPageActivityContainer>
<RichTextEditor <RichTextEditor
activityId={targetableObject.id} activityId={targetableObject.id}

View File

@ -46,7 +46,10 @@ export const ShowPageLeftContainer = ({
{children} {children}
</StyledInnerContainer> </StyledInnerContainer>
) : ( ) : (
<ScrollWrapper contextProviderName="showPageLeftContainer"> <ScrollWrapper
contextProviderName="showPageLeftContainer"
componentInstanceId={`scroll-wrapper-show-page-left-container`}
>
<StyledIntermediateContainer> <StyledIntermediateContainer>
<StyledInnerContainer isMobile={isMobile}> <StyledInnerContainer isMobile={isMobile}>
{children} {children}

View File

@ -7,8 +7,9 @@ import { TabListScope } from '@/ui/layout/tab/scopes/TabListScope';
import { TabListFromUrlOptionalEffect } from '@/ui/layout/tab/components/TabListFromUrlOptionalEffect'; import { TabListFromUrlOptionalEffect } from '@/ui/layout/tab/components/TabListFromUrlOptionalEffect';
import { LayoutCard } from '@/ui/layout/tab/types/LayoutCard'; import { LayoutCard } from '@/ui/layout/tab/types/LayoutCard';
import { Tab } from './Tab'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Tab } from './Tab';
export type SingleTabProps = { export type SingleTabProps = {
title: string; title: string;
@ -70,26 +71,32 @@ export const TabList = ({
componentInstanceId={tabListInstanceId} componentInstanceId={tabListInstanceId}
tabListIds={tabs.map((tab) => tab.id)} tabListIds={tabs.map((tab) => tab.id)}
/> />
<StyledTabsContainer> <ScrollWrapper
{visibleTabs.map((tab) => ( defaultEnableYScroll={false}
<Tab contextProviderName="tabList"
id={tab.id} componentInstanceId={`scroll-wrapper-tab-list-${tabListInstanceId}`}
key={tab.id} >
title={tab.title} <StyledTabsContainer>
Icon={tab.Icon} {visibleTabs.map((tab) => (
logo={tab.logo} <Tab
active={tab.id === activeTabId} id={tab.id}
disabled={tab.disabled ?? loading} key={tab.id}
pill={tab.pill} title={tab.title}
to={behaveAsLinks ? `#${tab.id}` : undefined} Icon={tab.Icon}
onClick={() => { logo={tab.logo}
if (!behaveAsLinks) { active={tab.id === activeTabId}
setActiveTabId(tab.id); disabled={tab.disabled ?? loading}
} pill={tab.pill}
}} to={behaveAsLinks ? `#${tab.id}` : undefined}
/> onClick={() => {
))} if (!behaveAsLinks) {
</StyledTabsContainer> setActiveTabId(tab.id);
}
}}
/>
))}
</StyledTabsContainer>
</ScrollWrapper>
</TabListScope> </TabListScope>
</StyledContainer> </StyledContainer>
); );

View File

@ -2,15 +2,17 @@ import styled from '@emotion/styled';
import { OverlayScrollbars } from 'overlayscrollbars'; import { OverlayScrollbars } from 'overlayscrollbars';
import { useOverlayScrollbars } from 'overlayscrollbars-react'; import { useOverlayScrollbars } from 'overlayscrollbars-react';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { useSetRecoilState } from 'recoil';
import { import {
ContextProviderName, ContextProviderName,
getContextByProviderName, getContextByProviderName,
} from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts'; } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
import { useScrollStates } from '@/ui/utilities/scroll/hooks/internal/useScrollStates';
import { overlayScrollbarsState } from '@/ui/utilities/scroll/states/overlayScrollbarsState';
import { ScrollWrapperComponentInstanceContext } from '@/ui/utilities/scroll/states/contexts/ScrollWrapperComponentInstanceContext';
import { scrollWrapperInstanceComponentState } from '@/ui/utilities/scroll/states/scrollWrapperInstanceComponentState';
import { scrollWrapperScrollLeftComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollLeftComponentState';
import { scrollWrapperScrollTopComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollTopComponentState';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import 'overlayscrollbars/overlayscrollbars.css'; import 'overlayscrollbars/overlayscrollbars.css';
const StyledScrollWrapper = styled.div<{ scrollHide?: boolean }>` const StyledScrollWrapper = styled.div<{ scrollHide?: boolean }>`
@ -31,41 +33,52 @@ const StyledInnerContainer = styled.div`
export type ScrollWrapperProps = { export type ScrollWrapperProps = {
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
enableXScroll?: boolean; defaultEnableXScroll?: boolean;
enableYScroll?: boolean; defaultEnableYScroll?: boolean;
contextProviderName: ContextProviderName; contextProviderName: ContextProviderName;
scrollHide?: boolean; scrollHide?: boolean;
componentInstanceId: string;
}; };
export const ScrollWrapper = ({ export const ScrollWrapper = ({
componentInstanceId,
children, children,
className, className,
enableXScroll = true, defaultEnableXScroll = true,
enableYScroll = true, defaultEnableYScroll = true,
contextProviderName, contextProviderName,
scrollHide = false, scrollHide = false,
}: ScrollWrapperProps) => { }: ScrollWrapperProps) => {
const scrollableRef = useRef<HTMLDivElement>(null); const scrollableRef = useRef<HTMLDivElement>(null);
const Context = getContextByProviderName(contextProviderName); const Context = getContextByProviderName(contextProviderName);
const { scrollTopComponentState, scrollLeftComponentState } = const setScrollTop = useSetRecoilComponentStateV2(
useScrollStates(contextProviderName); scrollWrapperScrollTopComponentState,
const setScrollTop = useSetRecoilState(scrollTopComponentState); componentInstanceId,
const setScrollLeft = useSetRecoilState(scrollLeftComponentState); );
const setScrollLeft = useSetRecoilComponentStateV2(
scrollWrapperScrollLeftComponentState,
componentInstanceId,
);
const handleScroll = (overlayScroll: OverlayScrollbars) => { const handleScroll = (overlayScroll: OverlayScrollbars) => {
const target = overlayScroll.elements().scrollOffsetElement; const target = overlayScroll.elements().scrollOffsetElement;
setScrollTop(target.scrollTop); setScrollTop(target.scrollTop);
setScrollLeft(target.scrollLeft); setScrollLeft(target.scrollLeft);
}; };
const setOverlayScrollbars = useSetRecoilState(overlayScrollbarsState); const setOverlayScrollbars = useSetRecoilComponentStateV2(
scrollWrapperInstanceComponentState,
componentInstanceId,
);
const [initialize, instance] = useOverlayScrollbars({ const [initialize, instance] = useOverlayScrollbars({
options: { options: {
scrollbars: { autoHide: 'scroll' }, scrollbars: { autoHide: 'scroll' },
overflow: { overflow: {
x: enableXScroll ? undefined : 'hidden', x: defaultEnableXScroll ? undefined : 'hidden',
y: enableYScroll ? undefined : 'hidden', y: defaultEnableYScroll ? undefined : 'hidden',
}, },
}, },
events: { events: {
@ -84,19 +97,23 @@ export const ScrollWrapper = ({
}, [instance, setOverlayScrollbars]); }, [instance, setOverlayScrollbars]);
return ( return (
<Context.Provider <ScrollWrapperComponentInstanceContext.Provider
value={{ value={{ instanceId: componentInstanceId }}
ref: scrollableRef,
id: contextProviderName,
}}
> >
<StyledScrollWrapper <Context.Provider
ref={scrollableRef} value={{
className={className} ref: scrollableRef,
scrollHide={scrollHide} id: contextProviderName,
}}
> >
<StyledInnerContainer>{children}</StyledInnerContainer> <StyledScrollWrapper
</StyledScrollWrapper> ref={scrollableRef}
</Context.Provider> className={className}
scrollHide={scrollHide}
>
<StyledInnerContainer>{children}</StyledInnerContainer>
</StyledScrollWrapper>
</Context.Provider>
</ScrollWrapperComponentInstanceContext.Provider>
); );
}; };

View File

@ -1,32 +0,0 @@
import {
ContextProviderName,
getContextByProviderName,
} from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
import { scrollLeftComponentState } from '@/ui/utilities/scroll/states/scrollLeftComponentState';
import { scrollTopComponentState } from '@/ui/utilities/scroll/states/scrollTopComponentState';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
import { useContext } from 'react';
export const useScrollStates = (contextProviderName: ContextProviderName) => {
const Context = getContextByProviderName(contextProviderName);
const context = useContext(Context);
if (!context) {
throw new Error('Context not found');
}
const { id: scopeId } = context;
return {
scrollLeftComponentState: extractComponentState(
scrollLeftComponentState,
scopeId,
),
scrollTopComponentState: extractComponentState(
scrollTopComponentState,
scopeId,
),
};
};

View File

@ -1,10 +0,0 @@
import { ContextProviderName } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
import { useScrollStates } from '@/ui/utilities/scroll/hooks/internal/useScrollStates';
import { useRecoilValue } from 'recoil';
export const useScrollLeftValue = (
contextProviderName: ContextProviderName,
) => {
const { scrollLeftComponentState } = useScrollStates(contextProviderName);
return useRecoilValue(scrollLeftComponentState);
};

View File

@ -1,8 +0,0 @@
import { ContextProviderName } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
import { useScrollStates } from '@/ui/utilities/scroll/hooks/internal/useScrollStates';
import { useRecoilValue } from 'recoil';
export const useScrollTopValue = (contextProviderName: ContextProviderName) => {
const { scrollTopComponentState } = useScrollStates(contextProviderName);
return useRecoilValue(scrollTopComponentState);
};

View File

@ -0,0 +1,34 @@
import { scrollWrapperInstanceComponentState } from '@/ui/utilities/scroll/states/scrollWrapperInstanceComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const useToggleScrollWrapper = () => {
const instanceOverlay = useRecoilComponentValueV2(
scrollWrapperInstanceComponentState,
);
const toggleScrollXWrapper = (isEnabled: boolean) => {
if (!instanceOverlay) {
return;
}
instanceOverlay.options({
overflow: {
x: isEnabled ? 'scroll' : 'hidden',
},
});
};
const toggleScrollYWrapper = (isEnabled: boolean) => {
if (!instanceOverlay) {
return;
}
instanceOverlay.options({
overflow: {
y: isEnabled ? 'scroll' : 'hidden',
},
});
};
return { toggleScrollXWrapper, toggleScrollYWrapper };
};

View File

@ -0,0 +1,4 @@
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
export const ScrollWrapperComponentInstanceContext =
createComponentInstanceContext();

View File

@ -1,7 +0,0 @@
import { OverlayScrollbars } from 'overlayscrollbars';
import { createState } from 'twenty-ui';
export const overlayScrollbarsState = createState<OverlayScrollbars | null>({
key: 'scroll/overlayScrollbarsState',
defaultValue: null,
});

View File

@ -1,6 +0,0 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const scrollLeftComponentState = createComponentState<number>({
key: 'scroll/scrollLeftComponentState',
defaultValue: 0,
});

View File

@ -1,6 +0,0 @@
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
export const scrollPositionState = createFamilyState({
key: 'scroll/scrollPositionState',
defaultValue: 0,
});

View File

@ -1,6 +0,0 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const scrollTopComponentState = createComponentState<number>({
key: 'scroll/scrollTopComponentState',
defaultValue: 0,
});

View File

@ -0,0 +1,10 @@
import { ScrollWrapperComponentInstanceContext } from '@/ui/utilities/scroll/states/contexts/ScrollWrapperComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
import { OverlayScrollbars } from 'overlayscrollbars';
export const scrollWrapperInstanceComponentState =
createComponentStateV2<OverlayScrollbars | null>({
key: 'scrollWrapperInstanceComponentState',
defaultValue: null,
componentInstanceContext: ScrollWrapperComponentInstanceContext,
});

View File

@ -0,0 +1,9 @@
import { ScrollWrapperComponentInstanceContext } from '@/ui/utilities/scroll/states/contexts/ScrollWrapperComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const scrollWrapperScrollLeftComponentState =
createComponentStateV2<number>({
key: 'scrollWrapperScrollLeftComponentState',
defaultValue: 0,
componentInstanceContext: ScrollWrapperComponentInstanceContext,
});

View File

@ -0,0 +1,9 @@
import { ScrollWrapperComponentInstanceContext } from '@/ui/utilities/scroll/states/contexts/ScrollWrapperComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const scrollWrapperScrollTopComponentState =
createComponentStateV2<number>({
key: 'scrollWrapperScrollTopComponentState',
defaultValue: 0,
componentInstanceContext: ScrollWrapperComponentInstanceContext,
});

View File

@ -14,6 +14,5 @@ export const mapColumnDefinitionsToViewFields = (
size: columnDefinition.size, size: columnDefinition.size,
isVisible: columnDefinition.isVisible ?? true, isVisible: columnDefinition.isVisible ?? true,
definition: columnDefinition, definition: columnDefinition,
aggregateOperation: columnDefinition.aggregateOperation,
})); }));
}; };

View File

@ -47,7 +47,6 @@ export const mapViewFieldsToColumnDefinitions = ({
isLabelIdentifier, isLabelIdentifier,
isVisible: isLabelIdentifier || viewField.isVisible, isVisible: isLabelIdentifier || viewField.isVisible,
viewFieldId: viewField.id, viewFieldId: viewField.id,
aggregateOperation: viewField.aggregateOperation,
isSortable: correspondingColumnDefinition.isSortable, isSortable: correspondingColumnDefinition.isSortable,
isFilterable: correspondingColumnDefinition.isFilterable, isFilterable: correspondingColumnDefinition.isFilterable,
defaultValue: correspondingColumnDefinition.defaultValue, defaultValue: correspondingColumnDefinition.defaultValue,

View File

@ -118,7 +118,10 @@ export const Releases = () => {
]} ]}
> >
<SettingsPageContainer> <SettingsPageContainer>
<ScrollWrapper contextProviderName="releases"> <ScrollWrapper
contextProviderName="releases"
componentInstanceId="scroll-wrapper-releases"
>
<StyledReleaseContainer> <StyledReleaseContainer>
{releases.map((release) => ( {releases.map((release) => (
<React.Fragment key={release.slug}> <React.Fragment key={release.slug}>