Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 5bec5c33ff fix: respect standard object label overrides for non-English locales
https://sonarly.com/issue/14771?type=bug

When a user renames a standard object (e.g., "People" → "Contacts"), the custom name is only displayed when the UI language is English. For all other locales, the i18n-translated default name is shown instead, ignoring the user's customization.

Fix: Removed the `safeLocale === SOURCE_LOCALE` / `locale === SOURCE_LOCALE` guard from the direct override check in both `resolveObjectMetadataStandardOverride` and `resolveFieldMetadataStandardOverride`.

Previously, when a user renamed a standard object (e.g., "People" → "Contacts"), the rename was stored in `standardOverrides.labelSingular` but only returned when the locale was English (`SOURCE_LOCALE`). For any other locale, the code skipped the user's override and fell through to the i18n auto-translation of the original default name.

The fix makes the direct override check locale-independent, so user renames are respected regardless of the UI language. The priority order is now:

1. Per-locale translation overrides (`standardOverrides.translations[locale]`) — highest priority, allows per-language customization
2. Direct overrides (`standardOverrides[labelKey]`) — the user's rename, applies to ALL locales
3. i18n auto-translation of the default label — fallback

Also cleaned up the unused `SOURCE_LOCALE` import from the field metadata util, and updated both test files to assert the corrected behavior (direct overrides should be used for non-English locales).
2026-03-14 22:23:34 +00:00
Sonarly Claude Code 05cd68eb19 fix: catch SAX XML parse errors in PDF file-type detection during upload
https://sonarly.com/issue/14764?type=bug

Uploading certain PDFs via the `UploadFilesFieldFile` mutation crashes with an unhandled SAX XML parsing error ("Unclosed root tag") thrown by the `@file-type/pdf` plugin during file type detection.

Fix: Wrapped the `fileParser.fromBuffer(file)` call in `extractFileInfo` with a try-catch to handle errors thrown by the `@file-type/pdf` plugin's internal SAX XML parser.

**What changed:**

In `extract-file-info.utils.ts`: Instead of `const { ext, mime } = (await fileParser.fromBuffer(file)) ?? {}`, the result is now obtained inside a try-catch. When `fromBuffer` throws (e.g., "Unclosed root tag" from the SAX parser parsing malformed XMP metadata inside a PDF), the error is caught and the function falls through to extension-based detection — the same path it takes when `fromBuffer` returns `undefined`.

This converts an unhandled 500 error into the existing structured error handling:
- For non-detectable types (txt, csv, json, etc.): returns extension-based MIME type as before
- For detectable types (pdf, png, etc.): throws a `FileStorageException` with a user-friendly message ("content doesn't match extension") — which is properly handled by the GraphQL error filters

In `extract-file-info.utils.spec.ts`: Added two tests:
1. Verifies that when `fromBuffer` throws, the function gracefully falls back to extension-based detection for non-detectable types
2. Verifies that when `fromBuffer` throws for a detectable type (pdf), the function throws a `FileStorageException` (not the raw SAX error)

**Why not fix deeper:** The root bug is in `@file-type/pdf` (third-party library) which doesn't catch SAX parser errors for non-PDF XML content. The `file-type` core's `fromTokenizer` also only catches `EndOfStreamError` and `ParserHardLimitError`. Both are outside this codebase. This defensive catch at the call site is the appropriate pattern.
2026-03-14 21:34:50 +00:00
Sonarly Claude Code 366a721649 window.open crashes with invalid URL when Links field has empty primaryLinkUrl
https://sonarly.com/issue/14749?type=bug

Clicking the "open link" button on a record table cell with a Links field that has an empty `primaryLinkUrl` calls `window.open('https://', '_blank')`, which Chrome rejects as an invalid URL.

Fix: Added empty-value guards in `useGetSecondaryRecordTableCellButton.ts` for all three field types (Links, Emails, Phones). When the primary value (URL, email, or phone number) is empty, the hook now returns an empty array (no action buttons) instead of creating handlers that would call `window.open` with invalid URLs like `https://`, `mailto:`, or `tel:`.

The fix uses `isNonEmptyString` from `@sniptt/guards`, which is the standard guard used throughout the twenty-front codebase for this purpose.

**Links field (the reported crash):** `primaryLinkUrl` being empty/null caused `getAbsoluteUrl('')` to return `'https://'`, which Chrome rejects as invalid in `window.open`.

**Emails/Phones fields (preventive):** The same class of bug exists — empty email would produce `mailto:` and empty phone would produce `tel:`, both invalid URLs for `window.open`.
2026-03-14 20:24:57 +00:00
4 changed files with 11 additions and 19 deletions
@@ -262,7 +262,7 @@ describe('resolveFieldMetadataStandardOverride', () => {
).toBe('overridden-icon');
});
it('should not use direct override for non-SOURCE_LOCALE', () => {
it('should use direct override for non-SOURCE_LOCALE', () => {
const fieldMetadata = {
label: 'Standard Label',
description: 'Standard Description',
@@ -273,9 +273,6 @@ describe('resolveFieldMetadataStandardOverride', () => {
},
};
mockGenerateMessageId.mockReturnValue('generated-message-id');
mockI18n._.mockReturnValue('generated-message-id');
const result = resolveFieldMetadataStandardOverride(
fieldMetadata,
'label',
@@ -283,7 +280,9 @@ describe('resolveFieldMetadataStandardOverride', () => {
mockI18n,
);
expect(result).toBe('Standard Label');
expect(result).toBe('Overridden Label');
expect(mockGenerateMessageId).not.toHaveBeenCalled();
expect(mockI18n._).not.toHaveBeenCalled();
});
it('should not use empty string override for SOURCE_LOCALE', () => {
@@ -1,6 +1,6 @@
import { type I18n } from '@lingui/core';
import { isNonEmptyString } from '@sniptt/guards';
import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
import { type APP_LOCALES } from 'twenty-shared/translations';
import { isDefined } from 'twenty-shared/utils';
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
@@ -37,10 +37,7 @@ export const resolveFieldMetadataStandardOverride = (
}
}
if (
locale === SOURCE_LOCALE &&
isNonEmptyString(fieldMetadata.standardOverrides?.[labelKey])
) {
if (isNonEmptyString(fieldMetadata.standardOverrides?.[labelKey])) {
return fieldMetadata.standardOverrides[labelKey] ?? '';
}
@@ -319,7 +319,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
).toBe('overridden-icon');
});
it('should not use direct override for non-SOURCE_LOCALE', () => {
it('should use direct override for non-SOURCE_LOCALE', () => {
const objectMetadata = {
labelSingular: 'Standard Label',
labelPlural: 'Standard Labels',
@@ -332,9 +332,6 @@ describe('resolveObjectMetadataStandardOverride', () => {
},
};
mockGenerateMessageId.mockReturnValue('generated-message-id');
mockI18n._.mockReturnValue('generated-message-id');
const result = resolveObjectMetadataStandardOverride(
objectMetadata,
'labelSingular',
@@ -342,7 +339,9 @@ describe('resolveObjectMetadataStandardOverride', () => {
mockI18n,
);
expect(result).toBe('Standard Label');
expect(result).toBe('Overridden Label');
expect(mockGenerateMessageId).not.toHaveBeenCalled();
expect(mockI18n._).not.toHaveBeenCalled();
});
it('should not use undefined override for SOURCE_LOCALE', () => {
@@ -45,10 +45,7 @@ export const resolveObjectMetadataStandardOverride = (
}
}
if (
safeLocale === SOURCE_LOCALE &&
isNonEmptyString(objectMetadata.standardOverrides?.[labelKey])
) {
if (isNonEmptyString(objectMetadata.standardOverrides?.[labelKey])) {
return objectMetadata.standardOverrides[labelKey] ?? '';
}