Files
twenty/packages/twenty-front/src/modules/error-handler/components/PromiseRejectionEffect.tsx
T
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

101 lines
2.7 KiB
TypeScript

import { useCallback, useEffect } from 'react';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import {
CombinedGraphQLErrors,
CombinedProtocolErrors,
LinkError,
LocalStateError,
ServerError,
ServerParseError,
UnconventionalError,
} from '@apollo/client/errors';
import { isDefined, isNonEmptyString, type CustomError } from 'twenty-shared/utils';
const isApolloError = (error: unknown): boolean =>
CombinedGraphQLErrors.is(error) ||
CombinedProtocolErrors.is(error) ||
LinkError.is(error) ||
LocalStateError.is(error) ||
ServerError.is(error) ||
ServerParseError.is(error) ||
UnconventionalError.is(error);
const hasErrorCode = (
error: CustomError | any,
): error is CustomError & { code: string } => {
return 'code' in error && isDefined(error.code);
};
export const PromiseRejectionEffect = () => {
const { enqueueErrorSnackBar } = useSnackBar();
const handlePromiseRejection = useCallback(
async (event: PromiseRejectionEvent) => {
const error = event.reason;
if (isApolloError(error)) {
enqueueErrorSnackBar({
apolloError: error,
});
return; // already handled by apolloLink
}
const isAbortError =
error?.networkError?.name === 'AbortError' ||
error?.name === 'AbortError';
if (isAbortError) {
return;
}
enqueueErrorSnackBar(
error instanceof Error ? { message: error.message } : {},
);
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.warn('Failed to capture exception with Sentry:', sentryError);
}
},
[enqueueErrorSnackBar],
);
useEffect(() => {
window.addEventListener('unhandledrejection', handlePromiseRejection);
return () => {
window.removeEventListener('unhandledrejection', handlePromiseRejection);
};
}, [handlePromiseRejection]);
return <></>;
};