Fix bugs tied to jotai migration (#18227)

Fixing a few bugs:
- CommandMenu not reactive
- Filtering on index view infinite loop
This commit is contained in:
Charles Bochet
2026-02-25 17:11:12 +01:00
committed by GitHub
parent 001c2097a3
commit 1fa55bfd02
31 changed files with 343 additions and 238 deletions
@@ -14,6 +14,7 @@ jest.mock('@/object-record/utils/generateAggregateQuery');
const fields = [
{
id: '20202020-fed9-4ce5-9502-02a8efaf46e1',
universalIdentifier: '20202020-fed9-4ce5-9502-02a8efaf46e1',
name: 'amount',
label: 'Amount',
type: FieldMetadataType.NUMBER,
@@ -24,6 +25,7 @@ const fields = [
} as FieldMetadataItem,
{
id: '20202020-dd4a-4ea4-bb7b-1c7300491b65',
universalIdentifier: '20202020-dd4a-4ea4-bb7b-1c7300491b65',
name: 'name',
label: 'Name',
type: FieldMetadataType.TEXT,
@@ -38,6 +40,7 @@ const mockObjectMetadataItem: ObjectMetadataItem = {
nameSingular: 'company',
namePlural: 'companies',
id: 'test-id',
universalIdentifier: 'test-id',
labelSingular: 'Company',
labelPlural: 'Companies',
isCustom: false,
@@ -16,6 +16,7 @@ const fields = [
updatedAt: '2021-01-01',
createdAt: '2021-01-01',
id: '20202020-18b3-4099-86e3-c46b2d5d42f2',
universalIdentifier: '20202020-18b3-4099-86e3-c46b2d5d42f2',
type: FieldMetadataType.POSITION,
label: 'label',
},
@@ -23,6 +24,7 @@ const fields = [
const objectMetadataItemWithPositionField: ObjectMetadataItem = {
id: 'object1',
universalIdentifier: 'object1',
fields,
readableFields: fields,
updatableFields: fields,
@@ -50,6 +52,7 @@ const getMockFieldMetadataItem = (
overrides: PartialFieldMetadaItemWithRequiredId,
): FieldMetadataItem => ({
name: 'name',
universalIdentifier: overrides.id,
updatedAt: '2021-01-01',
createdAt: '2021-01-01',
type: FieldMetadataType.TEXT,
@@ -170,9 +173,11 @@ describe('turnSortsIntoOrderBy', () => {
describe('relation field sorting', () => {
const companyObjectMetadataItem: ObjectMetadataItem = {
id: 'company-object-id',
universalIdentifier: 'company-object-id',
fields: [
{
id: 'company-name-field-id',
universalIdentifier: 'company-name-field-id',
name: 'name',
type: FieldMetadataType.TEXT,
label: 'Name',
@@ -203,9 +208,11 @@ describe('turnSortsIntoOrderBy', () => {
const personObjectMetadataItem: ObjectMetadataItem = {
id: 'person-object-id',
universalIdentifier: 'person-object-id',
fields: [
{
id: 'company-relation-field-id',
universalIdentifier: 'company-relation-field-id',
name: 'company',
type: FieldMetadataType.RELATION,
label: 'Company',
@@ -221,6 +228,7 @@ describe('turnSortsIntoOrderBy', () => {
} as unknown as FieldMetadataItem,
{
id: 'position-field-id',
universalIdentifier: 'position-field-id',
name: 'position',
type: FieldMetadataType.POSITION,
label: 'Position',
@@ -2,20 +2,16 @@ import { useEffect } from 'react';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
export const RecordBoardSelectRecordsEffect = () => {
const { recordIndexId } = useRecordIndexContextOrThrow();
const selectedRecordIds = useAtomComponentSelectorValue(
recordBoardSelectedRecordIdsComponentSelector,
);
const setContextStoreTargetedRecords = useSetAtomComponentState(
contextStoreTargetedRecordsRuleComponentState,
recordIndexId,
);
useEffect(() => {
@@ -11,16 +11,19 @@ describe('buildRecordGqlFieldsAggregateForView', () => {
const fields = [
{
id: MOCK_FIELD_ID,
universalIdentifier: MOCK_FIELD_ID,
name: 'amount',
type: FieldMetadataType.NUMBER,
} as FieldMetadataItem,
{
id: '06b33746-5293-4d07-9f7f-ebf5ad396064',
universalIdentifier: '06b33746-5293-4d07-9f7f-ebf5ad396064',
name: 'name',
type: FieldMetadataType.TEXT,
} as FieldMetadataItem,
{
id: 'e46b9ba4-144b-4d10-a092-03a7521c8aa0',
universalIdentifier: 'e46b9ba4-144b-4d10-a092-03a7521c8aa0',
name: 'createdAt',
type: FieldMetadataType.DATE_TIME,
} as FieldMetadataItem,
@@ -28,6 +31,7 @@ describe('buildRecordGqlFieldsAggregateForView', () => {
const mockObjectMetadata: ObjectMetadataItem = {
id: '123',
universalIdentifier: '123',
nameSingular: 'opportunity',
namePlural: 'opportunities',
labelSingular: 'Opportunity',
@@ -40,7 +40,6 @@ export const RecordIndexContainerGater = () => {
const { indexIdentifierUrl } = useHandleIndexIdentifierClick({
objectMetadataItem,
recordIndexId,
});
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
@@ -1,23 +1,32 @@
import { useEffect } from 'react';
import { useEffect, useMemo } from 'react';
import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState';
import { contextStoreFilterGroupsComponentState } from '@/context-store/states/contextStoreFilterGroupsComponentState';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import {
contextStoreTargetedRecordsRuleComponentState,
type ContextStoreTargetedRecordsRule,
} from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { type RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { hasUserSelectedAllRowsComponentState } from '@/object-record/record-table/record-table-row/states/hasUserSelectedAllRowsFamilyState';
import { selectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsComponentSelector';
import { unselectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useAtomComponentSelectorCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorCallbackState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { atom, useStore } from 'jotai';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const RecordIndexFiltersToContextStoreEffect = () => {
const { recordIndexId } = useRecordIndexContextOrThrow();
const store = useStore();
const recordIndexFilters = useAtomComponentStateValue(
currentRecordFiltersComponentState,
recordIndexId,
@@ -28,92 +37,158 @@ export const RecordIndexFiltersToContextStoreEffect = () => {
recordIndexId,
);
const setContextStoreTargetedRecords = useSetAtomComponentState(
contextStoreTargetedRecordsRuleComponentState,
recordIndexId,
);
const hasUserSelectedAllRows = useAtomComponentStateValue(
hasUserSelectedAllRowsComponentState,
recordIndexId,
);
const selectedRowIds = useAtomComponentSelectorValue(
selectedRowIdsComponentSelector,
recordIndexId,
);
const unselectedRowIds = useAtomComponentSelectorValue(
unselectedRowIdsComponentSelector,
recordIndexId,
);
useEffect(() => {
if (hasUserSelectedAllRows) {
setContextStoreTargetedRecords({
mode: 'exclusion',
excludedRecordIds: unselectedRowIds,
});
} else {
setContextStoreTargetedRecords({
mode: 'selection',
selectedRecordIds: selectedRowIds,
});
}
return () => {
setContextStoreTargetedRecords({
mode: 'selection',
selectedRecordIds: [],
});
};
}, [
hasUserSelectedAllRows,
selectedRowIds,
setContextStoreTargetedRecords,
unselectedRowIds,
]);
const setContextStoreFilters = useSetAtomComponentState(
contextStoreFiltersComponentState,
recordIndexId,
);
const setContextStoreFilterGroups = useSetAtomComponentState(
contextStoreFilterGroupsComponentState,
recordIndexId,
);
useEffect(() => {
setContextStoreFilters(recordIndexFilters);
setContextStoreFilterGroups(recordIndexFilterGroups);
return () => {
setContextStoreFilters([]);
};
}, [
recordIndexFilterGroups,
recordIndexFilters,
setContextStoreFilterGroups,
setContextStoreFilters,
]);
const setContextStoreAnyFieldFilterValue = useSetAtomComponentState(
contextStoreAnyFieldFilterValueComponentState,
recordIndexId,
);
const anyFieldFilterValue = useAtomComponentStateValue(
anyFieldFilterValueComponentState,
recordIndexId,
);
const hasUserSelectedAllRowsAtom = useAtomComponentStateCallbackState(
hasUserSelectedAllRowsComponentState,
recordIndexId,
);
const selectedRowIdsAtom = useAtomComponentSelectorCallbackState(
selectedRowIdsComponentSelector,
recordIndexId,
);
const unselectedRowIdsAtom = useAtomComponentSelectorCallbackState(
unselectedRowIdsComponentSelector,
recordIndexId,
);
const contextStoreTargetedRecordsRuleAtom =
useAtomComponentStateCallbackState(
contextStoreTargetedRecordsRuleComponentState,
);
const contextStoreFiltersAtom = useAtomComponentStateCallbackState(
contextStoreFiltersComponentState,
);
const contextStoreFilterGroupsAtom = useAtomComponentStateCallbackState(
contextStoreFilterGroupsComponentState,
);
const contextStoreAnyFieldFilterValueAtom =
useAtomComponentStateCallbackState(
contextStoreAnyFieldFilterValueComponentState,
);
const syncWriteAtom = useMemo(
() =>
atom(
null,
(
get,
set,
payload: {
filters: RecordFilter[];
filterGroups: RecordFilterGroup[];
anyFieldFilterValue: string;
},
) => {
const hasUserSelectedAllRows = get(hasUserSelectedAllRowsAtom);
let newRule: ContextStoreTargetedRecordsRule;
if (hasUserSelectedAllRows) {
const unselectedRowIds = get(unselectedRowIdsAtom);
newRule = {
mode: 'exclusion',
excludedRecordIds: unselectedRowIds,
};
} else {
const selectedRowIds = get(selectedRowIdsAtom);
newRule = {
mode: 'selection',
selectedRecordIds: selectedRowIds,
};
}
const currentRule = get(contextStoreTargetedRecordsRuleAtom);
if (!isDeeplyEqual(currentRule, newRule)) {
set(contextStoreTargetedRecordsRuleAtom, newRule);
}
const currentFilters = get(contextStoreFiltersAtom);
if (!isDeeplyEqual(currentFilters, payload.filters)) {
set(contextStoreFiltersAtom, payload.filters);
}
const currentFilterGroups = get(contextStoreFilterGroupsAtom);
if (!isDeeplyEqual(currentFilterGroups, payload.filterGroups)) {
set(contextStoreFilterGroupsAtom, payload.filterGroups);
}
const currentAnyFieldFilter = get(
contextStoreAnyFieldFilterValueAtom,
);
if (currentAnyFieldFilter !== payload.anyFieldFilterValue) {
set(
contextStoreAnyFieldFilterValueAtom,
payload.anyFieldFilterValue,
);
}
},
),
[
hasUserSelectedAllRowsAtom,
selectedRowIdsAtom,
unselectedRowIdsAtom,
contextStoreTargetedRecordsRuleAtom,
contextStoreFiltersAtom,
contextStoreFilterGroupsAtom,
contextStoreAnyFieldFilterValueAtom,
],
);
const resetWriteAtom = useMemo(
() =>
atom(null, (get, set) => {
const currentRule = get(contextStoreTargetedRecordsRuleAtom);
const resetRule: ContextStoreTargetedRecordsRule = {
mode: 'selection',
selectedRecordIds: [],
};
if (!isDeeplyEqual(currentRule, resetRule)) {
set(contextStoreTargetedRecordsRuleAtom, resetRule);
}
const currentFilters = get(contextStoreFiltersAtom);
if (!isDeeplyEqual(currentFilters, [])) {
set(contextStoreFiltersAtom, []);
}
const currentAnyFieldFilter = get(contextStoreAnyFieldFilterValueAtom);
if (currentAnyFieldFilter !== '') {
set(contextStoreAnyFieldFilterValueAtom, '');
}
}),
[
contextStoreTargetedRecordsRuleAtom,
contextStoreFiltersAtom,
contextStoreAnyFieldFilterValueAtom,
],
);
useEffect(() => {
setContextStoreAnyFieldFilterValue(anyFieldFilterValue);
store.set(syncWriteAtom, {
filters: recordIndexFilters,
filterGroups: recordIndexFilterGroups,
anyFieldFilterValue,
});
return () => {
setContextStoreAnyFieldFilterValue('');
store.set(resetWriteAtom);
};
}, [anyFieldFilterValue, setContextStoreAnyFieldFilterValue]);
}, [
recordIndexFilters,
recordIndexFilterGroups,
anyFieldFilterValue,
store,
syncWriteAtom,
resetWriteAtom,
]);
return <></>;
};
@@ -70,22 +70,18 @@ export const useRecordIndexLazyFetchRecords = ({
const contextStoreTargetedRecordsRule = useAtomComponentStateValue(
contextStoreTargetedRecordsRuleComponentState,
recordIndexId,
);
const contextStoreFilters = useAtomComponentStateValue(
contextStoreFiltersComponentState,
recordIndexId,
);
const contextStoreFilterGroups = useAtomComponentStateValue(
contextStoreFilterGroupsComponentState,
recordIndexId,
);
const contextStoreAnyFieldFilterValue = useAtomComponentStateValue(
contextStoreAnyFieldFilterValueComponentState,
recordIndexId,
);
const { filterValueDependencies } = useFilterValueDependencies();
@@ -6,14 +6,11 @@ import { getAppPath } from 'twenty-shared/utils';
export const useHandleIndexIdentifierClick = ({
objectMetadataItem,
recordIndexId,
}: {
recordIndexId: string;
objectMetadataItem: ObjectMetadataItem;
}) => {
const currentViewId = useAtomComponentStateValue(
contextStoreCurrentViewIdComponentState,
recordIndexId,
);
const indexIdentifierUrl = (recordId: string) => {
@@ -23,6 +23,7 @@ type RecordInlineCellAnchoredPortalProps = {
fieldMetadataItem: Pick<
FieldMetadataItem,
| 'id'
| 'universalIdentifier'
| 'name'
| 'type'
| 'createdAt'
@@ -1,19 +1,24 @@
import { useCallback } from 'react';
import { useStore } from 'jotai';
import { useSelectAllRows } from '@/object-record/record-table/hooks/internal/useSelectAllRows';
import { hasUserSelectedAllRowsComponentState } from '@/object-record/record-table/record-table-row/states/hasUserSelectedAllRowsFamilyState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
export const useReapplyRowSelection = () => {
const { selectAllRows } = useSelectAllRows();
const hasUserSelectedAllRows = useAtomComponentStateValue(
const hasUserSelectedAllRowsAtom = useAtomComponentStateCallbackState(
hasUserSelectedAllRowsComponentState,
);
const reapplyRowSelection = () => {
if (hasUserSelectedAllRows) {
const store = useStore();
const reapplyRowSelection = useCallback(() => {
if (store.get(hasUserSelectedAllRowsAtom)) {
selectAllRows();
}
};
}, [store, hasUserSelectedAllRowsAtom, selectAllRows]);
return {
reapplyRowSelection,
@@ -1,3 +1,5 @@
import { useCallback } from 'react';
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
import { useUnfocusRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useUnfocusRecordTableCell';
import { recordTableHoverPositionComponentState } from '@/object-record/record-table/states/recordTableHoverPositionComponentState';
@@ -12,11 +14,15 @@ export const useResetTableFocuses = (recordTableId: string) => {
recordTableId,
);
const resetTableFocuses = () => {
const resetTableFocuses = useCallback(() => {
unfocusRecordTableCell();
unfocusRecordTableRow();
setRecordTableHoverPosition(null);
};
}, [
unfocusRecordTableCell,
unfocusRecordTableRow,
setRecordTableHoverPosition,
]);
return {
resetTableFocuses,
@@ -41,6 +41,7 @@ describe('useBuildSpreadSheetImportFields', () => {
overrides: Partial<FieldMetadataItem> = {},
): FieldMetadataItem => ({
id: 'test-field-id',
universalIdentifier: 'test-field-id',
name: 'testField',
label: 'Test Field',
type: FieldMetadataType.TEXT,
@@ -59,6 +60,7 @@ describe('useBuildSpreadSheetImportFields', () => {
): ObjectMetadataItem =>
({
id: 'test-object-id',
universalIdentifier: 'test-object-id',
nameSingular: 'testObject',
namePlural: 'testObjects',
labelSingular: 'Test Object',
@@ -11,6 +11,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
const fields: FieldMetadataItem[] = [
{
id: '3',
universalIdentifier: '3',
name: 'booleanField',
label: 'Boolean Field',
type: FieldMetadataType.BOOLEAN,
@@ -25,6 +26,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
},
{
id: '4',
universalIdentifier: '4',
name: 'numberField',
label: 'Number Field',
type: FieldMetadataType.NUMBER,
@@ -39,6 +41,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
},
{
id: '5',
universalIdentifier: '5',
name: 'multiSelectField',
label: 'Multi-Select Field',
type: FieldMetadataType.MULTI_SELECT,
@@ -76,6 +79,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
},
{
id: '6',
universalIdentifier: '6',
name: 'relationField',
label: 'Relation Field',
type: FieldMetadataType.RELATION,
@@ -93,6 +97,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
},
{
id: '7',
universalIdentifier: '7',
name: 'fullNameField',
label: 'Full Name Field',
type: FieldMetadataType.FULL_NAME,
@@ -107,6 +112,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
},
{
id: '8',
universalIdentifier: '8',
name: 'currencyField',
label: 'Currency Field',
type: FieldMetadataType.CURRENCY,
@@ -121,6 +127,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
},
{
id: '9',
universalIdentifier: '9',
name: 'addressField',
label: 'Address Field',
type: FieldMetadataType.ADDRESS,
@@ -135,6 +142,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
},
{
id: '10',
universalIdentifier: '10',
name: 'selectField',
label: 'Select Field',
type: FieldMetadataType.SELECT,
@@ -165,6 +173,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
},
{
id: '11',
universalIdentifier: '11',
name: 'arrayField',
label: 'Array Field',
type: FieldMetadataType.ARRAY,
@@ -179,6 +188,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
},
{
id: '12',
universalIdentifier: '12',
name: 'jsonField',
label: 'JSON Field',
type: FieldMetadataType.RAW_JSON,
@@ -193,6 +203,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
},
{
id: '13',
universalIdentifier: '13',
name: 'phoneField',
label: 'Phone Field',
type: FieldMetadataType.PHONES,
@@ -207,6 +218,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
},
{
id: '14',
universalIdentifier: '14',
name: 'linksField',
label: 'Links Field',
type: FieldMetadataType.LINKS,
@@ -221,6 +233,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
},
{
id: '15',
universalIdentifier: '15',
name: 'createdBy',
label: 'Created by',
type: FieldMetadataType.ACTOR,
@@ -235,6 +248,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
},
{
id: '16',
universalIdentifier: '16',
name: 'richTextField',
label: 'Rich Text Field',
type: FieldMetadataType.RICH_TEXT_V2,
@@ -249,6 +263,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
},
{
id: '17',
universalIdentifier: '17',
name: 'dateField',
label: 'Date Field',
type: FieldMetadataType.DATE,
@@ -263,6 +278,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
},
{
id: '18',
universalIdentifier: '18',
name: 'dateTimeField',
label: 'Date Time Field',
type: FieldMetadataType.DATE_TIME,
@@ -277,6 +293,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
},
{
id: '19',
universalIdentifier: '19',
name: 'ratingField',
label: 'Rating Field',
type: FieldMetadataType.RATING,
@@ -291,6 +308,7 @@ describe('buildRecordFromImportedStructuredRow', () => {
},
{
id: '20',
universalIdentifier: '20',
name: 'emailField',
label: 'Email Field',
type: FieldMetadataType.EMAILS,
@@ -7,6 +7,7 @@ describe('generateAggregateQuery', () => {
nameSingular: 'company',
namePlural: 'companies',
id: 'test-id',
universalIdentifier: 'test-id',
labelSingular: 'Company',
labelPlural: 'Companies',
labelIdentifierFieldMetadataId: '20202020-72ba-4e11-a36d-e17b544541e1',
@@ -49,6 +50,7 @@ describe('generateAggregateQuery', () => {
nameSingular: 'person',
namePlural: 'people',
id: 'test-id',
universalIdentifier: 'test-id',
labelSingular: 'Person',
labelPlural: 'People',
labelIdentifierFieldMetadataId: '20202020-72ba-4e11-a36d-e17b544541e1',