Fix merge with null value + reset data virtualization before init load (#19633)
**Merge records fix:** selectPriorityFieldValue throws when merging records if the priority record has no value for a field (e.g., null/empty) but 2+ other records do. The recordsWithValues array is pre-filtered to only records with non-empty values, so the priority record isn't in the list. The fix: instead of throwing, fall back to null since this is the priority record actual value **Duplicated IDs fix** https://github.com/user-attachments/assets/bd6d7d08-d079-49a5-aad4-740b59a3c246 When applying a filter that reduces the record count, the virtualized table's record ID array keeps stale entries from the previous larger result set. loadRecordsToVirtualRows clones the old array (e.g., 60 entries) and only overwrites the first N positions (e.g., 9) with the new filtered results, leaving positions 9-59 with old IDs. If any old ID matches a new one, it appears twice in the selection, causing "-> 2 selected" for a single click and a duplicate ID in the merge mutation payload. The fix: clear the record IDs array in useTriggerInitialRecordTableDataLoad before repopulating it with fresh data. --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
co-authored by
Charles Bochet
parent
87f5c0083f
commit
353d1e89d5
+128
@@ -0,0 +1,128 @@
|
||||
import { defaultMergeFieldValue } from 'src/engine/api/graphql/graphql-query-runner/utils/default-merge-field-value.util';
|
||||
|
||||
describe('defaultMergeFieldValue', () => {
|
||||
it('should return priority record value when available', () => {
|
||||
const recordsWithValues = [
|
||||
{
|
||||
recordId: '1',
|
||||
value: 'priority value',
|
||||
},
|
||||
{
|
||||
recordId: '2',
|
||||
value: 'other value',
|
||||
},
|
||||
];
|
||||
|
||||
const result = defaultMergeFieldValue(recordsWithValues, '1');
|
||||
|
||||
expect(result).toBe('priority value');
|
||||
});
|
||||
|
||||
it('should fall back to first record with value when priority record not found', () => {
|
||||
const recordsWithValues = [
|
||||
{
|
||||
recordId: '2',
|
||||
value: 'first value',
|
||||
},
|
||||
{
|
||||
recordId: '3',
|
||||
value: 'second value',
|
||||
},
|
||||
];
|
||||
|
||||
const result = defaultMergeFieldValue(recordsWithValues, '1');
|
||||
|
||||
expect(result).toBe('first value');
|
||||
});
|
||||
|
||||
it('should fall back to other record when priority record has null value', () => {
|
||||
const recordsWithValues = [
|
||||
{
|
||||
recordId: '1',
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
recordId: '2',
|
||||
value: 'fallback value',
|
||||
},
|
||||
];
|
||||
|
||||
const result = defaultMergeFieldValue(recordsWithValues, '1');
|
||||
|
||||
expect(result).toBe('fallback value');
|
||||
});
|
||||
|
||||
it('should fall back to other record when priority record has empty string', () => {
|
||||
const recordsWithValues = [
|
||||
{
|
||||
recordId: '1',
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
recordId: '2',
|
||||
value: 'fallback value',
|
||||
},
|
||||
];
|
||||
|
||||
const result = defaultMergeFieldValue(recordsWithValues, '1');
|
||||
|
||||
expect(result).toBe('fallback value');
|
||||
});
|
||||
|
||||
it('should fall back to other record when priority record has undefined value', () => {
|
||||
const recordsWithValues = [
|
||||
{
|
||||
recordId: '1',
|
||||
value: undefined,
|
||||
},
|
||||
{
|
||||
recordId: '2',
|
||||
value: 'fallback value',
|
||||
},
|
||||
];
|
||||
|
||||
const result = defaultMergeFieldValue(recordsWithValues, '1');
|
||||
|
||||
expect(result).toBe('fallback value');
|
||||
});
|
||||
|
||||
it('should return null when no records exist', () => {
|
||||
const result = defaultMergeFieldValue([], '1');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when no record has a value', () => {
|
||||
const recordsWithValues = [
|
||||
{
|
||||
recordId: '1',
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
recordId: '2',
|
||||
value: '',
|
||||
},
|
||||
];
|
||||
|
||||
const result = defaultMergeFieldValue(recordsWithValues, '1');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle complex object values', () => {
|
||||
const recordsWithValues = [
|
||||
{
|
||||
recordId: '1',
|
||||
value: { name: 'priority object', id: 1 },
|
||||
},
|
||||
{
|
||||
recordId: '2',
|
||||
value: { name: 'fallback object', id: 2 },
|
||||
},
|
||||
];
|
||||
|
||||
const result = defaultMergeFieldValue(recordsWithValues, '1');
|
||||
|
||||
expect(result).toEqual({ name: 'priority object', id: 1 });
|
||||
});
|
||||
});
|
||||
+9
-10
@@ -21,20 +21,19 @@ describe('mergeFieldValues', () => {
|
||||
expect(result).toBe('value2');
|
||||
});
|
||||
|
||||
it('should throw error when priority record is not found', () => {
|
||||
const recordsWithoutPriorityValue = [
|
||||
it('should fall back to first record with value when priority record is not found', () => {
|
||||
const recordsWithValues = [
|
||||
{ value: 'value1', recordId: 'record1' },
|
||||
{ value: null, recordId: PRIORITY_RECORD_ID },
|
||||
{ value: 'value3', recordId: 'record3' },
|
||||
];
|
||||
|
||||
expect(() =>
|
||||
mergeFieldValues(
|
||||
FieldMetadataType.TEXT,
|
||||
recordsWithoutPriorityValue,
|
||||
'non-existent-id',
|
||||
),
|
||||
).toThrow('Priority record with ID non-existent-id not found');
|
||||
const result = mergeFieldValues(
|
||||
FieldMetadataType.TEXT,
|
||||
recordsWithValues,
|
||||
'non-existent-id',
|
||||
);
|
||||
|
||||
expect(result).toBe('value1');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
-111
@@ -1,111 +0,0 @@
|
||||
import { selectPriorityFieldValue } from 'src/engine/api/graphql/graphql-query-runner/utils/select-priority-field-value.util';
|
||||
|
||||
describe('selectPriorityFieldValue', () => {
|
||||
it('should return priority record value when available', () => {
|
||||
const recordsWithValues = [
|
||||
{
|
||||
recordId: '1',
|
||||
value: 'priority value',
|
||||
},
|
||||
{
|
||||
recordId: '2',
|
||||
value: 'other value',
|
||||
},
|
||||
];
|
||||
|
||||
const result = selectPriorityFieldValue(recordsWithValues, '1');
|
||||
|
||||
expect(result).toBe('priority value');
|
||||
});
|
||||
|
||||
it('should throw error when priority record not found', () => {
|
||||
const recordsWithValues = [
|
||||
{
|
||||
recordId: '2',
|
||||
value: 'first value',
|
||||
},
|
||||
{
|
||||
recordId: '3',
|
||||
value: 'second value',
|
||||
},
|
||||
];
|
||||
|
||||
expect(() => selectPriorityFieldValue(recordsWithValues, '1')).toThrow(
|
||||
'Priority record with ID 1 not found in merge candidates',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return null when priority record has no value', () => {
|
||||
const recordsWithValues = [
|
||||
{
|
||||
recordId: '1',
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
recordId: '2',
|
||||
value: 'fallback value',
|
||||
},
|
||||
];
|
||||
|
||||
const result = selectPriorityFieldValue(recordsWithValues, '1');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when priority record has empty string', () => {
|
||||
const recordsWithValues = [
|
||||
{
|
||||
recordId: '1',
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
recordId: '2',
|
||||
value: 'fallback value',
|
||||
},
|
||||
];
|
||||
|
||||
const result = selectPriorityFieldValue(recordsWithValues, '1');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when priority record has undefined value', () => {
|
||||
const recordsWithValues = [
|
||||
{
|
||||
recordId: '1',
|
||||
value: undefined,
|
||||
},
|
||||
{
|
||||
recordId: '2',
|
||||
value: 'fallback value',
|
||||
},
|
||||
];
|
||||
|
||||
const result = selectPriorityFieldValue(recordsWithValues, '1');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should throw error when no records exist', () => {
|
||||
expect(() => selectPriorityFieldValue([], '1')).toThrow(
|
||||
'Priority record with ID 1 not found in merge candidates',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle complex object values', () => {
|
||||
const recordsWithValues = [
|
||||
{
|
||||
recordId: '1',
|
||||
value: { name: 'priority object', id: 1 },
|
||||
},
|
||||
{
|
||||
recordId: '2',
|
||||
value: { name: 'fallback object', id: 2 },
|
||||
},
|
||||
];
|
||||
|
||||
const result = selectPriorityFieldValue(recordsWithValues, '1');
|
||||
|
||||
expect(result).toEqual({ name: 'priority object', id: 1 });
|
||||
});
|
||||
});
|
||||
+9
-7
@@ -2,7 +2,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { hasRecordFieldValue } from 'src/engine/api/graphql/graphql-query-runner/utils/has-record-field-value.util';
|
||||
|
||||
export const selectPriorityFieldValue = <T>(
|
||||
export const defaultMergeFieldValue = <T>(
|
||||
recordsWithValues: { value: T; recordId: string }[],
|
||||
priorityRecordId: string,
|
||||
): T | null => {
|
||||
@@ -10,14 +10,16 @@ export const selectPriorityFieldValue = <T>(
|
||||
(record) => record.recordId === priorityRecordId,
|
||||
);
|
||||
|
||||
if (!isDefined(priorityRecord)) {
|
||||
throw new Error(
|
||||
`Priority record with ID ${priorityRecordId} not found in merge candidates`,
|
||||
);
|
||||
if (isDefined(priorityRecord) && hasRecordFieldValue(priorityRecord.value)) {
|
||||
return priorityRecord.value;
|
||||
}
|
||||
|
||||
if (hasRecordFieldValue(priorityRecord.value)) {
|
||||
return priorityRecord.value;
|
||||
const fallbackRecord = recordsWithValues.find((record) =>
|
||||
hasRecordFieldValue(record.value),
|
||||
);
|
||||
|
||||
if (fallbackRecord) {
|
||||
return fallbackRecord.value;
|
||||
}
|
||||
|
||||
return null;
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
FieldMetadataType,
|
||||
type RelationType,
|
||||
type EmailsMetadata,
|
||||
type LinksMetadata,
|
||||
type PhonesMetadata,
|
||||
type RelationType,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { mergeArrayFieldValues } from './merge-array-field-values.util';
|
||||
@@ -11,7 +11,7 @@ import { mergeEmailsFieldValues } from './merge-emails-field-values.util';
|
||||
import { mergeLinksFieldValues } from './merge-links-field-values.util';
|
||||
import { mergePhonesFieldValues } from './merge-phones-field-values.util';
|
||||
import { mergeRelationFieldValuesForDryRunRecord } from './merge-relation-field-values-for-dry-run-record.util';
|
||||
import { selectPriorityFieldValue } from './select-priority-field-value.util';
|
||||
import { defaultMergeFieldValue } from './default-merge-field-value.util';
|
||||
|
||||
export const mergeFieldValues = (
|
||||
fieldType: FieldMetadataType,
|
||||
@@ -34,7 +34,7 @@ export const mergeFieldValues = (
|
||||
);
|
||||
}
|
||||
|
||||
return selectPriorityFieldValue(recordsWithValues, priorityRecordId);
|
||||
return defaultMergeFieldValue(recordsWithValues, priorityRecordId);
|
||||
|
||||
case FieldMetadataType.EMAILS:
|
||||
return mergeEmailsFieldValues(
|
||||
@@ -55,6 +55,6 @@ export const mergeFieldValues = (
|
||||
);
|
||||
|
||||
default:
|
||||
return selectPriorityFieldValue(recordsWithValues, priorityRecordId);
|
||||
return defaultMergeFieldValue(recordsWithValues, priorityRecordId);
|
||||
}
|
||||
};
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { type ObjectRecord, RelationType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { selectPriorityFieldValue } from 'src/engine/api/graphql/graphql-query-runner/utils/select-priority-field-value.util';
|
||||
import { defaultMergeFieldValue } from 'src/engine/api/graphql/graphql-query-runner/utils/default-merge-field-value.util';
|
||||
|
||||
export const mergeRelationFieldValuesForDryRunRecord = (
|
||||
recordsWithValues: { value: unknown; recordId: string }[],
|
||||
@@ -12,7 +12,7 @@ export const mergeRelationFieldValuesForDryRunRecord = (
|
||||
return mergeOneToManyRelationArrays(recordsWithValues);
|
||||
}
|
||||
|
||||
return selectPriorityFieldValue(
|
||||
return defaultMergeFieldValue(
|
||||
recordsWithValues as { value: ObjectRecord | null; recordId: string }[],
|
||||
priorityRecordId,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user