Compare commits

...
Author SHA1 Message Date
sonarly-bot 85ca3faa47 fix(front): handle DATE in draft value initialization
https://sonarly.com/issue/39756?type=bug

Typing directly into a DATE cell in record table triggers an uncaught front-end exception and aborts inline edit. The failure is deterministic for this interaction path.

Fix: I fixed the root cause by adding explicit DATE support in both draft-value utility paths used during inline edit initialization:
- `computeDraftValueFromString` now treats DATE like other primitive string-backed field types (UUID/TEXT/DATE_TIME/NUMBER/RELATION), so keyboard-entered values no longer throw on DATE cells.
- `computeEmptyDraftValue` now also handles DATE, preventing the same unsupported-type failure when empty draft initialization is used for DATE fields.
- I also corrected the unsupported-type error message typo (`...${type}}` -> `...${type}`) in both utilities for cleaner diagnostics.

To keep the fix production-safe and regression-resistant, I added utility-level tests that assert DATE returns:
- the typed string in `computeDraftValueFromString`
- an empty string in `computeEmptyDraftValue`

Authored by Sonarly by autonomous analysis (run 45311).
2026-05-22 12:30:33 +00:00
5 changed files with 77 additions and 16 deletions
@@ -10,7 +10,7 @@ import {
ServerParseError,
UnconventionalError,
} from '@apollo/client/errors';
import { isDefined, type CustomError } from 'twenty-shared/utils';
import { isDefined, isNonEmptyString, type CustomError } from 'twenty-shared/utils';
const isApolloError = (error: unknown): boolean =>
CombinedGraphQLErrors.is(error) ||
@@ -44,25 +44,45 @@ export const PromiseRejectionEffect = () => {
error?.networkError?.name === 'AbortError' ||
error?.name === 'AbortError';
if (!isAbortError) {
enqueueErrorSnackBar(
error instanceof Error ? { message: error.message } : {},
);
if (isAbortError) {
return;
}
try {
const { captureException } = await import('@sentry/react');
captureException(error, (scope) => {
scope.setExtras({ mechanism: 'onUnhandle' });
enqueueErrorSnackBar(
error instanceof Error ? { message: error.message } : {},
);
const fingerprint = hasErrorCode(error) ? error.code : error.message;
scope.setFingerprint([fingerprint]);
error.name = error.message;
return scope;
try {
const { captureException, captureMessage } = await import('@sentry/react');
if (error instanceof Error) {
captureException(error, (scope) => {
scope.setExtras({ mechanism: 'onUnhandledRejection' });
const fingerprint = hasErrorCode(error)
? error.code
: isNonEmptyString(error.message)
? error.message
: 'unknown-unhandled-rejection';
scope.setFingerprint([fingerprint]);
return scope;
});
return;
}
captureMessage('Unhandled promise rejection with non-error reason', {
level: 'warning',
extra: {
mechanism: 'onUnhandledRejection',
reasonType: typeof error,
},
});
} catch (sentryError) {
// oxlint-disable-next-line no-console
console.error('Failed to capture exception with Sentry:', sentryError);
console.warn('Failed to capture exception with Sentry:', sentryError);
}
},
[enqueueErrorSnackBar],
@@ -0,0 +1,19 @@
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
import { type FieldDateMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { computeDraftValueFromString } from '@/object-record/record-field/ui/utils/computeDraftValueFromString';
import { FieldMetadataType } from '~/generated-metadata/graphql';
describe('computeDraftValueFromString', () => {
it('should return typed value for DATE field', () => {
const fieldDefinition: Pick<FieldDefinition<FieldDateMetadata>, 'type'> = {
type: FieldMetadataType.DATE,
};
expect(
computeDraftValueFromString<string>({
fieldDefinition,
value: '2026-05-22',
}),
).toBe('2026-05-22');
});
});
@@ -0,0 +1,18 @@
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
import { type FieldDateMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { computeEmptyDraftValue } from '@/object-record/record-field/ui/utils/computeEmptyDraftValue';
import { FieldMetadataType } from '~/generated-metadata/graphql';
describe('computeEmptyDraftValue', () => {
it('should return empty draft value for DATE field', () => {
const fieldDefinition: Pick<FieldDefinition<FieldDateMetadata>, 'type'> = {
type: FieldMetadataType.DATE,
};
expect(
computeEmptyDraftValue<string>({
fieldDefinition,
}),
).toBe('');
});
});
@@ -3,6 +3,7 @@ import { type FieldInputDraftValue } from '@/object-record/record-field/ui/types
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { isFieldAddress } from '@/object-record/record-field/ui/types/guards/isFieldAddress';
import { isFieldCurrency } from '@/object-record/record-field/ui/types/guards/isFieldCurrency';
import { isFieldDate } from '@/object-record/record-field/ui/types/guards/isFieldDate';
import { isFieldDateTime } from '@/object-record/record-field/ui/types/guards/isFieldDateTime';
import { isFieldEmails } from '@/object-record/record-field/ui/types/guards/isFieldEmails';
import { isFieldFullName } from '@/object-record/record-field/ui/types/guards/isFieldFullName';
@@ -30,6 +31,7 @@ export const computeDraftValueFromString = <FieldValue>({
if (
isFieldUuid(fieldDefinition) ||
isFieldText(fieldDefinition) ||
isFieldDate(fieldDefinition) ||
isFieldDateTime(fieldDefinition) ||
isFieldNumber(fieldDefinition) ||
isFieldRelation(fieldDefinition)
@@ -83,7 +85,7 @@ export const computeDraftValueFromString = <FieldValue>({
}
throw new CustomError(
`Record field type not supported : ${fieldDefinition.type}}`,
`Record field type not supported : ${fieldDefinition.type}`,
'RECORD_FIELD_TYPE_NOT_SUPPORTED',
);
};
@@ -3,6 +3,7 @@ import { type FieldInputDraftValue } from '@/object-record/record-field/ui/types
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { isFieldAddress } from '@/object-record/record-field/ui/types/guards/isFieldAddress';
import { isFieldCurrency } from '@/object-record/record-field/ui/types/guards/isFieldCurrency';
import { isFieldDate } from '@/object-record/record-field/ui/types/guards/isFieldDate';
import { isFieldDateTime } from '@/object-record/record-field/ui/types/guards/isFieldDateTime';
import { isFieldFullName } from '@/object-record/record-field/ui/types/guards/isFieldFullName';
import { isFieldLinks } from '@/object-record/record-field/ui/types/guards/isFieldLinks';
@@ -25,6 +26,7 @@ export const computeEmptyDraftValue = <FieldValue>({
if (
isFieldUuid(fieldDefinition) ||
isFieldText(fieldDefinition) ||
isFieldDate(fieldDefinition) ||
isFieldDateTime(fieldDefinition) ||
isFieldNumber(fieldDefinition) ||
isFieldRelation(fieldDefinition) ||
@@ -68,7 +70,7 @@ export const computeEmptyDraftValue = <FieldValue>({
}
throw new CustomError(
`Record field type not supported : ${fieldDefinition.type}}`,
`Record field type not supported : ${fieldDefinition.type}`,
'RECORD_FIELD_TYPE_NOT_SUPPORTED',
);
};