Added parallel code path to set new record sorts state (#10345)

This PR implements a parallel code path to set record sorts without
impacting the actual sort, like we did on record filter.

We add a ViewBarRecordSortEffect which mirrors
ViewBarRecordFilterEffect.

It also adds availableFieldMetadataItemsForSortFamilySelector to replace
sortDefinitions progressively.
This commit is contained in:
Lucas Bordeau
2025-02-20 10:40:25 +01:00
committed by GitHub
parent 316876fcb5
commit 3f93aba5fc
31 changed files with 326 additions and 129 deletions

View File

@ -0,0 +1,26 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { filterSortableFieldMetadataItems } from '@/object-metadata/utils/filterSortableFieldMetadataItems';
import { selectorFamily } from 'recoil';
import { isDefined } from 'twenty-shared';
export const availableFieldMetadataItemsForSortFamilySelector = selectorFamily({
key: 'availableFieldMetadataItemsForSortFamilySelector',
get:
({ objectMetadataItemId }: { objectMetadataItemId: string }) =>
({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === objectMetadataItemId,
);
if (!isDefined(objectMetadataItem)) {
return [];
}
const availableFieldMetadataItemsForSort =
objectMetadataItem.fields.filter(filterSortableFieldMetadataItems);
return availableFieldMetadataItemsForSort;
},
});

View File

@ -0,0 +1,13 @@
import { SORTABLE_FIELD_METADATA_TYPES } from '@/object-metadata/constants/SortableFieldMetadataTypes';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
export const filterSortableFieldMetadataItems = (field: FieldMetadataItem) => {
const isSystemField = field.isSystem;
const isFieldActive = field.isActive;
const isFieldTypeSortable = SORTABLE_FIELD_METADATA_TYPES.includes(
field.type,
);
return !isSystemField && isFieldActive && isFieldTypeSortable;
};

View File

@ -11,14 +11,6 @@ export const getRelationObjectMetadataNameSingular = ({
return field.relationDefinition?.targetObjectMetadata.nameSingular; return field.relationDefinition?.targetObjectMetadata.nameSingular;
}; };
export const getRelationObjectMetadataNamePlural = ({
field,
}: {
field: ObjectMetadataItem['fields'][0];
}): string | undefined => {
return field.relationDefinition?.targetObjectMetadata.namePlural;
};
export const getFilterTypeFromFieldType = ( export const getFilterTypeFromFieldType = (
fieldType: FieldMetadataType, fieldType: FieldMetadataType,
): FilterableFieldType => { ): FilterableFieldType => {

View File

@ -6,12 +6,16 @@ import { useCloseSortDropdown } from '@/object-record/object-sort-dropdown/hooks
import { useResetRecordSortDropdownSearchInput } from '@/object-record/object-sort-dropdown/hooks/useResetRecordSortDropdownSearchInput'; import { useResetRecordSortDropdownSearchInput } from '@/object-record/object-sort-dropdown/hooks/useResetRecordSortDropdownSearchInput';
import { useResetSortDropdown } from '@/object-record/object-sort-dropdown/hooks/useResetSortDropdown'; import { useResetSortDropdown } from '@/object-record/object-sort-dropdown/hooks/useResetSortDropdown';
import { useToggleSortDropdown } from '@/object-record/object-sort-dropdown/hooks/useToggleSortDropdown'; import { useToggleSortDropdown } from '@/object-record/object-sort-dropdown/hooks/useToggleSortDropdown';
import { isSortDirectionMenuUnfoldedComponentState } from '@/object-record/object-sort-dropdown/states/isSortDirectionMenuUnfoldedState'; import { isRecordSortDirectionMenuUnfoldedComponentState } from '@/object-record/object-sort-dropdown/states/isRecordSortDirectionMenuUnfoldedComponentState';
import { objectSortDropdownSearchInputComponentState } from '@/object-record/object-sort-dropdown/states/objectSortDropdownSearchInputComponentState'; import { objectSortDropdownSearchInputComponentState } from '@/object-record/object-sort-dropdown/states/objectSortDropdownSearchInputComponentState';
import { onSortSelectComponentState } from '@/object-record/object-sort-dropdown/states/onSortSelectScopedState'; import { onSortSelectComponentState } from '@/object-record/object-sort-dropdown/states/onSortSelectScopedState';
import { selectedSortDirectionComponentState } from '@/object-record/object-sort-dropdown/states/selectedSortDirectionState'; import { selectedRecordSortDirectionComponentState } from '@/object-record/object-sort-dropdown/states/selectedRecordSortDirectionComponentState';
import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition'; import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import {
RECORD_SORT_DIRECTIONS,
RecordSortDirection,
} from '@/object-record/record-sort/types/RecordSortDirection';
import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector'; import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
@ -26,7 +30,7 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { availableSortDefinitionsComponentState } from '@/views/states/availableSortDefinitionsComponentState'; import { availableSortDefinitionsComponentState } from '@/views/states/availableSortDefinitionsComponentState';
import { Trans, useLingui } from '@lingui/react/macro'; import { Trans, useLingui } from '@lingui/react/macro';
import { SORT_DIRECTIONS, SortDirection } from '../types/SortDirection'; import { v4 } from 'uuid';
export const StyledInput = styled.input` export const StyledInput = styled.input`
background: transparent; background: transparent;
@ -79,8 +83,8 @@ export const ObjectSortDropdownButton = ({
objectSortDropdownSearchInputComponentState, objectSortDropdownSearchInputComponentState,
); );
const isSortDirectionMenuUnfolded = useRecoilComponentValueV2( const isRecordSortDirectionMenuUnfolded = useRecoilComponentValueV2(
isSortDirectionMenuUnfoldedComponentState, isRecordSortDirectionMenuUnfoldedComponentState,
); );
const { resetSortDropdown } = useResetSortDropdown(); const { resetSortDropdown } = useResetSortDropdown();
@ -153,22 +157,23 @@ export const ObjectSortDropdownButton = ({
setObjectSortDropdownSearchInput(''); setObjectSortDropdownSearchInput('');
closeSortDropdown(); closeSortDropdown();
onSortSelect?.({ onSortSelect?.({
id: v4(),
fieldMetadataId: sortDefinition.fieldMetadataId, fieldMetadataId: sortDefinition.fieldMetadataId,
direction: selectedSortDirection, direction: selectedRecordSortDirection,
definition: sortDefinition, definition: sortDefinition,
}); });
}; };
const [selectedSortDirection, setSelectedSortDirection] = const [selectedRecordSortDirection, setSelectedRecordSortDirection] =
useRecoilComponentStateV2(selectedSortDirectionComponentState); useRecoilComponentStateV2(selectedRecordSortDirectionComponentState);
const setIsSortDirectionMenuUnfolded = useSetRecoilComponentStateV2( const setIsRecordSortDirectionMenuUnfolded = useSetRecoilComponentStateV2(
isSortDirectionMenuUnfoldedComponentState, isRecordSortDirectionMenuUnfoldedComponentState,
); );
const handleSortDirectionClick = (sortDirection: SortDirection) => { const handleSortDirectionClick = (sortDirection: RecordSortDirection) => {
setSelectedSortDirection(sortDirection); setSelectedRecordSortDirection(sortDirection);
setIsSortDirectionMenuUnfolded(false); setIsRecordSortDirectionMenuUnfolded(false);
}; };
const { isDropdownOpen } = useDropdown(OBJECT_SORT_DROPDOWN_ID); const { isDropdownOpen } = useDropdown(OBJECT_SORT_DROPDOWN_ID);
@ -190,10 +195,10 @@ export const ObjectSortDropdownButton = ({
} }
dropdownComponents={ dropdownComponents={
<> <>
{isSortDirectionMenuUnfolded && ( {isRecordSortDirectionMenuUnfolded && (
<StyledSelectedSortDirectionContainer> <StyledSelectedSortDirectionContainer>
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
{SORT_DIRECTIONS.map((sortDirection, index) => ( {RECORD_SORT_DIRECTIONS.map((sortDirection, index) => (
<MenuItem <MenuItem
key={index} key={index}
onClick={() => handleSortDirectionClick(sortDirection)} onClick={() => handleSortDirectionClick(sortDirection)}
@ -208,10 +213,14 @@ export const ObjectSortDropdownButton = ({
<DropdownMenuHeader <DropdownMenuHeader
EndIcon={IconChevronDown} EndIcon={IconChevronDown}
onClick={() => onClick={() =>
setIsSortDirectionMenuUnfolded(!isSortDirectionMenuUnfolded) setIsRecordSortDirectionMenuUnfolded(
!isRecordSortDirectionMenuUnfolded,
)
} }
> >
{selectedSortDirection === 'asc' ? t`Ascending` : t`Descending`} {selectedRecordSortDirection === 'asc'
? t`Ascending`
: t`Descending`}
</DropdownMenuHeader> </DropdownMenuHeader>
<StyledInput <StyledInput
autoFocus autoFocus

View File

@ -1,19 +1,19 @@
import { isSortDirectionMenuUnfoldedComponentState } from '@/object-record/object-sort-dropdown/states/isSortDirectionMenuUnfoldedState'; import { isRecordSortDirectionMenuUnfoldedComponentState } from '@/object-record/object-sort-dropdown/states/isRecordSortDirectionMenuUnfoldedComponentState';
import { selectedSortDirectionComponentState } from '@/object-record/object-sort-dropdown/states/selectedSortDirectionState'; import { selectedRecordSortDirectionComponentState } from '@/object-record/object-sort-dropdown/states/selectedRecordSortDirectionComponentState';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
export const useResetSortDropdown = () => { export const useResetSortDropdown = () => {
const setIsSortDirectionMenuUnfolded = useSetRecoilComponentStateV2( const setIsRecordSortDirectionMenuUnfolded = useSetRecoilComponentStateV2(
isSortDirectionMenuUnfoldedComponentState, isRecordSortDirectionMenuUnfoldedComponentState,
); );
const setSelectedSortDirection = useSetRecoilComponentStateV2( const setSelectedRecordSortDirection = useSetRecoilComponentStateV2(
selectedSortDirectionComponentState, selectedRecordSortDirectionComponentState,
); );
const resetSortDropdown = () => { const resetSortDropdown = () => {
setIsSortDirectionMenuUnfolded(false); setIsRecordSortDirectionMenuUnfolded(false);
setSelectedSortDirection('asc'); setSelectedRecordSortDirection('asc');
}; };
return { return {

View File

@ -1,24 +0,0 @@
import { isSortSelectedComponentState } from '@/object-record/object-sort-dropdown/states/isSortSelectedScopedState';
import { objectSortDropdownSearchInputComponentState } from '@/object-record/object-sort-dropdown/states/objectSortDropdownSearchInputComponentState';
import { onSortSelectComponentState } from '@/object-record/object-sort-dropdown/states/onSortSelectScopedState';
export const useSortDropdownStates = (scopeId: string) => {
const isSortSelectedState = isSortSelectedComponentState.atomFamily({
instanceId: scopeId,
});
const onSortSelectState = onSortSelectComponentState.atomFamily({
instanceId: scopeId,
});
const objectSortDropdownSearchInputState =
objectSortDropdownSearchInputComponentState.atomFamily({
instanceId: scopeId,
});
return {
isSortSelectedState,
onSortSelectState,
objectSortDropdownSearchInputState,
};
};

View File

@ -1,8 +1,9 @@
import { ObjectSortDropdownComponentInstanceContext } from '@/object-record/object-sort-dropdown/states/context/ObjectSortDropdownComponentInstanceContext'; import { ObjectSortDropdownComponentInstanceContext } from '@/object-record/object-sort-dropdown/states/context/ObjectSortDropdownComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const isSortSelectedComponentState = createComponentStateV2<boolean>({ export const isRecordSortDirectionMenuUnfoldedComponentState =
key: 'isSortSelectedComponentState', createComponentStateV2<boolean>({
defaultValue: false, key: 'isRecordSortDirectionMenuUnfoldedComponentState',
componentInstanceContext: ObjectSortDropdownComponentInstanceContext, defaultValue: false,
}); componentInstanceContext: ObjectSortDropdownComponentInstanceContext,
});

View File

@ -1,9 +1,9 @@
import { ObjectSortDropdownComponentInstanceContext } from '@/object-record/object-sort-dropdown/states/context/ObjectSortDropdownComponentInstanceContext'; import { ObjectSortDropdownComponentInstanceContext } from '@/object-record/object-sort-dropdown/states/context/ObjectSortDropdownComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const isSortDirectionMenuUnfoldedComponentState = export const isRecordSortSelectedComponentState =
createComponentStateV2<boolean>({ createComponentStateV2<boolean>({
key: 'isSortDirectionMenuUnfoldedComponentState', key: 'isRecordSortSelectedComponentState',
defaultValue: false, defaultValue: false,
componentInstanceContext: ObjectSortDropdownComponentInstanceContext, componentInstanceContext: ObjectSortDropdownComponentInstanceContext,
}); });

View File

@ -1,9 +1,9 @@
import { ObjectSortDropdownComponentInstanceContext } from '@/object-record/object-sort-dropdown/states/context/ObjectSortDropdownComponentInstanceContext'; import { ObjectSortDropdownComponentInstanceContext } from '@/object-record/object-sort-dropdown/states/context/ObjectSortDropdownComponentInstanceContext';
import { RecordSort } from '@/object-record/record-sort/types/RecordSort';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
import { Sort } from '../types/Sort';
export const onSortSelectComponentState = createComponentStateV2< export const onSortSelectComponentState = createComponentStateV2<
((sort: Sort) => void) | undefined ((sort: RecordSort) => void) | undefined
>({ >({
key: 'onSortSelectComponentState', key: 'onSortSelectComponentState',
defaultValue: undefined, defaultValue: undefined,

View File

@ -1,10 +1,10 @@
import { ObjectSortDropdownComponentInstanceContext } from '@/object-record/object-sort-dropdown/states/context/ObjectSortDropdownComponentInstanceContext'; import { ObjectSortDropdownComponentInstanceContext } from '@/object-record/object-sort-dropdown/states/context/ObjectSortDropdownComponentInstanceContext';
import { SortDirection } from '@/object-record/object-sort-dropdown/types/SortDirection'; import { RecordSortDirection } from '@/object-record/record-sort/types/RecordSortDirection';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const selectedSortDirectionComponentState = export const selectedRecordSortDirectionComponentState =
createComponentStateV2<SortDirection>({ createComponentStateV2<RecordSortDirection>({
key: 'selectedSortDirectionComponentState', key: 'selectedRecordSortDirectionComponentState',
defaultValue: 'asc', defaultValue: 'asc',
componentInstanceContext: ObjectSortDropdownComponentInstanceContext, componentInstanceContext: ObjectSortDropdownComponentInstanceContext,
}); });

View File

@ -1,8 +0,0 @@
import { SortDefinition } from './SortDefinition';
import { SortDirection } from './SortDirection';
export type Sort = {
fieldMetadataId: string;
direction: SortDirection;
definition: SortDefinition;
};

View File

@ -1,8 +1,5 @@
import { SortDirection } from './SortDirection';
export type SortDefinition = { export type SortDefinition = {
fieldMetadataId: string; fieldMetadataId: string;
label: string; label: string;
iconName: string; iconName: string;
getOrderByTemplate?: (direction: SortDirection) => any[];
}; };

View File

@ -1,3 +0,0 @@
export const SORT_DIRECTIONS = ['asc', 'desc'] as const;
export type SortDirection = (typeof SORT_DIRECTIONS)[number];

View File

@ -1,8 +1,8 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { Sort } from '@/object-record/object-sort-dropdown/types/Sort';
import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition'; import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { RecordSort } from '@/object-record/record-sort/types/RecordSort';
const sortDefinition: SortDefinition = { const sortDefinition: SortDefinition = {
fieldMetadataId: 'id', fieldMetadataId: 'id',
@ -42,8 +42,9 @@ describe('turnSortsIntoOrderBy', () => {
}); });
it('should create OrderByField with single sort', () => { it('should create OrderByField with single sort', () => {
const sorts: Sort[] = [ const sorts: RecordSort[] = [
{ {
id: 'id',
fieldMetadataId: 'field1', fieldMetadataId: 'field1',
direction: 'asc', direction: 'asc',
definition: sortDefinition, definition: sortDefinition,
@ -56,13 +57,15 @@ describe('turnSortsIntoOrderBy', () => {
}); });
it('should create OrderByField with multiple sorts', () => { it('should create OrderByField with multiple sorts', () => {
const sorts: Sort[] = [ const sorts: RecordSort[] = [
{ {
id: 'id',
fieldMetadataId: 'field1', fieldMetadataId: 'field1',
direction: 'asc', direction: 'asc',
definition: sortDefinition, definition: sortDefinition,
}, },
{ {
id: 'id',
fieldMetadataId: 'field2', fieldMetadataId: 'field2',
direction: 'desc', direction: 'desc',
definition: sortDefinition, definition: sortDefinition,
@ -82,8 +85,9 @@ describe('turnSortsIntoOrderBy', () => {
}); });
it('should ignore if field not found', () => { it('should ignore if field not found', () => {
const sorts: Sort[] = [ const sorts: RecordSort[] = [
{ {
id: 'id',
fieldMetadataId: 'invalidField', fieldMetadataId: 'invalidField',
direction: 'asc', direction: 'asc',
definition: sortDefinition, definition: sortDefinition,
@ -95,8 +99,9 @@ describe('turnSortsIntoOrderBy', () => {
}); });
it('should not return position for remotes', () => { it('should not return position for remotes', () => {
const sorts: Sort[] = [ const sorts: RecordSort[] = [
{ {
id: 'id',
fieldMetadataId: 'invalidField', fieldMetadataId: 'invalidField',
direction: 'asc', direction: 'asc',
definition: sortDefinition, definition: sortDefinition,

View File

@ -8,12 +8,12 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getOrderByForFieldMetadataType } from '@/object-metadata/utils/getOrderByForFieldMetadataType'; import { getOrderByForFieldMetadataType } from '@/object-metadata/utils/getOrderByForFieldMetadataType';
import { RecordSort } from '@/object-record/record-sort/types/RecordSort';
import { OrderBy } from '@/types/OrderBy'; import { OrderBy } from '@/types/OrderBy';
import { Sort } from '../types/Sort';
export const turnSortsIntoOrderBy = ( export const turnSortsIntoOrderBy = (
objectMetadataItem: ObjectMetadataItem, objectMetadataItem: ObjectMetadataItem,
sorts: Sort[], sorts: RecordSort[],
): RecordGqlOperationOrderBy => { ): RecordGqlOperationOrderBy => {
const fields: Pick<FieldMetadataItem, 'id' | 'name' | 'type'>[] = const fields: Pick<FieldMetadataItem, 'id' | 'name' | 'type'>[] =
objectMetadataItem?.fields ?? []; objectMetadataItem?.fields ?? [];

View File

@ -2,9 +2,11 @@ import { useCallback } from 'react';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { Sort } from '@/object-record/object-sort-dropdown/types/Sort'; import { useUpsertRecordSort } from '@/object-record/record-sort/hooks/useUpsertRecordSort';
import { RecordSort } from '@/object-record/record-sort/types/RecordSort';
import { useUpsertCombinedViewSorts } from '@/views/hooks/useUpsertCombinedViewSorts'; import { useUpsertCombinedViewSorts } from '@/views/hooks/useUpsertCombinedViewSorts';
import { isDefined } from 'twenty-shared'; import { isDefined } from 'twenty-shared';
import { v4 } from 'uuid';
type UseHandleToggleColumnSortProps = { type UseHandleToggleColumnSortProps = {
objectNameSingular: string; objectNameSingular: string;
@ -24,6 +26,8 @@ export const useHandleToggleColumnSort = ({
const { upsertCombinedViewSort } = useUpsertCombinedViewSorts(viewBarId); const { upsertCombinedViewSort } = useUpsertCombinedViewSorts(viewBarId);
const { upsertRecordSort } = useUpsertRecordSort();
const handleToggleColumnSort = useCallback( const handleToggleColumnSort = useCallback(
(fieldMetadataId: string) => { (fieldMetadataId: string) => {
const correspondingColumnDefinition = columnDefinitions.find( const correspondingColumnDefinition = columnDefinitions.find(
@ -33,7 +37,8 @@ export const useHandleToggleColumnSort = ({
if (!isDefined(correspondingColumnDefinition)) return; if (!isDefined(correspondingColumnDefinition)) return;
const newSort: Sort = { const newSort: RecordSort = {
id: v4(),
fieldMetadataId, fieldMetadataId,
definition: { definition: {
fieldMetadataId, fieldMetadataId,
@ -44,8 +49,9 @@ export const useHandleToggleColumnSort = ({
}; };
upsertCombinedViewSort(newSort); upsertCombinedViewSort(newSort);
upsertRecordSort(newSort);
}, },
[columnDefinitions, upsertCombinedViewSort], [columnDefinitions, upsertCombinedViewSort, upsertRecordSort],
); );
return handleToggleColumnSort; return handleToggleColumnSort;

View File

@ -1,8 +1,7 @@
import { RecordSort } from '@/object-record/record-sort/types/RecordSort';
import { createState } from '@ui/utilities/state/utils/createState'; import { createState } from '@ui/utilities/state/utils/createState';
import { Sort } from '@/object-record/object-sort-dropdown/types/Sort'; export const recordIndexSortsState = createState<RecordSort[]>({
export const recordIndexSortsState = createState<Sort[]>({
key: 'recordIndexSortsState', key: 'recordIndexSortsState',
defaultValue: [], defaultValue: [],
}); });

View File

@ -0,0 +1,15 @@
import { availableFieldMetadataItemsForSortFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useRecoilValue } from 'recoil';
export const useSortableFieldMetadataItemsInRecordIndexContext = () => {
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const sortableFieldMetadataItems = useRecoilValue(
availableFieldMetadataItemsForSortFamilySelector({
objectMetadataItemId: objectMetadataItem.id,
}),
);
return { sortableFieldMetadataItems };
};

View File

@ -0,0 +1,58 @@
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
import { RecordSort } from '@/object-record/record-sort/types/RecordSort';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { useRecoilCallback } from 'recoil';
export const useUpsertRecordSort = () => {
const currentRecordSortsCallbackState = useRecoilComponentCallbackStateV2(
currentRecordSortsComponentState,
);
const upsertRecordSort = useRecoilCallback(
({ set, snapshot }) =>
(recordSortToSet: RecordSort) => {
const currentRecordSorts = getSnapshotValue(
snapshot,
currentRecordSortsCallbackState,
);
const hasFoundRecordSortInCurrentRecordSorts = currentRecordSorts.some(
(existingSort) =>
existingSort.fieldMetadataId === recordSortToSet.fieldMetadataId,
);
if (!hasFoundRecordSortInCurrentRecordSorts) {
set(currentRecordSortsCallbackState, [
...currentRecordSorts,
recordSortToSet,
]);
} else {
set(currentRecordSortsCallbackState, (currentRecordSorts) => {
const newCurrentRecordSorts = [...currentRecordSorts];
const indexOfSortToUpdate = newCurrentRecordSorts.findIndex(
(existingSort) =>
existingSort.fieldMetadataId ===
recordSortToSet.fieldMetadataId,
);
if (indexOfSortToUpdate < 0) {
return newCurrentRecordSorts;
}
newCurrentRecordSorts[indexOfSortToUpdate] = {
...recordSortToSet,
};
return newCurrentRecordSorts;
});
}
},
[currentRecordSortsCallbackState],
);
return {
upsertRecordSort,
};
};

View File

@ -1,8 +1,8 @@
import { RecordSort } from '@/object-record/record-sort/types/RecordSort';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
import { Sort } from '../../object-sort-dropdown/types/Sort';
export const tableSortsComponentState = createComponentStateV2<Sort[]>({ export const tableSortsComponentState = createComponentStateV2<RecordSort[]>({
key: 'tableSortsComponentState', key: 'tableSortsComponentState',
defaultValue: [], defaultValue: [],
componentInstanceContext: RecordTableComponentInstanceContext, componentInstanceContext: RecordTableComponentInstanceContext,

View File

@ -1,36 +1,42 @@
import { IconArrowDown, IconArrowUp } from 'twenty-ui'; import { IconArrowDown, IconArrowUp } from 'twenty-ui';
import { Sort } from '@/object-record/object-sort-dropdown/types/Sort'; import { useUpsertRecordSort } from '@/object-record/record-sort/hooks/useUpsertRecordSort';
import { RecordSort } from '@/object-record/record-sort/types/RecordSort';
import { SortOrFilterChip } from '@/views/components/SortOrFilterChip'; import { SortOrFilterChip } from '@/views/components/SortOrFilterChip';
import { useDeleteCombinedViewSorts } from '@/views/hooks/useDeleteCombinedViewSorts'; import { useDeleteCombinedViewSorts } from '@/views/hooks/useDeleteCombinedViewSorts';
import { useUpsertCombinedViewSorts } from '@/views/hooks/useUpsertCombinedViewSorts'; import { useUpsertCombinedViewSorts } from '@/views/hooks/useUpsertCombinedViewSorts';
type EditableSortChipProps = { type EditableSortChipProps = {
viewSort: Sort; recordSort: RecordSort;
}; };
export const EditableSortChip = ({ viewSort }: EditableSortChipProps) => { export const EditableSortChip = ({ recordSort }: EditableSortChipProps) => {
const { deleteCombinedViewSort } = useDeleteCombinedViewSorts(); const { deleteCombinedViewSort } = useDeleteCombinedViewSorts();
const { upsertCombinedViewSort } = useUpsertCombinedViewSorts(); const { upsertCombinedViewSort } = useUpsertCombinedViewSorts();
const { upsertRecordSort } = useUpsertRecordSort();
const handleRemoveClick = () => { const handleRemoveClick = () => {
deleteCombinedViewSort(viewSort.fieldMetadataId); deleteCombinedViewSort(recordSort.fieldMetadataId);
}; };
const handleClick = () => { const handleClick = () => {
upsertCombinedViewSort({ const newSort: RecordSort = {
...viewSort, ...recordSort,
direction: viewSort.direction === 'asc' ? 'desc' : 'asc', direction: recordSort.direction === 'asc' ? 'desc' : 'asc',
}); };
upsertCombinedViewSort(newSort);
upsertRecordSort(newSort);
}; };
return ( return (
<SortOrFilterChip <SortOrFilterChip
key={viewSort.fieldMetadataId} key={recordSort.fieldMetadataId}
testId={viewSort.fieldMetadataId} testId={recordSort.fieldMetadataId}
labelValue={viewSort.definition.label} labelValue={recordSort.definition.label}
Icon={viewSort.direction === 'desc' ? IconArrowDown : IconArrowUp} Icon={recordSort.direction === 'desc' ? IconArrowDown : IconArrowUp}
onRemove={handleRemoveClick} onRemove={handleRemoveClick}
onClick={handleClick} onClick={handleClick}
/> />

View File

@ -19,6 +19,7 @@ import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types
import { VIEW_SORT_DROPDOWN_ID } from '@/object-record/object-sort-dropdown/constants/ViewSortDropdownId'; import { VIEW_SORT_DROPDOWN_ID } from '@/object-record/object-sort-dropdown/constants/ViewSortDropdownId';
import { ObjectSortDropdownComponentInstanceContext } from '@/object-record/object-sort-dropdown/states/context/ObjectSortDropdownComponentInstanceContext'; import { ObjectSortDropdownComponentInstanceContext } from '@/object-record/object-sort-dropdown/states/context/ObjectSortDropdownComponentInstanceContext';
import { ViewBarRecordFilterEffect } from '@/views/components/ViewBarRecordFilterEffect'; import { ViewBarRecordFilterEffect } from '@/views/components/ViewBarRecordFilterEffect';
import { ViewBarRecordSortEffect } from '@/views/components/ViewBarRecordSortEffect';
import { UpdateViewButtonGroup } from './UpdateViewButtonGroup'; import { UpdateViewButtonGroup } from './UpdateViewButtonGroup';
import { ViewBarDetails } from './ViewBarDetails'; import { ViewBarDetails } from './ViewBarDetails';
@ -48,6 +49,7 @@ export const ViewBar = ({
value={{ instanceId: VIEW_SORT_DROPDOWN_ID }} value={{ instanceId: VIEW_SORT_DROPDOWN_ID }}
> >
<ViewBarRecordFilterEffect /> <ViewBarRecordFilterEffect />
<ViewBarRecordSortEffect />
<ViewBarFilterEffect filterDropdownId={filterDropdownId} /> <ViewBarFilterEffect filterDropdownId={filterDropdownId} />
<ViewBarSortEffect /> <ViewBarSortEffect />
<QueryParamsFiltersEffect /> <QueryParamsFiltersEffect />

View File

@ -204,8 +204,11 @@ export const ViewBarDetails = ({
{mapViewSortsToSorts( {mapViewSortsToSorts(
currentViewWithCombinedFiltersAndSorts?.viewSorts ?? [], currentViewWithCombinedFiltersAndSorts?.viewSorts ?? [],
availableSortDefinitions, availableSortDefinitions,
).map((sort) => ( ).map((recordSort) => (
<EditableSortChip key={sort.fieldMetadataId} viewSort={sort} /> <EditableSortChip
key={recordSort.fieldMetadataId}
recordSort={recordSort}
/>
))} ))}
{isNonEmptyArray(recordFilters) && {isNonEmptyArray(recordFilters) &&
isNonEmptyArray( isNonEmptyArray(

View File

@ -21,6 +21,12 @@ export const ViewBarRecordFilterEffect = () => {
contextStoreCurrentObjectMetadataItemComponentState, contextStoreCurrentObjectMetadataItemComponentState,
); );
const currentView = useRecoilValue(
prefetchViewFromViewIdFamilySelector({
viewId: currentViewId ?? '',
}),
);
const [ const [
hasInitializedCurrentRecordFilters, hasInitializedCurrentRecordFilters,
setHasInitializedCurrentRecordFilters, setHasInitializedCurrentRecordFilters,
@ -43,12 +49,6 @@ export const ViewBarRecordFilterEffect = () => {
contextStoreCurrentObjectMetadataItem?.id, contextStoreCurrentObjectMetadataItem?.id,
); );
const currentView = useRecoilValue(
prefetchViewFromViewIdFamilySelector({
viewId: currentViewId ?? '',
}),
);
useEffect(() => { useEffect(() => {
if (isDefined(currentView) && !hasInitializedCurrentRecordFilters) { if (isDefined(currentView) && !hasInitializedCurrentRecordFilters) {
if ( if (

View File

@ -0,0 +1,83 @@
import { contextStoreCurrentObjectMetadataItemComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { availableFieldMetadataItemsForSortFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector';
import { formatFieldMetadataItemsAsSortDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions';
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { hasInitializedCurrentRecordSortsComponentFamilyState } from '@/views/states/hasInitializedCurrentRecordSortsComponentFamilyState';
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
export const ViewBarRecordSortEffect = () => {
const currentViewId = useRecoilComponentValueV2(
contextStoreCurrentViewIdComponentState,
);
const contextStoreCurrentObjectMetadataItem = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataItemComponentState,
);
const currentView = useRecoilValue(
prefetchViewFromViewIdFamilySelector({
viewId: currentViewId ?? '',
}),
);
const [
hasInitializedCurrentRecordSorts,
setHasInitializedCurrentRecordSorts,
] = useRecoilComponentFamilyStateV2(
hasInitializedCurrentRecordSortsComponentFamilyState,
{
viewId: currentViewId ?? undefined,
},
);
const setCurrentRecordSorts = useSetRecoilComponentStateV2(
currentRecordSortsComponentState,
);
const sortableFieldMetadataItems = useRecoilValue(
availableFieldMetadataItemsForSortFamilySelector({
objectMetadataItemId: contextStoreCurrentObjectMetadataItem?.id,
}),
);
useEffect(() => {
if (isDefined(currentView) && !hasInitializedCurrentRecordSorts) {
if (
currentView.objectMetadataId !==
contextStoreCurrentObjectMetadataItem?.id
) {
return;
}
const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({
fields: sortableFieldMetadataItems,
});
if (isDefined(currentView)) {
setCurrentRecordSorts(
mapViewSortsToSorts(currentView.viewSorts, sortDefinitions),
);
setHasInitializedCurrentRecordSorts(true);
}
}
}, [
hasInitializedCurrentRecordSorts,
currentView,
sortableFieldMetadataItems,
setCurrentRecordSorts,
contextStoreCurrentObjectMetadataItem,
setHasInitializedCurrentRecordSorts,
]);
return null;
};

View File

@ -1,7 +1,8 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { onSortSelectComponentState } from '@/object-record/object-sort-dropdown/states/onSortSelectScopedState'; import { onSortSelectComponentState } from '@/object-record/object-sort-dropdown/states/onSortSelectScopedState';
import { Sort } from '@/object-record/object-sort-dropdown/types/Sort'; import { useUpsertRecordSort } from '@/object-record/record-sort/hooks/useUpsertRecordSort';
import { RecordSort } from '@/object-record/record-sort/types/RecordSort';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; 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';
import { useUpsertCombinedViewSorts } from '@/views/hooks/useUpsertCombinedViewSorts'; import { useUpsertCombinedViewSorts } from '@/views/hooks/useUpsertCombinedViewSorts';
@ -16,6 +17,8 @@ export const ViewBarSortEffect = () => {
availableSortDefinitionsComponentState, availableSortDefinitionsComponentState,
); );
const { upsertRecordSort } = useUpsertRecordSort();
const setOnSortSelect = useSetRecoilComponentStateV2( const setOnSortSelect = useSetRecoilComponentStateV2(
onSortSelectComponentState, onSortSelectComponentState,
); );
@ -28,9 +31,10 @@ export const ViewBarSortEffect = () => {
if (isDefined(availableSortDefinitions)) { if (isDefined(availableSortDefinitions)) {
setAvailableSortDefinitionsInSortDropdown(availableSortDefinitions); setAvailableSortDefinitionsInSortDropdown(availableSortDefinitions);
} }
setOnSortSelect(() => (sort: Sort | null) => { setOnSortSelect(() => (sort: RecordSort | null) => {
if (isDefined(sort)) { if (isDefined(sort)) {
upsertCombinedViewSort(sort); upsertCombinedViewSort(sort);
upsertRecordSort(sort);
} }
}); });
}, [ }, [
@ -38,6 +42,7 @@ export const ViewBarSortEffect = () => {
setAvailableSortDefinitionsInSortDropdown, setAvailableSortDefinitionsInSortDropdown,
setOnSortSelect, setOnSortSelect,
upsertCombinedViewSort, upsertCombinedViewSort,
upsertRecordSort,
]); ]);
return <></>; return <></>;

View File

@ -2,7 +2,7 @@ import { useRecoilCallback } from 'recoil';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { Sort } from '@/object-record/object-sort-dropdown/types/Sort'; import { RecordSort } from '@/object-record/record-sort/types/RecordSort';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useGetViewFromPrefetchState } from '@/views/hooks/useGetViewFromPrefetchState'; import { useGetViewFromPrefetchState } from '@/views/hooks/useGetViewFromPrefetchState';
@ -32,7 +32,7 @@ export const useUpsertCombinedViewSorts = (viewBarComponentId?: string) => {
const upsertCombinedViewSort = useRecoilCallback( const upsertCombinedViewSort = useRecoilCallback(
({ snapshot, set }) => ({ snapshot, set }) =>
async (upsertedSort: Sort) => { async (upsertedSort: RecordSort) => {
const currentViewId = getSnapshotValue( const currentViewId = getSnapshotValue(
snapshot, snapshot,
currentViewIdCallbackState, currentViewIdCallbackState,

View File

@ -0,0 +1,9 @@
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2';
export const hasInitializedCurrentRecordSortsComponentFamilyState =
createComponentFamilyStateV2<boolean, { viewId?: string }>({
key: 'hasInitializedCurrentRecordSortsComponentFamilyState',
defaultValue: false,
componentInstanceContext: RecordSortsComponentInstanceContext,
});

View File

@ -1,8 +1,8 @@
import { SortDirection } from '@/object-record/object-sort-dropdown/types/SortDirection'; import { RecordSortDirection } from '@/object-record/record-sort/types/RecordSortDirection';
export type ViewSort = { export type ViewSort = {
__typename: 'ViewSort'; __typename: 'ViewSort';
id: string; id: string;
fieldMetadataId: string; fieldMetadataId: string;
direction: SortDirection; direction: RecordSortDirection;
}; };

View File

@ -1,6 +1,6 @@
import { Sort } from '@/object-record/object-sort-dropdown/types/Sort';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { RecordSort } from '@/object-record/record-sort/types/RecordSort';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { ViewField } from '@/views/types/ViewField'; import { ViewField } from '@/views/types/ViewField';
import { ViewFilter } from '@/views/types/ViewFilter'; import { ViewFilter } from '@/views/types/ViewFilter';
@ -39,8 +39,9 @@ describe('mapViewSortsToSorts', () => {
direction: 'asc', direction: 'asc',
}, },
]; ];
const expectedSorts: Sort[] = [ const expectedSorts: RecordSort[] = [
{ {
id: 'id',
fieldMetadataId: '05731f68-6e7a-4903-8374-c0b6a9063482', fieldMetadataId: '05731f68-6e7a-4903-8374-c0b6a9063482',
direction: 'asc', direction: 'asc',
definition: baseDefinition, definition: baseDefinition,

View File

@ -1,13 +1,13 @@
import { Sort } from '@/object-record/object-sort-dropdown/types/Sort';
import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition'; import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition';
import { isDefined } from 'twenty-shared'; import { isDefined } from 'twenty-shared';
import { RecordSort } from '@/object-record/record-sort/types/RecordSort';
import { ViewSort } from '../types/ViewSort'; import { ViewSort } from '../types/ViewSort';
export const mapViewSortsToSorts = ( export const mapViewSortsToSorts = (
viewSorts: ViewSort[], viewSorts: ViewSort[],
availableSortDefinitions: SortDefinition[], availableSortDefinitions: SortDefinition[],
): Sort[] => { ): RecordSort[] => {
return viewSorts return viewSorts
.map((viewSort) => { .map((viewSort) => {
const availableSortDefinition = availableSortDefinitions.find( const availableSortDefinition = availableSortDefinitions.find(
@ -16,7 +16,9 @@ export const mapViewSortsToSorts = (
); );
if (!availableSortDefinition) return null; if (!availableSortDefinition) return null;
return { return {
id: viewSort.id,
fieldMetadataId: viewSort.fieldMetadataId, fieldMetadataId: viewSort.fieldMetadataId,
direction: viewSort.direction, direction: viewSort.direction,
definition: availableSortDefinition, definition: availableSortDefinition,