From c16ba6a7d7b2811b3f317dbe77706198ab0be8c7 Mon Sep 17 00:00:00 2001
From: Etienne <45695613+etiennejouan@users.noreply.github.com>
Date: Mon, 16 Jun 2025 16:01:27 +0200
Subject: [PATCH] download record sample - Import (#12489)
closes https://github.com/twentyhq/core-team-issues/issues/915
---------
Co-authored-by: Charles Bochet
---
.../AdvancedFilterSubFieldSelectMenu.tsx | 6 +-
.../utils/getCompositeSubFieldLabel.ts | 9 +-
.../utils/isExpectedSubFieldName.ts | 9 +-
.../export/hooks/useExportRecords.ts | 7 +-
.../constants/CompositeFieldImportLabels.ts | 97 ----
.../hooks/useBuildAvailableFieldsForImport.ts | 9 +-
...penObjectRecordsSpreadsheetImportDialog.ts | 35 +-
.../buildRecordFromImportedStructuredRow.ts | 54 +-
...etSpreadSheetFieldValidationDefinitions.ts | 14 +-
.../utils/getSubFieldOptionKey.ts | 14 +-
...ortFilterAvailableFieldMetadataItems.ts.ts | 23 +
.../data-model/constants/AllSubFields.ts | 4 +-
.../SettingsCompositeFieldTypeConfigs.ts | 549 ++++++++++++------
.../SettingsNonCompositeFieldTypeConfigs.ts | 39 +-
.../__tests__/getFieldPreviewValue.test.ts | 2 +-
.../utils/getAddressFieldPreviewValue.ts | 2 +-
.../utils/getCurrencyFieldPreviewValue.ts | 2 +-
.../preview/utils/getFieldPreviewValue.ts | 6 +-
.../utils/getPhonesFieldPreviewValue.ts | 2 +-
.../data-model/utils/isValidSubFieldName.ts | 7 +-
...umnSelectSubFieldSelectDropdownContent.tsx | 8 +-
.../SpreadsheetMaxRecordImportCapacity.ts | 1 +
.../provider/components/SpreadsheetImport.tsx | 3 +-
.../UploadStep/components/DropZone.tsx | 27 +
.../UploadStep/components/ExampleTable.tsx | 26 -
.../hooks/useDownloadFakeRecords.ts | 138 +++++
.../components/__stories__/Steps.stories.tsx | 4 +
.../components/__stories__/Upload.stories.tsx | 4 +
.../utils/__tests__/escapeCSVValue.test.ts | 24 +
.../__tests__/generateExampleRow.test.ts | 65 ---
.../utils/escapeCSVValue.ts | 18 +
.../utils/generateExampleRow.ts | 26 -
32 files changed, 753 insertions(+), 481 deletions(-)
delete mode 100644 packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts
create mode 100644 packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems.ts.ts
create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/constants/SpreadsheetMaxRecordImportCapacity.ts
delete mode 100644 packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/ExampleTable.tsx
create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/hooks/useDownloadFakeRecords.ts
create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/escapeCSVValue.test.ts
delete mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/__tests__/generateExampleRow.test.ts
create mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/escapeCSVValue.ts
delete mode 100644 packages/twenty-front/src/modules/spreadsheet-import/utils/generateExampleRow.ts
diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
index e693d1606..3e8001950 100644
--- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
+++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
@@ -90,9 +90,9 @@ export const AdvancedFilterSubFieldSelectMenu = ({
return null;
}
- const subFieldNames =
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[objectFilterDropdownSubMenuFieldType]
- .filterableSubFields;
+ const subFieldNames = SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[
+ objectFilterDropdownSubMenuFieldType
+ ].subFields.map((subField) => subField.subFieldName);
const subFieldsAreFilterable =
isDefined(fieldMetadataItemUsedInDropdown) &&
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel.ts
index 36c6f04a4..fa5fc9066 100644
--- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel.ts
+++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel.ts
@@ -3,10 +3,11 @@ import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldTy
export const getCompositeSubFieldLabel = (
compositeFieldType: CompositeFieldType,
- subFieldName: (typeof SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS)[CompositeFieldType]['subFields'][number],
+ subFieldName: (typeof SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS)[CompositeFieldType]['subFields'][number]['subFieldName'],
): string => {
return (
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[compositeFieldType]
- .labelBySubField as any
- )[subFieldName];
+ SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[compositeFieldType].subFields.find(
+ (subField) => subField.subFieldName === subFieldName,
+ )?.subFieldLabel || ''
+ );
};
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isExpectedSubFieldName.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isExpectedSubFieldName.ts
index 65cf31e04..8074ec708 100644
--- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isExpectedSubFieldName.ts
+++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isExpectedSubFieldName.ts
@@ -5,16 +5,15 @@ export const isExpectedSubFieldName = <
CompositeFieldTypeSettings extends
typeof SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS,
PossibleSubFieldsForGivenFieldType extends
- CompositeFieldTypeSettings[GivenFieldType]['subFields'][number],
+ CompositeFieldTypeSettings[GivenFieldType]['subFields'][number]['subFieldName'],
>(
fieldMetadataType: GivenFieldType,
subFieldName: PossibleSubFieldsForGivenFieldType,
subFieldNameToCheck: string | null | undefined,
): subFieldNameToCheck is PossibleSubFieldsForGivenFieldType => {
return (
- (
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[fieldMetadataType]
- .subFields as string[]
- ).includes(subFieldName) && subFieldName === subFieldNameToCheck
+ SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[fieldMetadataType].subFields
+ .map((subField) => subField.subFieldName)
+ .includes(subFieldName) && subFieldName === subFieldNameToCheck
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportRecords.ts b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportRecords.ts
index 6b5838881..3db1e131c 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportRecords.ts
+++ b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportRecords.ts
@@ -12,6 +12,7 @@ import {
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { 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 { t } from '@lingui/core/macro';
import { saveAs } from 'file-saver';
import { isDefined } from 'twenty-shared/utils';
@@ -59,9 +60,9 @@ export const generateCsv: GenerateExport = ({
const keys = columnsToExportWithIdColumn.flatMap((col) => {
const column = {
field: `${col.metadata.fieldName}${col.type === 'RELATION' ? 'Id' : ''}`,
- title: [col.label, col.type === 'RELATION' ? 'Id' : null]
- .filter(isDefined)
- .join(' '),
+ title: escapeCSVValue(
+ `${col.label}${col.type === 'RELATION' ? ' Id' : ''}`,
+ ),
};
const columnType = col.type;
diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts
deleted file mode 100644
index 2db9a5443..000000000
--- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import {
- FieldActorValue,
- FieldAddressValue,
- FieldCurrencyValue,
- FieldEmailsValue,
- FieldFullNameValue,
- FieldLinksValue,
- FieldPhonesValue,
- FieldRichTextV2Value,
-} from '@/object-record/record-field/types/FieldMetadata';
-import { CompositeFieldLabels } from '@/object-record/spreadsheet-import/types/CompositeFieldLabels';
-import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
-import { FieldMetadataType } from '~/generated-metadata/graphql';
-
-export const COMPOSITE_FIELD_IMPORT_LABELS = {
- [FieldMetadataType.FULL_NAME]: {
- firstNameLabel:
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.FULL_NAME.labelBySubField.firstName,
- lastNameLabel:
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.FULL_NAME.labelBySubField.lastName,
- } satisfies CompositeFieldLabels,
- [FieldMetadataType.CURRENCY]: {
- currencyCodeLabel:
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.CURRENCY.labelBySubField
- .currencyCode,
- amountMicrosLabel:
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.CURRENCY.labelBySubField
- .amountMicros,
- } satisfies CompositeFieldLabels,
- [FieldMetadataType.ADDRESS]: {
- addressStreet1Label:
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.ADDRESS.labelBySubField
- .addressStreet1,
- addressStreet2Label:
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.ADDRESS.labelBySubField
- .addressStreet2,
- addressCityLabel:
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.ADDRESS.labelBySubField.addressCity,
- addressPostcodeLabel:
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.ADDRESS.labelBySubField
- .addressPostcode,
- addressStateLabel:
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.ADDRESS.labelBySubField
- .addressState,
- addressCountryLabel:
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.ADDRESS.labelBySubField
- .addressCountry,
- } satisfies Omit<
- CompositeFieldLabels,
- 'addressLatLabel' | 'addressLngLabel'
- >,
- [FieldMetadataType.LINKS]: {
- primaryLinkUrlLabel:
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.LINKS.labelBySubField
- .primaryLinkUrl,
- primaryLinkLabelLabel:
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.LINKS.labelBySubField
- .primaryLinkLabel,
- secondaryLinksLabel:
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.LINKS.labelBySubField
- .secondaryLinks,
- } satisfies CompositeFieldLabels,
- [FieldMetadataType.EMAILS]: {
- primaryEmailLabel:
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.EMAILS.labelBySubField.primaryEmail,
- additionalEmailsLabel:
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.EMAILS.labelBySubField
- .additionalEmails,
- } satisfies CompositeFieldLabels,
- [FieldMetadataType.PHONES]: {
- primaryPhoneCountryCodeLabel:
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.PHONES.labelBySubField
- .primaryPhoneCountryCode,
- primaryPhoneNumberLabel:
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.PHONES.labelBySubField
- .primaryPhoneNumber,
- primaryPhoneCallingCodeLabel:
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.PHONES.labelBySubField
- .primaryPhoneCallingCode,
- additionalPhonesLabel:
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.PHONES.labelBySubField
- .additionalPhones,
- } satisfies CompositeFieldLabels,
- [FieldMetadataType.RICH_TEXT_V2]: {
- blocknoteLabel:
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.RICH_TEXT_V2.labelBySubField
- .blocknote,
- markdownLabel:
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.RICH_TEXT_V2.labelBySubField
- .markdown,
- } satisfies CompositeFieldLabels,
- [FieldMetadataType.ACTOR]: {
- sourceLabel:
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.ACTOR.labelBySubField.source,
- nameLabel: SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.ACTOR.labelBySubField.name,
- } satisfies Partial>,
-};
diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts
index 607c167ff..135176e76 100644
--- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts
+++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts
@@ -1,8 +1,8 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
-import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-import/constants/CompositeFieldImportLabels';
import { AvailableFieldForImport } from '@/object-record/spreadsheet-import/types/AvailableFieldForImport';
import { getSpreadSheetFieldValidationDefinitions } from '@/object-record/spreadsheet-import/utils/getSpreadSheetFieldValidationDefinitions';
+import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType';
import { useIcons } from 'twenty-ui/display';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@@ -36,8 +36,9 @@ export const useBuildAvailableFieldsForImport = () => {
fieldMetadataItem: FieldMetadataItem,
fieldType: CompositeFieldType,
) => {
- Object.entries(COMPOSITE_FIELD_IMPORT_LABELS[fieldType]).forEach(
- ([subFieldKey, subFieldLabel]) => {
+ SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[fieldType].subFields.forEach(
+ ({ subFieldName, subFieldLabel, isImportable }) => {
+ if (!isImportable) return;
const label = `${fieldMetadataItem.label} / ${subFieldLabel}`;
availableFieldsForImport.push(
@@ -48,7 +49,7 @@ export const useBuildAvailableFieldsForImport = () => {
getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
label,
- subFieldKey,
+ subFieldName,
),
}),
);
diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts
index c1db47fc2..f48cfd232 100644
--- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts
+++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts
@@ -2,11 +2,12 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
import { useBuildAvailableFieldsForImport } from '@/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport';
import { buildRecordFromImportedStructuredRow } from '@/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow';
+import { spreadsheetImportFilterAvailableFieldMetadataItems } from '@/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems.ts';
import { useOpenSpreadsheetImportDialog } from '@/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog';
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
-import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
+import { FieldMetadataType } from '~/generated-metadata/graphql';
export const useOpenObjectRecordsSpreadsheetImportDialog = (
objectNameSingular: string,
@@ -30,22 +31,20 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
'fields' | 'isOpen' | 'onClose'
>,
) => {
- const availableFieldMetadataItems = objectMetadataItem.fields
- .filter(
- (fieldMetadataItem) =>
- fieldMetadataItem.isActive &&
- (!fieldMetadataItem.isSystem || fieldMetadataItem.name === 'id') &&
- fieldMetadataItem.name !== 'createdAt' &&
- fieldMetadataItem.name !== 'updatedAt' &&
- (fieldMetadataItem.type !== FieldMetadataType.RELATION ||
- fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE),
- )
- .sort((fieldMetadataItemA, fieldMetadataItemB) =>
- fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),
+ //All fields that can be imported (included matchable and auto-filled)
+ const availableFieldMetadataItemsToImport =
+ spreadsheetImportFilterAvailableFieldMetadataItems(
+ objectMetadataItem.fields,
);
- const availableFields = buildAvailableFieldsForImport(
- availableFieldMetadataItems,
+ const availableFieldMetadataItemsForMatching =
+ availableFieldMetadataItemsToImport.filter(
+ (fieldMetadataItem) =>
+ fieldMetadataItem.type !== FieldMetadataType.ACTOR,
+ );
+
+ const availableFieldsForMatching = buildAvailableFieldsForImport(
+ availableFieldMetadataItemsForMatching,
);
openSpreadsheetImportDialog({
@@ -55,7 +54,7 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
const fieldMapping: Record =
buildRecordFromImportedStructuredRow({
importedStructuredRow: record,
- fields: availableFieldMetadataItems,
+ fields: availableFieldMetadataItemsToImport,
});
return fieldMapping;
@@ -70,8 +69,8 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
});
}
},
- fields: availableFields,
- availableFieldMetadataItems,
+ fields: availableFieldsForMatching,
+ availableFieldMetadataItems: availableFieldMetadataItemsToImport,
});
};
diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts
index 05647babf..4f93d5f4b 100644
--- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts
+++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts
@@ -7,7 +7,7 @@ import {
FieldPhonesValue,
FieldRichTextV2Value,
} from '@/object-record/record-field/types/FieldMetadata';
-import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-import/constants/CompositeFieldImportLabels';
+import { COMPOSITE_FIELD_SUB_FIELD_LABELS } from '@/settings/data-model/constants/CompositeFieldSubFieldLabel';
import { ImportedStructuredRow } from '@/spreadsheet-import/types';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
@@ -85,25 +85,35 @@ export const buildRecordFromImportedStructuredRow = ({
const {
ADDRESS: {
- addressCityLabel,
- addressCountryLabel,
- addressPostcodeLabel,
- addressStateLabel,
- addressStreet1Label,
- addressStreet2Label,
+ addressCity: addressCityLabel,
+ addressCountry: addressCountryLabel,
+ addressPostcode: addressPostcodeLabel,
+ addressState: addressStateLabel,
+ addressStreet1: addressStreet1Label,
+ addressStreet2: addressStreet2Label,
+ },
+ CURRENCY: {
+ amountMicros: amountMicrosLabel,
+ currencyCode: currencyCodeLabel,
+ },
+ FULL_NAME: { firstName: firstNameLabel, lastName: lastNameLabel },
+ LINKS: {
+ primaryLinkUrl: primaryLinkUrlLabel,
+ primaryLinkLabel: primaryLinkLabelLabel,
+ secondaryLinks: secondaryLinksLabel,
+ },
+ EMAILS: {
+ primaryEmail: primaryEmailLabel,
+ additionalEmails: additionalEmailsLabel,
},
- CURRENCY: { amountMicrosLabel, currencyCodeLabel },
- FULL_NAME: { firstNameLabel, lastNameLabel },
- LINKS: { primaryLinkUrlLabel, primaryLinkLabelLabel, secondaryLinksLabel },
- EMAILS: { primaryEmailLabel, additionalEmailsLabel },
PHONES: {
- primaryPhoneNumberLabel,
- primaryPhoneCountryCodeLabel,
- primaryPhoneCallingCodeLabel,
- additionalPhonesLabel,
+ primaryPhoneNumber: primaryPhoneNumberLabel,
+ primaryPhoneCountryCode: primaryPhoneCountryCodeLabel,
+ primaryPhoneCallingCode: primaryPhoneCallingCodeLabel,
+ additionalPhones: additionalPhonesLabel,
},
- RICH_TEXT_V2: { blocknoteLabel, markdownLabel },
- } = COMPOSITE_FIELD_IMPORT_LABELS;
+ RICH_TEXT_V2: { blocknote: blocknoteLabel, markdown: markdownLabel },
+ } = COMPOSITE_FIELD_SUB_FIELD_LABELS;
for (const field of fields) {
const importedFieldValue = importedStructuredRow[field.name];
@@ -274,10 +284,18 @@ export const buildRecordFromImportedStructuredRow = ({
}
break;
}
+ case FieldMetadataType.UUID:
+ if (
+ isDefined(importedFieldValue) &&
+ isNonEmptyString(importedFieldValue)
+ ) {
+ recordToBuild[field.name] = importedFieldValue;
+ }
+ break;
case FieldMetadataType.RELATION:
if (
isDefined(importedFieldValue) &&
- (isNonEmptyString(importedFieldValue) || importedFieldValue !== false)
+ isNonEmptyString(importedFieldValue)
) {
recordToBuild[field.name + 'Id'] = importedFieldValue;
}
diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSpreadSheetFieldValidationDefinitions.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSpreadSheetFieldValidationDefinitions.ts
index fba6efedd..b3aadea9c 100644
--- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSpreadSheetFieldValidationDefinitions.ts
+++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSpreadSheetFieldValidationDefinitions.ts
@@ -35,14 +35,14 @@ export const getSpreadSheetFieldValidationDefinitions = (
];
case FieldMetadataType.CURRENCY:
switch (subFieldKey) {
- case 'amountMicrosLabel':
+ case 'amountMicros':
return [getNumberValidationDefinition(fieldName)];
default:
return [];
}
case FieldMetadataType.EMAILS:
switch (subFieldKey) {
- case 'primaryEmailLabel':
+ case 'primaryEmail':
return [
{
rule: 'function',
@@ -51,7 +51,7 @@ export const getSpreadSheetFieldValidationDefinitions = (
level: 'error',
},
];
- case 'additionalEmailsLabel':
+ case 'additionalEmails':
return [
{
rule: 'function',
@@ -77,7 +77,7 @@ export const getSpreadSheetFieldValidationDefinitions = (
}
case FieldMetadataType.LINKS:
switch (subFieldKey) {
- case 'primaryLinkUrlLabel':
+ case 'primaryLinkUrl':
return [
{
rule: 'function',
@@ -89,7 +89,7 @@ export const getSpreadSheetFieldValidationDefinitions = (
level: 'error',
},
];
- case 'secondaryLinksLabel':
+ case 'secondaryLinks':
return [
{
rule: 'function',
@@ -139,7 +139,7 @@ export const getSpreadSheetFieldValidationDefinitions = (
];
case FieldMetadataType.PHONES:
switch (subFieldKey) {
- case 'primaryPhoneNumberLabel':
+ case 'primaryPhoneNumber':
return [
{
rule: 'regex',
@@ -148,7 +148,7 @@ export const getSpreadSheetFieldValidationDefinitions = (
level: 'error',
},
];
- case 'additionalPhonesLabel':
+ case 'additionalPhones':
return [
{
rule: 'function',
diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSubFieldOptionKey.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSubFieldOptionKey.ts
index 1af94727d..b9f0d78d5 100644
--- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSubFieldOptionKey.ts
+++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSubFieldOptionKey.ts
@@ -1,15 +1,19 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
-import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-import/constants/CompositeFieldImportLabels';
+import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
+import { COMPOSITE_FIELD_SUB_FIELD_LABELS } from '@/settings/data-model/constants/CompositeFieldSubFieldLabel';
export const getSubFieldOptionKey = (
fieldMetadataItem: FieldMetadataItem,
subFieldName: string,
) => {
- const subFieldNameLabelKey = `${subFieldName}Label`;
+ if (!isCompositeFieldType(fieldMetadataItem.type)) {
+ throw new Error(
+ `getSubFieldOptionKey can only be called for composite field types. Received: ${fieldMetadataItem.type}`,
+ );
+ }
- const subFieldLabel = (
- (COMPOSITE_FIELD_IMPORT_LABELS as any)[fieldMetadataItem.type] as any
- )[subFieldNameLabelKey];
+ const subFieldLabel =
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[fieldMetadataItem.type][subFieldName];
const subFieldKey = `${subFieldLabel} (${fieldMetadataItem.name})`;
diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems.ts.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems.ts.ts
new file mode 100644
index 000000000..f91bd066a
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems.ts.ts
@@ -0,0 +1,23 @@
+import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
+import { FieldMetadataType } from 'twenty-shared/types';
+import { RelationType } from '~/generated-metadata/graphql';
+
+export const spreadsheetImportFilterAvailableFieldMetadataItems = (
+ fields: FieldMetadataItem[],
+) => {
+ return fields
+ .filter(
+ (fieldMetadataItem) =>
+ fieldMetadataItem.isActive &&
+ (!fieldMetadataItem.isSystem || fieldMetadataItem.name === 'id') &&
+ fieldMetadataItem.name !== 'createdAt' &&
+ fieldMetadataItem.name !== 'updatedAt' &&
+ (![FieldMetadataType.RELATION, FieldMetadataType.RICH_TEXT].includes(
+ fieldMetadataItem.type,
+ ) ||
+ fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE),
+ )
+ .sort((fieldMetadataItemA, fieldMetadataItemB) =>
+ fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),
+ );
+};
diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/AllSubFields.ts b/packages/twenty-front/src/modules/settings/data-model/constants/AllSubFields.ts
index 307d935df..f92b4f652 100644
--- a/packages/twenty-front/src/modules/settings/data-model/constants/AllSubFields.ts
+++ b/packages/twenty-front/src/modules/settings/data-model/constants/AllSubFields.ts
@@ -3,5 +3,7 @@ import { COMPOSITE_FIELD_TYPES } from '@/settings/data-model/types/CompositeFiel
export const ALL_SUB_FIELDS = COMPOSITE_FIELD_TYPES.flatMap(
(compositeFieldType) =>
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[compositeFieldType].subFields,
+ SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[compositeFieldType].subFields.map(
+ (subField) => subField.subFieldName,
+ ),
);
diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts
index 5b92fa58d..f525810c4 100644
--- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts
+++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts
@@ -25,11 +25,16 @@ import {
} from 'twenty-ui/display';
import { FieldMetadataType } from '~/generated-metadata/graphql';
+type CompositeSubFieldConfig = {
+ subFieldName: keyof T;
+ subFieldLabel: string;
+ isImportable: boolean;
+ isFilterable: boolean;
+};
+
export type SettingsCompositeFieldTypeConfig = SettingsFieldTypeConfig & {
- subFields: (keyof T)[];
- filterableSubFields: (keyof T)[];
- labelBySubField: Record;
- exampleValue: T;
+ subFields: CompositeSubFieldConfig[];
+ exampleValues: [T, T, T];
};
type SettingsCompositeFieldTypeConfigArray = Record<
@@ -41,215 +46,415 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
[FieldMetadataType.CURRENCY]: {
label: 'Currency',
Icon: IllustrationIconCurrency,
- subFields: ['amountMicros', 'currencyCode'],
- filterableSubFields: ['amountMicros', 'currencyCode'],
- labelBySubField: {
- amountMicros:
- COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.CURRENCY]
- .amountMicros,
- currencyCode:
- COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.CURRENCY]
- .currencyCode,
- },
- exampleValue: {
- amountMicros: 2000000000,
- currencyCode: CurrencyCode.USD,
- },
+ subFields: [
+ {
+ subFieldName: 'amountMicros',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.CURRENCY]
+ .amountMicros,
+ isImportable: true,
+ isFilterable: true,
+ },
+ {
+ subFieldName: 'currencyCode',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.CURRENCY]
+ .currencyCode,
+ isImportable: true,
+ isFilterable: true,
+ },
+ ],
+ exampleValues: [
+ {
+ amountMicros: 2000000000,
+ currencyCode: CurrencyCode.USD,
+ },
+ {
+ amountMicros: 3000000000,
+ currencyCode: CurrencyCode.GBP,
+ },
+ {
+ amountMicros: 100000000,
+ currencyCode: CurrencyCode.AED,
+ },
+ ],
category: 'Basic',
} as const satisfies SettingsCompositeFieldTypeConfig,
[FieldMetadataType.EMAILS]: {
label: 'Emails',
Icon: IllustrationIconMail,
- subFields: ['primaryEmail', 'additionalEmails'],
- filterableSubFields: ['primaryEmail', 'additionalEmails'],
- labelBySubField: {
- primaryEmail: 'Primary Email',
- additionalEmails: 'Additional Emails',
- },
- exampleValue: {
- primaryEmail: 'john@twenty.com',
- additionalEmails: [
- 'tim@twenty.com',
- 'timapple@twenty.com',
- 'johnappletim@twenty.com',
- ],
- },
+ subFields: [
+ {
+ subFieldName: 'primaryEmail',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.EMAILS]
+ .primaryEmail,
+ isImportable: true,
+ isFilterable: true,
+ },
+ {
+ subFieldName: 'additionalEmails',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.EMAILS]
+ .additionalEmails,
+ isImportable: true,
+ isFilterable: true,
+ },
+ ],
+ exampleValues: [
+ {
+ primaryEmail: 'tim@twenty.com',
+ additionalEmails: [
+ 'tim@twenty.com',
+ 'timapple@twenty.com',
+ 'johnappletim@twenty.com',
+ ],
+ },
+ {
+ primaryEmail: 'jane@twenty.com',
+ additionalEmails: ['jane@twenty.com', 'jane.doe@twenty.com'],
+ },
+ {
+ primaryEmail: 'john@twenty.com',
+ additionalEmails: ['john.doe@twenty.com'],
+ },
+ ],
category: 'Basic',
} as const satisfies SettingsCompositeFieldTypeConfig,
[FieldMetadataType.LINKS]: {
label: 'Links',
Icon: IllustrationIconLink,
- exampleValue: {
- primaryLinkUrl: 'twenty.com',
- primaryLinkLabel: '',
- secondaryLinks: [{ url: 'twenty.com', label: 'Twenty' }],
- },
- category: 'Basic',
- subFields: ['primaryLinkUrl', 'primaryLinkLabel', 'secondaryLinks'],
- filterableSubFields: [
- 'primaryLinkUrl',
- 'primaryLinkLabel',
- 'secondaryLinks',
+ subFields: [
+ {
+ subFieldName: 'primaryLinkUrl',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.LINKS]
+ .primaryLinkUrl,
+ isImportable: true,
+ isFilterable: true,
+ },
+ {
+ subFieldName: 'primaryLinkLabel',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.LINKS]
+ .primaryLinkLabel,
+ isImportable: true,
+ isFilterable: true,
+ },
+ {
+ subFieldName: 'secondaryLinks',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.LINKS]
+ .secondaryLinks,
+ isImportable: true,
+ isFilterable: true,
+ },
],
- labelBySubField: {
- primaryLinkUrl:
- COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.LINKS]
- .primaryLinkUrl,
- primaryLinkLabel:
- COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.LINKS]
- .primaryLinkLabel,
- secondaryLinks:
- COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.LINKS]
- .secondaryLinks,
- },
+ exampleValues: [
+ {
+ primaryLinkUrl: 'twenty.com',
+ primaryLinkLabel: '',
+ secondaryLinks: [{ url: 'twenty.com', label: 'Twenty' }],
+ },
+ {
+ primaryLinkUrl: 'github.com/twentyhq/twenty',
+ primaryLinkLabel: 'Twenty Repo',
+ secondaryLinks: [{ url: 'twenty.com', label: '' }],
+ },
+ {
+ primaryLinkUrl: 'react.dev',
+ primaryLinkLabel: '',
+ secondaryLinks: [],
+ },
+ ],
+ category: 'Basic',
} as const satisfies SettingsCompositeFieldTypeConfig,
[FieldMetadataType.PHONES]: {
label: 'Phones',
Icon: IllustrationIconPhone,
- exampleValue: {
- primaryPhoneCallingCode: '+33',
- primaryPhoneCountryCode: 'FR',
- primaryPhoneNumber: '789012345',
- additionalPhones: [
- { number: '617272323', callingCode: '+33', countryCode: 'FR' },
- ],
- },
subFields: [
- 'primaryPhoneNumber',
- 'primaryPhoneCountryCode',
- 'primaryPhoneCallingCode',
- 'additionalPhones',
+ {
+ subFieldName: 'primaryPhoneCallingCode',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.PHONES]
+ .primaryPhoneCallingCode,
+ isImportable: true,
+ isFilterable: true,
+ },
+ {
+ subFieldName: 'primaryPhoneCountryCode',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.PHONES]
+ .primaryPhoneCountryCode,
+ isImportable: true,
+ isFilterable: false,
+ },
+ {
+ subFieldName: 'primaryPhoneNumber',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.PHONES]
+ .primaryPhoneNumber,
+ isImportable: true,
+ isFilterable: true,
+ },
+ {
+ subFieldName: 'additionalPhones',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.PHONES]
+ .additionalPhones,
+ isImportable: true,
+ isFilterable: true,
+ },
],
- filterableSubFields: [
- 'primaryPhoneNumber',
- 'primaryPhoneCallingCode',
- 'additionalPhones',
+ exampleValues: [
+ {
+ primaryPhoneCallingCode: '+33',
+ primaryPhoneCountryCode: 'FR',
+ primaryPhoneNumber: '789012345',
+ additionalPhones: [
+ { number: '617272323', callingCode: '+33', countryCode: 'FR' },
+ ],
+ },
+ {
+ primaryPhoneCallingCode: '+1',
+ primaryPhoneCountryCode: 'US',
+ primaryPhoneNumber: '612345789',
+ additionalPhones: [
+ { number: '123456789', callingCode: '+1', countryCode: 'US' },
+ { number: '617272323', callingCode: '+1', countryCode: 'US' },
+ ],
+ },
+ {
+ primaryPhoneCallingCode: '+33',
+ primaryPhoneCountryCode: 'FR',
+ primaryPhoneNumber: '123456789',
+ additionalPhones: [],
+ },
],
- labelBySubField: {
- primaryPhoneNumber:
- COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.PHONES]
- .primaryPhoneNumber,
- primaryPhoneCountryCode:
- COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.PHONES]
- .primaryPhoneCountryCode,
- primaryPhoneCallingCode:
- COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.PHONES]
- .primaryPhoneCallingCode,
- additionalPhones:
- COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.PHONES]
- .additionalPhones,
- },
category: 'Basic',
} as const satisfies SettingsCompositeFieldTypeConfig,
[FieldMetadataType.FULL_NAME]: {
label: 'Full Name',
Icon: IllustrationIconUser,
- exampleValue: { firstName: 'John', lastName: 'Doe' },
+ subFields: [
+ {
+ subFieldName: 'firstName',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.FULL_NAME]
+ .firstName,
+ isImportable: true,
+ isFilterable: true,
+ },
+ {
+ subFieldName: 'lastName',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.FULL_NAME]
+ .lastName,
+ isImportable: true,
+ isFilterable: true,
+ },
+ ],
+ exampleValues: [
+ { firstName: 'John', lastName: 'Doe' },
+ { firstName: 'Jane', lastName: 'Doe' },
+ { firstName: 'John', lastName: 'Smith' },
+ ],
category: 'Basic',
- subFields: ['firstName', 'lastName'],
- filterableSubFields: ['firstName', 'lastName'],
- labelBySubField: {
- firstName:
- COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.FULL_NAME].firstName,
- lastName:
- COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.FULL_NAME].lastName,
- },
} as const satisfies SettingsCompositeFieldTypeConfig,
[FieldMetadataType.ADDRESS]: {
label: 'Address',
Icon: IllustrationIconMap,
subFields: [
- 'addressStreet1',
- 'addressStreet2',
- 'addressCity',
- 'addressState',
- 'addressCountry',
- 'addressPostcode',
- 'addressLat',
- 'addressLng',
+ {
+ subFieldName: 'addressStreet1',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS]
+ .addressStreet1,
+ isImportable: true,
+ isFilterable: true,
+ },
+ {
+ subFieldName: 'addressStreet2',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS]
+ .addressStreet2,
+ isImportable: true,
+ isFilterable: true,
+ },
+ {
+ subFieldName: 'addressCity',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS]
+ .addressCity,
+ isImportable: true,
+ isFilterable: true,
+ },
+ {
+ subFieldName: 'addressState',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS]
+ .addressState,
+ isImportable: true,
+ isFilterable: true,
+ },
+ {
+ subFieldName: 'addressCountry',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS]
+ .addressCountry,
+ isImportable: true,
+ isFilterable: true,
+ },
+ {
+ subFieldName: 'addressPostcode',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS]
+ .addressPostcode,
+ isImportable: true,
+ isFilterable: true,
+ },
+ {
+ subFieldName: 'addressLat',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS]
+ .addressLat,
+ isImportable: false,
+ isFilterable: false,
+ },
+ {
+ subFieldName: 'addressLng',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS]
+ .addressLng,
+ isImportable: false,
+ isFilterable: false,
+ },
],
- filterableSubFields: [
- 'addressStreet1',
- 'addressStreet2',
- 'addressCity',
- 'addressState',
- 'addressCountry',
- 'addressPostcode',
+ exampleValues: [
+ {
+ addressStreet1: '456 Oak Street',
+ addressStreet2: '',
+ addressCity: 'Springfield',
+ addressState: 'California',
+ addressCountry: 'United States',
+ addressPostcode: '90210',
+ addressLat: 34.0522,
+ addressLng: -118.2437,
+ },
+ {
+ addressStreet1: '123 Main Street',
+ addressStreet2: '',
+ addressCity: 'New York',
+ addressState: 'New York',
+ addressCountry: 'United States',
+ addressPostcode: '10001',
+ addressLat: 40.7128,
+ addressLng: -74.006,
+ },
+ {
+ addressStreet1: '8 rue Saint-Anne',
+ addressStreet2: '',
+ addressCity: 'Paris',
+ addressState: 'Ile-de-France',
+ addressCountry: 'France',
+ addressPostcode: '75001',
+ addressLat: 40.7128,
+ addressLng: -74.006,
+ },
],
- labelBySubField: {
- addressStreet1:
- COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS]
- .addressStreet1,
- addressStreet2:
- COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS]
- .addressStreet2,
- addressCity:
- COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS].addressCity,
- addressState:
- COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS]
- .addressState,
- addressCountry:
- COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS]
- .addressCountry,
- addressPostcode:
- COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS]
- .addressPostcode,
- addressLat:
- COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS].addressLat,
- addressLng:
- COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS].addressLng,
- },
- exampleValue: {
- addressStreet1: '456 Oak Street',
- addressStreet2: '',
- addressCity: 'Springfield',
- addressState: 'California',
- addressCountry: 'United States',
- addressPostcode: '90210',
- addressLat: 34.0522,
- addressLng: -118.2437,
- },
category: 'Basic',
} as const satisfies SettingsCompositeFieldTypeConfig,
[FieldMetadataType.ACTOR]: {
label: 'Actor',
Icon: IllustrationIconSetting,
category: 'Basic',
- subFields: ['source', 'name'],
- filterableSubFields: ['source', 'name'],
- labelBySubField: {
- source: COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ACTOR].source,
- name: COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ACTOR].name,
- workspaceMemberId:
- COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ACTOR]
- .workspaceMemberId,
- context:
- COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ACTOR].context,
- },
- exampleValue: {
- source: 'IMPORT',
- name: 'name',
- workspaceMemberId: 'id',
- context: { provider: ConnectedAccountProvider.GOOGLE },
- },
+ subFields: [
+ {
+ subFieldName: 'source',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ACTOR].source,
+ isImportable: true,
+ isFilterable: true,
+ },
+ {
+ subFieldName: 'name',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ACTOR].name,
+ isImportable: true,
+ isFilterable: true,
+ },
+ {
+ subFieldName: 'workspaceMemberId',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ACTOR]
+ .workspaceMemberId,
+ isImportable: true,
+ isFilterable: false,
+ },
+ {
+ subFieldName: 'context',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ACTOR].context,
+ isImportable: true,
+ isFilterable: false,
+ },
+ ],
+ exampleValues: [
+ {
+ source: 'IMPORT',
+ name: 'name',
+ workspaceMemberId: 'id',
+ context: { provider: ConnectedAccountProvider.GOOGLE },
+ },
+ {
+ source: 'MANUAL',
+ name: 'name',
+ workspaceMemberId: 'id',
+ context: { provider: ConnectedAccountProvider.MICROSOFT },
+ },
+ {
+ source: 'WEBHOOK',
+ name: 'name',
+ workspaceMemberId: 'id',
+ context: {},
+ },
+ ],
} as const satisfies SettingsCompositeFieldTypeConfig,
[FieldMetadataType.RICH_TEXT_V2]: {
label: 'Rich Text',
Icon: IllustrationIconText,
- subFields: ['blocknote', 'markdown'],
- filterableSubFields: [],
- labelBySubField: {
- blocknote:
- COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.RICH_TEXT_V2]
- .blocknote,
- markdown:
- COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.RICH_TEXT_V2]
- .markdown,
- },
- exampleValue: {
- blocknote: '[{"type":"heading","content":"Hello"}]',
- markdown: '# Hello',
- },
category: 'Basic',
+ subFields: [
+ {
+ subFieldName: 'blocknote',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.RICH_TEXT_V2]
+ .blocknote,
+ isImportable: false,
+ isFilterable: false,
+ },
+ {
+ subFieldName: 'markdown',
+ subFieldLabel:
+ COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.RICH_TEXT_V2]
+ .markdown,
+ isImportable: false,
+ isFilterable: false,
+ },
+ ],
+ exampleValues: [
+ {
+ blocknote: '[{"type":"heading","content":"Hello"}]',
+ markdown: '# Hello',
+ },
+ {
+ blocknote: '[{"type":"heading","content":"Hello World"}]',
+ markdown: '# Hello World',
+ },
+ {
+ blocknote: '[{"type":"heading","content":"Hello Again"}]',
+ markdown: '# Hello Again',
+ },
+ ],
} as const satisfies SettingsCompositeFieldTypeConfig,
} as const satisfies SettingsCompositeFieldTypeConfigArray;
diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs.ts
index b400d2846..d0f33a001 100644
--- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs.ts
+++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs.ts
@@ -15,7 +15,6 @@ import {
import { DEFAULT_DATE_VALUE } from '@/settings/data-model/constants/DefaultDateValue';
import { SettingsFieldTypeCategoryType } from '@/settings/data-model/types/SettingsFieldTypeCategoryType';
import { SettingsNonCompositeFieldType } from '@/settings/data-model/types/SettingsNonCompositeFieldType';
-import { FieldMetadataType } from '~/generated-metadata/graphql';
import {
IconComponent,
IllustrationIconArray,
@@ -31,13 +30,14 @@ import {
IllustrationIconToggle,
IllustrationIconUid,
} from 'twenty-ui/display';
+import { FieldMetadataType } from '~/generated-metadata/graphql';
DEFAULT_DATE_VALUE.setFullYear(DEFAULT_DATE_VALUE.getFullYear() + 2);
export type SettingsFieldTypeConfig = {
label: string;
Icon: IconComponent;
- exampleValue?: T;
+ exampleValues?: [T, T, T];
category: SettingsFieldTypeCategoryType;
};
@@ -52,44 +52,59 @@ export const SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS: SettingsNonCompositeFiel
[FieldMetadataType.UUID]: {
label: 'Unique ID',
Icon: IllustrationIconUid,
- exampleValue: '00000000-0000-0000-0000-000000000000',
+ exampleValues: [
+ '00000000-0000-0000-0000-000000000000',
+ '00000000-0000-0000-0000-000000000001',
+ '00000000-0000-0000-0000-000000000002',
+ ],
category: 'Advanced',
} as const satisfies SettingsFieldTypeConfig,
[FieldMetadataType.TEXT]: {
label: 'Text',
Icon: IllustrationIconText,
- exampleValue:
+ exampleValues: [
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum magna enim, dapibus non enim in, lacinia faucibus nunc. Sed interdum ante sed felis facilisis, eget ultricies neque molestie. Mauris auctor, justo eu volutpat cursus, libero erat tempus nulla, non sodales lorem lacus a est.',
+ 'Vestibulum magna enim, dapibus non enim in, lacinia faucibus nunc. Sed interdum ante sed felis facilisis, eget ultricies neque molestie. Mauris auctor, justo eu volutpat cursus, libero erat tempus nulla, non sodales lorem lacus a est.',
+ 'Sed interdum ante sed felis facilisis, eget ultricies neque molestie. Mauris auctor, justo eu volutpat cursus, libero erat tempus nulla, non sodales lorem lacus a est.',
+ ],
category: 'Basic',
} as const satisfies SettingsFieldTypeConfig,
[FieldMetadataType.NUMERIC]: {
label: 'Numeric',
Icon: IllustrationIconNumbers,
- exampleValue: 2000,
+ exampleValues: [2000, 3000, 4000],
category: 'Basic',
} as const satisfies SettingsFieldTypeConfig,
[FieldMetadataType.NUMBER]: {
label: 'Number',
Icon: IllustrationIconNumbers,
- exampleValue: 2000,
+ exampleValues: [2000, 3000, 4000],
category: 'Basic',
} as const satisfies SettingsFieldTypeConfig,
[FieldMetadataType.BOOLEAN]: {
label: 'True/False',
Icon: IllustrationIconToggle,
- exampleValue: true,
+ exampleValues: [true, false, true],
category: 'Basic',
} as const satisfies SettingsFieldTypeConfig,
[FieldMetadataType.DATE_TIME]: {
label: 'Date and Time',
Icon: IllustrationIconCalendarTime,
- exampleValue: DEFAULT_DATE_VALUE.toISOString(),
+ exampleValues: [
+ DEFAULT_DATE_VALUE.toISOString(),
+ '2025-06-10T12:01:00.000Z',
+ '2018-07-14T12:02:00.000Z',
+ ],
category: 'Basic',
} as const satisfies SettingsFieldTypeConfig,
[FieldMetadataType.DATE]: {
label: 'Date',
Icon: IllustrationIconCalendarEvent,
- exampleValue: DEFAULT_DATE_VALUE.toISOString(),
+ exampleValues: [
+ DEFAULT_DATE_VALUE.toISOString(),
+ '2025-06-10T00:00:00.000Z',
+ '2018-07-14T00:00:00.000Z',
+ ],
category: 'Basic',
} as const satisfies SettingsFieldTypeConfig,
[FieldMetadataType.SELECT]: {
@@ -110,19 +125,19 @@ export const SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS: SettingsNonCompositeFiel
[FieldMetadataType.RATING]: {
label: 'Rating',
Icon: IllustrationIconStar,
- exampleValue: 'RATING_3',
+ exampleValues: ['RATING_3', 'RATING_4', 'RATING_5'],
category: 'Basic',
} as const satisfies SettingsFieldTypeConfig,
[FieldMetadataType.RAW_JSON]: {
label: 'JSON',
Icon: IllustrationIconJson,
- exampleValue: { key: 'value' },
+ exampleValues: [{ key: 'value1' }, { key: 'value2', key2: 'value2' }, {}],
category: 'Advanced',
} as const satisfies SettingsFieldTypeConfig,
[FieldMetadataType.ARRAY]: {
label: 'Array',
Icon: IllustrationIconArray,
category: 'Advanced',
- exampleValue: ['value1', 'value2'],
+ exampleValues: [['value1', 'value2'], ['value3'], []],
} as const satisfies SettingsFieldTypeConfig,
};
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getFieldPreviewValue.test.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getFieldPreviewValue.test.ts
index 3e4d9e9ea..3e09efe70 100644
--- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getFieldPreviewValue.test.ts
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getFieldPreviewValue.test.ts
@@ -51,7 +51,7 @@ describe('getFieldPreviewValue', () => {
// Then
expect(result).toBe(2000);
expect(result).toBe(
- getSettingsFieldTypeConfig(FieldMetadataType.NUMBER).exampleValue,
+ getSettingsFieldTypeConfig(FieldMetadataType.NUMBER).exampleValues?.[0],
);
});
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getAddressFieldPreviewValue.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getAddressFieldPreviewValue.ts
index ec238cb3b..2d91dc803 100644
--- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getAddressFieldPreviewValue.ts
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getAddressFieldPreviewValue.ts
@@ -18,7 +18,7 @@ export const getAddressFieldPreviewValue = ({
FieldMetadataType.ADDRESS,
);
- const placeholderDefaultValue = addressFieldTypeConfig.exampleValue;
+ const placeholderDefaultValue = addressFieldTypeConfig.exampleValues?.[0];
const addressCountry =
fieldMetadataItem.defaultValue?.addressCountry &&
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getCurrencyFieldPreviewValue.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getCurrencyFieldPreviewValue.ts
index 70d378044..1cc84e4eb 100644
--- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getCurrencyFieldPreviewValue.ts
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getCurrencyFieldPreviewValue.ts
@@ -20,7 +20,7 @@ export const getCurrencyFieldPreviewValue = ({
FieldMetadataType.CURRENCY,
);
- const placeholderDefaultValue = currencyFieldTypeConfig.exampleValue;
+ const placeholderDefaultValue = currencyFieldTypeConfig.exampleValues?.[0];
return currencyFieldDefaultValueSchema
.transform((value) => ({
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getFieldPreviewValue.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getFieldPreviewValue.ts
index 870734598..c89bb537a 100644
--- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getFieldPreviewValue.ts
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getFieldPreviewValue.ts
@@ -31,10 +31,10 @@ export const getFieldPreviewValue = ({
if (
isDefined(fieldTypeConfig) &&
- 'exampleValue' in fieldTypeConfig &&
- isDefined(fieldTypeConfig.exampleValue)
+ 'exampleValues' in fieldTypeConfig &&
+ isDefined(fieldTypeConfig.exampleValues?.[0])
) {
- return fieldTypeConfig.exampleValue;
+ return fieldTypeConfig.exampleValues?.[0];
}
return null;
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getPhonesFieldPreviewValue.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getPhonesFieldPreviewValue.ts
index 714ccb21f..903f2dad9 100644
--- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getPhonesFieldPreviewValue.ts
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getPhonesFieldPreviewValue.ts
@@ -38,7 +38,7 @@ export const getPhonesFieldPreviewValue = ({
FieldMetadataType.PHONES,
);
- const placeholderDefaultValue = phonesFieldTypeConfig.exampleValue;
+ const placeholderDefaultValue = phonesFieldTypeConfig.exampleValues?.[0];
const primaryPhoneCountryCode =
fieldMetadataItem.defaultValue?.primaryPhoneCountryCode &&
fieldMetadataItem.defaultValue.primaryPhoneCountryCode !== ''
diff --git a/packages/twenty-front/src/modules/settings/data-model/utils/isValidSubFieldName.ts b/packages/twenty-front/src/modules/settings/data-model/utils/isValidSubFieldName.ts
index c6ef93a20..a8fca9243 100644
--- a/packages/twenty-front/src/modules/settings/data-model/utils/isValidSubFieldName.ts
+++ b/packages/twenty-front/src/modules/settings/data-model/utils/isValidSubFieldName.ts
@@ -5,9 +5,10 @@ import { COMPOSITE_FIELD_TYPES } from '@/settings/data-model/types/CompositeFiel
export const isValidSubFieldName = (
subFieldName: string,
): subFieldName is CompositeFieldSubFieldName => {
- const allSubFields = COMPOSITE_FIELD_TYPES.flatMap(
- (compositeFieldType) =>
- SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[compositeFieldType].subFields,
+ const allSubFields = COMPOSITE_FIELD_TYPES.flatMap((compositeFieldType) =>
+ SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[compositeFieldType].subFields.map(
+ (subField) => subField.subFieldName,
+ ),
);
return allSubFields.includes(subFieldName as any);
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent.tsx
index 8a41b646c..f95e2f4da 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent.tsx
+++ b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelectSubFieldSelectDropdownContent.tsx
@@ -58,8 +58,8 @@ export const MatchColumnSelectSubFieldSelectDropdownContent = ({
const fieldMetadataItemSettings =
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[fieldMetadataItem.type];
- const subFieldNamesThatExistInOptions = fieldMetadataItemSettings.subFields
- .filter((subFieldName) => {
+ const subFieldsThatExistInOptions = fieldMetadataItemSettings.subFields
+ .filter(({ subFieldName }) => {
const optionKey = getSubFieldOptionKey(fieldMetadataItem, subFieldName);
const correspondingOption = options.find(
@@ -68,7 +68,7 @@ export const MatchColumnSelectSubFieldSelectDropdownContent = ({
return isDefined(correspondingOption);
})
- .filter((subFieldName) =>
+ .filter(({ subFieldName }) =>
getCompositeSubFieldLabel(
fieldMetadataItem.type as CompositeFieldType,
subFieldName,
@@ -96,7 +96,7 @@ export const MatchColumnSelectSubFieldSelectDropdownContent = ({
/>
- {subFieldNamesThatExistInOptions.map((subFieldName) => (
+ {subFieldsThatExistInOptions.map(({ subFieldName }) => (