download record sample - Import (#12489)
<img width="400" alt="Screenshot 2025-06-10 at 18 14 17" src="https://github.com/user-attachments/assets/05591b46-c36d-45c6-a236-3469c29d7420" /> closes https://github.com/twentyhq/core-team-issues/issues/915 --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -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) &&
|
||||
|
||||
@ -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 || ''
|
||||
);
|
||||
};
|
||||
|
||||
@ -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
|
||||
);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<FieldFullNameValue>,
|
||||
[FieldMetadataType.CURRENCY]: {
|
||||
currencyCodeLabel:
|
||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.CURRENCY.labelBySubField
|
||||
.currencyCode,
|
||||
amountMicrosLabel:
|
||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.CURRENCY.labelBySubField
|
||||
.amountMicros,
|
||||
} satisfies CompositeFieldLabels<FieldCurrencyValue>,
|
||||
[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<FieldAddressValue>,
|
||||
'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<FieldLinksValue>,
|
||||
[FieldMetadataType.EMAILS]: {
|
||||
primaryEmailLabel:
|
||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.EMAILS.labelBySubField.primaryEmail,
|
||||
additionalEmailsLabel:
|
||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.EMAILS.labelBySubField
|
||||
.additionalEmails,
|
||||
} satisfies CompositeFieldLabels<FieldEmailsValue>,
|
||||
[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<FieldPhonesValue>,
|
||||
[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<FieldRichTextV2Value>,
|
||||
[FieldMetadataType.ACTOR]: {
|
||||
sourceLabel:
|
||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.ACTOR.labelBySubField.source,
|
||||
nameLabel: SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.ACTOR.labelBySubField.name,
|
||||
} satisfies Partial<CompositeFieldLabels<FieldActorValue>>,
|
||||
};
|
||||
@ -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,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
@ -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<string, any> =
|
||||
buildRecordFromImportedStructuredRow({
|
||||
importedStructuredRow: record,
|
||||
fields: availableFieldMetadataItems,
|
||||
fields: availableFieldMetadataItemsToImport,
|
||||
});
|
||||
|
||||
return fieldMapping;
|
||||
@ -70,8 +69,8 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
|
||||
});
|
||||
}
|
||||
},
|
||||
fields: availableFields,
|
||||
availableFieldMetadataItems,
|
||||
fields: availableFieldsForMatching,
|
||||
availableFieldMetadataItems: availableFieldMetadataItemsToImport,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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})`;
|
||||
|
||||
|
||||
@ -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),
|
||||
);
|
||||
};
|
||||
@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
@ -25,11 +25,16 @@ import {
|
||||
} from 'twenty-ui/display';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
type CompositeSubFieldConfig<T> = {
|
||||
subFieldName: keyof T;
|
||||
subFieldLabel: string;
|
||||
isImportable: boolean;
|
||||
isFilterable: boolean;
|
||||
};
|
||||
|
||||
export type SettingsCompositeFieldTypeConfig<T> = SettingsFieldTypeConfig<T> & {
|
||||
subFields: (keyof T)[];
|
||||
filterableSubFields: (keyof T)[];
|
||||
labelBySubField: Record<keyof T, string>;
|
||||
exampleValue: T;
|
||||
subFields: CompositeSubFieldConfig<T>[];
|
||||
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<FieldCurrencyValue>,
|
||||
[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<FieldEmailsValue>,
|
||||
[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<FieldLinksValue>,
|
||||
[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<FieldPhonesValue>,
|
||||
[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<FieldFullNameValue>,
|
||||
[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<FieldAddressValue>,
|
||||
[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<FieldActorValue>,
|
||||
[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<FieldRichTextV2Value>,
|
||||
} as const satisfies SettingsCompositeFieldTypeConfigArray;
|
||||
|
||||
@ -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<T> = {
|
||||
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<FieldUUidValue>,
|
||||
[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<FieldTextValue>,
|
||||
[FieldMetadataType.NUMERIC]: {
|
||||
label: 'Numeric',
|
||||
Icon: IllustrationIconNumbers,
|
||||
exampleValue: 2000,
|
||||
exampleValues: [2000, 3000, 4000],
|
||||
category: 'Basic',
|
||||
} as const satisfies SettingsFieldTypeConfig<FieldNumberValue>,
|
||||
[FieldMetadataType.NUMBER]: {
|
||||
label: 'Number',
|
||||
Icon: IllustrationIconNumbers,
|
||||
exampleValue: 2000,
|
||||
exampleValues: [2000, 3000, 4000],
|
||||
category: 'Basic',
|
||||
} as const satisfies SettingsFieldTypeConfig<FieldNumberValue>,
|
||||
[FieldMetadataType.BOOLEAN]: {
|
||||
label: 'True/False',
|
||||
Icon: IllustrationIconToggle,
|
||||
exampleValue: true,
|
||||
exampleValues: [true, false, true],
|
||||
category: 'Basic',
|
||||
} as const satisfies SettingsFieldTypeConfig<FieldBooleanValue>,
|
||||
[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<FieldDateTimeValue>,
|
||||
[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<FieldDateValue>,
|
||||
[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<FieldRatingValue>,
|
||||
[FieldMetadataType.RAW_JSON]: {
|
||||
label: 'JSON',
|
||||
Icon: IllustrationIconJson,
|
||||
exampleValue: { key: 'value' },
|
||||
exampleValues: [{ key: 'value1' }, { key: 'value2', key2: 'value2' }, {}],
|
||||
category: 'Advanced',
|
||||
} as const satisfies SettingsFieldTypeConfig<FieldJsonValue>,
|
||||
[FieldMetadataType.ARRAY]: {
|
||||
label: 'Array',
|
||||
Icon: IllustrationIconArray,
|
||||
category: 'Advanced',
|
||||
exampleValue: ['value1', 'value2'],
|
||||
exampleValues: [['value1', 'value2'], ['value3'], []],
|
||||
} as const satisfies SettingsFieldTypeConfig<FieldArrayValue>,
|
||||
};
|
||||
|
||||
@ -51,7 +51,7 @@ describe('getFieldPreviewValue', () => {
|
||||
// Then
|
||||
expect(result).toBe(2000);
|
||||
expect(result).toBe(
|
||||
getSettingsFieldTypeConfig(FieldMetadataType.NUMBER).exampleValue,
|
||||
getSettingsFieldTypeConfig(FieldMetadataType.NUMBER).exampleValues?.[0],
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ export const getAddressFieldPreviewValue = ({
|
||||
FieldMetadataType.ADDRESS,
|
||||
);
|
||||
|
||||
const placeholderDefaultValue = addressFieldTypeConfig.exampleValue;
|
||||
const placeholderDefaultValue = addressFieldTypeConfig.exampleValues?.[0];
|
||||
|
||||
const addressCountry =
|
||||
fieldMetadataItem.defaultValue?.addressCountry &&
|
||||
|
||||
@ -20,7 +20,7 @@ export const getCurrencyFieldPreviewValue = ({
|
||||
FieldMetadataType.CURRENCY,
|
||||
);
|
||||
|
||||
const placeholderDefaultValue = currencyFieldTypeConfig.exampleValue;
|
||||
const placeholderDefaultValue = currencyFieldTypeConfig.exampleValues?.[0];
|
||||
|
||||
return currencyFieldDefaultValueSchema
|
||||
.transform((value) => ({
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 !== ''
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 = ({
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{subFieldNamesThatExistInOptions.map((subFieldName) => (
|
||||
{subFieldsThatExistInOptions.map(({ subFieldName }) => (
|
||||
<MenuItem
|
||||
key={subFieldName}
|
||||
onClick={() => handleSubFieldSelect(subFieldName)}
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export const SpreadsheetMaxRecordImportCapacity = 2000;
|
||||
@ -1,6 +1,7 @@
|
||||
import { ReactSpreadsheetImportContextProvider } from '@/spreadsheet-import/components/ReactSpreadsheetImportContextProvider';
|
||||
import { SpreadSheetImportModalWrapper } from '@/spreadsheet-import/components/SpreadSheetImportModalWrapper';
|
||||
import { SPREADSHEET_IMPORT_MODAL_ID } from '@/spreadsheet-import/constants/SpreadsheetImportModalId';
|
||||
import { SpreadsheetMaxRecordImportCapacity } from '@/spreadsheet-import/constants/SpreadsheetMaxRecordImportCapacity';
|
||||
import { SpreadsheetImportStepperContainer } from '@/spreadsheet-import/steps/components/SpreadsheetImportStepperContainer';
|
||||
import { SpreadsheetImportDialogOptions as SpreadsheetImportProps } from '@/spreadsheet-import/types';
|
||||
|
||||
@ -19,7 +20,7 @@ export const defaultSpreadsheetImportProps: Partial<
|
||||
dateFormat: 'yyyy-mm-dd', // ISO 8601,
|
||||
parseRaw: true,
|
||||
selectHeader: false,
|
||||
maxRecords: 2000,
|
||||
maxRecords: SpreadsheetMaxRecordImportCapacity,
|
||||
} as const;
|
||||
|
||||
export const SpreadsheetImport = <T extends string>(
|
||||
|
||||
@ -3,7 +3,9 @@ import { useState } from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { read, WorkBook } from 'xlsx-ugnis';
|
||||
|
||||
import { SpreadsheetMaxRecordImportCapacity } from '@/spreadsheet-import/constants/SpreadsheetMaxRecordImportCapacity';
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import { useDownloadFakeRecords } from '@/spreadsheet-import/steps/components/UploadStep/hooks/useDownloadFakeRecords';
|
||||
import { readFileAsync } from '@/spreadsheet-import/utils/readFilesAsync';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
@ -83,6 +85,23 @@ const StyledText = styled.span`
|
||||
padding: 16px;
|
||||
`;
|
||||
|
||||
const StyledFooterText = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
bottom: ${({ theme }) => theme.spacing(4)};
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTextAction = styled.span`
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
`;
|
||||
|
||||
type DropZoneProps = {
|
||||
onContinue: (data: WorkBook, file: File) => void;
|
||||
isLoading: boolean;
|
||||
@ -95,6 +114,8 @@ export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => {
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const { downloadSample } = useDownloadFakeRecords();
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
|
||||
noClick: true,
|
||||
noKeyboard: true,
|
||||
@ -157,6 +178,12 @@ export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => {
|
||||
<Trans>Upload .xlsx, .xls or .csv file</Trans>
|
||||
</StyledText>
|
||||
<MainButton onClick={open} title={t`Select file`} />
|
||||
<StyledFooterText>
|
||||
{t`Max import capacity: ${SpreadsheetMaxRecordImportCapacity} records. Otherwise, consider splitting your file or using the API.`}{' '}
|
||||
<StyledTextAction onClick={downloadSample}>
|
||||
{t`Download sample file.`}
|
||||
</StyledTextAction>
|
||||
</StyledFooterText>
|
||||
</>
|
||||
)}
|
||||
</StyledContainer>
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { SpreadsheetImportTable } from '@/spreadsheet-import/components/SpreadsheetImportTable';
|
||||
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
||||
import { generateExampleRow } from '@/spreadsheet-import/utils/generateExampleRow';
|
||||
|
||||
import { generateColumns } from './columns';
|
||||
|
||||
interface ExampleTableProps<T extends string> {
|
||||
fields: SpreadsheetImportFields<T>;
|
||||
}
|
||||
|
||||
export const ExampleTable = <T extends string>({
|
||||
fields,
|
||||
}: ExampleTableProps<T>) => {
|
||||
const data = useMemo(() => generateExampleRow(fields), [fields]);
|
||||
const columns = useMemo(() => generateColumns(fields), [fields]);
|
||||
|
||||
return (
|
||||
<SpreadsheetImportTable
|
||||
rows={data}
|
||||
columns={columns}
|
||||
className={'rdg-example'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,138 @@
|
||||
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
|
||||
import { spreadsheetImportFilterAvailableFieldMetadataItems } from '@/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems.ts';
|
||||
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
||||
import { SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs';
|
||||
import { escapeCSVValue } from '@/spreadsheet-import/utils/escapeCSVValue';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
export const useDownloadFakeRecords = () => {
|
||||
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
|
||||
|
||||
const availableFieldMetadataItems =
|
||||
spreadsheetImportFilterAvailableFieldMetadataItems(
|
||||
objectMetadataItem.fields,
|
||||
);
|
||||
|
||||
const buildTableWithFakeRecords = () => {
|
||||
const headerRow: string[] = [];
|
||||
const bodyRows: string[][] = [[], [], []];
|
||||
|
||||
availableFieldMetadataItems.forEach((field) => {
|
||||
switch (field.type) {
|
||||
case FieldMetadataType.RATING:
|
||||
case FieldMetadataType.ARRAY:
|
||||
case FieldMetadataType.RAW_JSON:
|
||||
case FieldMetadataType.UUID:
|
||||
case FieldMetadataType.DATE_TIME:
|
||||
case FieldMetadataType.DATE:
|
||||
case FieldMetadataType.BOOLEAN:
|
||||
case FieldMetadataType.NUMBER:
|
||||
case FieldMetadataType.TEXT: {
|
||||
headerRow.push(field.label);
|
||||
const exampleValues =
|
||||
SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS[field.type].exampleValues;
|
||||
|
||||
bodyRows.forEach((_, index) => {
|
||||
bodyRows[index].push(exampleValues?.[index] || '');
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case FieldMetadataType.ACTOR:
|
||||
case FieldMetadataType.EMAILS:
|
||||
case FieldMetadataType.CURRENCY:
|
||||
case FieldMetadataType.FULL_NAME:
|
||||
case FieldMetadataType.LINKS:
|
||||
case FieldMetadataType.PHONES:
|
||||
case FieldMetadataType.ADDRESS: {
|
||||
const compositeFieldSettings =
|
||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[field.type];
|
||||
|
||||
const subFields = compositeFieldSettings.subFields.filter(
|
||||
(subField) => subField.isImportable,
|
||||
);
|
||||
|
||||
const exampleValues =
|
||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[field.type].exampleValues;
|
||||
|
||||
headerRow.push(
|
||||
...subFields.map(
|
||||
({ subFieldLabel }) => `${field.label} / ${subFieldLabel}`,
|
||||
),
|
||||
);
|
||||
|
||||
bodyRows.forEach((_, index) => {
|
||||
subFields.forEach(({ subFieldName }) => {
|
||||
bodyRows[index].push(
|
||||
exampleValues?.[index]?.[
|
||||
subFieldName as keyof (typeof exampleValues)[typeof index]
|
||||
] || '',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case FieldMetadataType.RELATION: {
|
||||
headerRow.push(`${field.label} (ID)`);
|
||||
|
||||
const exampleValues =
|
||||
SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS[FieldMetadataType.UUID]
|
||||
.exampleValues;
|
||||
|
||||
bodyRows.forEach((_, index) => {
|
||||
bodyRows[index].push(exampleValues?.[index] || '');
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case FieldMetadataType.MULTI_SELECT:
|
||||
headerRow.push(field.label);
|
||||
|
||||
bodyRows.forEach((_, index) => {
|
||||
bodyRows[index].push(
|
||||
JSON.stringify(
|
||||
field?.options
|
||||
?.map((option) => option?.value)
|
||||
.slice(0, index) || [],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case FieldMetadataType.SELECT:
|
||||
headerRow.push(field.label);
|
||||
|
||||
bodyRows.forEach((_, index) => {
|
||||
bodyRows[index].push(field?.options?.[index]?.value || '');
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return { headerRow, bodyRows };
|
||||
};
|
||||
|
||||
const formatToCsvContent = (rows: string[][]) => {
|
||||
const escapedRows = rows.map((row) => {
|
||||
return row.map((value) => escapeCSVValue(value));
|
||||
});
|
||||
|
||||
const csvContent = [...escapedRows.map((row) => row.join(','))].join('\n');
|
||||
return [csvContent];
|
||||
};
|
||||
|
||||
const downloadSample = () => {
|
||||
const { headerRow, bodyRows } = buildTableWithFakeRecords();
|
||||
const csvContent = formatToCsvContent([headerRow, ...bodyRows]);
|
||||
const blob = new Blob(csvContent, { type: 'text/csv' });
|
||||
saveAs(blob, `${objectMetadataItem.labelPlural.toLowerCase()}-sample.csv`);
|
||||
};
|
||||
|
||||
return { downloadSample };
|
||||
};
|
||||
@ -4,7 +4,9 @@ import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/Componen
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
|
||||
import { stepBarInternalState } from '@/ui/navigation/step-bar/states/stepBarInternalState';
|
||||
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||
import { SpreadsheetImportStepperContainer } from '../SpreadsheetImportStepperContainer';
|
||||
|
||||
const meta: Meta<typeof SpreadsheetImportStepperContainer> = {
|
||||
@ -14,6 +16,8 @@ const meta: Meta<typeof SpreadsheetImportStepperContainer> = {
|
||||
ComponentWithRecoilScopeDecorator,
|
||||
SnackBarDecorator,
|
||||
I18nFrontDecorator,
|
||||
ObjectMetadataItemsDecorator,
|
||||
ContextStoreDecorator,
|
||||
],
|
||||
parameters: {
|
||||
initialRecoilState: {
|
||||
|
||||
@ -8,7 +8,9 @@ import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/Spre
|
||||
import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
|
||||
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
|
||||
const meta: Meta<typeof UploadStep> = {
|
||||
@ -18,6 +20,8 @@ const meta: Meta<typeof UploadStep> = {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
decorators: [
|
||||
ObjectMetadataItemsDecorator,
|
||||
ContextStoreDecorator,
|
||||
(Story) => (
|
||||
<RecoilRoot
|
||||
initializeState={({ set }) => {
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
import { escapeCSVValue } from '@/spreadsheet-import/utils/escapeCSVValue';
|
||||
|
||||
describe('escapeCSVValue', () => {
|
||||
it('should escape values with commas, quotes, newlines and carriage returns', () => {
|
||||
expect(escapeCSVValue('test,test')).toBe('"test,test"');
|
||||
});
|
||||
|
||||
it('should escape array or JSON values', () => {
|
||||
expect(escapeCSVValue(['test', 'test'])).toBe('"[""test"",""test""]"');
|
||||
expect(escapeCSVValue({ test: 'test' })).toBe('"{""test"":""test""}"');
|
||||
});
|
||||
|
||||
it('should escape null values', () => {
|
||||
expect(escapeCSVValue(null)).toBe('');
|
||||
});
|
||||
|
||||
it('should escape simple string value', () => {
|
||||
expect(escapeCSVValue('test')).toBe('test');
|
||||
});
|
||||
|
||||
it('should escape simple number value', () => {
|
||||
expect(escapeCSVValue(1)).toBe('1');
|
||||
});
|
||||
});
|
||||
@ -1,65 +0,0 @@
|
||||
import { SpreadsheetImportField } from '@/spreadsheet-import/types';
|
||||
import { generateExampleRow } from '@/spreadsheet-import/utils/generateExampleRow';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
describe('generateExampleRow', () => {
|
||||
const defaultField: SpreadsheetImportField<'defaultField'> = {
|
||||
key: 'defaultField',
|
||||
Icon: null,
|
||||
label: 'label',
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
},
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
};
|
||||
|
||||
it('should generate an example row from input field type', () => {
|
||||
const fields: SpreadsheetImportField<'defaultField'>[] = [defaultField];
|
||||
|
||||
const result = generateExampleRow(fields);
|
||||
|
||||
expect(result).toStrictEqual([{ defaultField: 'Text' }]);
|
||||
});
|
||||
|
||||
it('should generate an example row from checkbox field type', () => {
|
||||
const fields: SpreadsheetImportField<'defaultField'>[] = [
|
||||
{
|
||||
...defaultField,
|
||||
fieldType: { type: 'checkbox' },
|
||||
fieldMetadataType: FieldMetadataType.BOOLEAN,
|
||||
},
|
||||
];
|
||||
|
||||
const result = generateExampleRow(fields);
|
||||
|
||||
expect(result).toStrictEqual([{ defaultField: 'Boolean' }]);
|
||||
});
|
||||
|
||||
it('should generate an example row from select field type', () => {
|
||||
const fields: SpreadsheetImportField<'defaultField'>[] = [
|
||||
{
|
||||
...defaultField,
|
||||
fieldType: { type: 'select', options: [] },
|
||||
fieldMetadataType: FieldMetadataType.SELECT,
|
||||
},
|
||||
];
|
||||
|
||||
const result = generateExampleRow(fields);
|
||||
|
||||
expect(result).toStrictEqual([{ defaultField: 'Options' }]);
|
||||
});
|
||||
|
||||
it('should generate an example row with provided example values for fields', () => {
|
||||
const fields: SpreadsheetImportField<'defaultField'>[] = [
|
||||
{
|
||||
...defaultField,
|
||||
example: 'Example',
|
||||
fieldMetadataType: FieldMetadataType.TEXT,
|
||||
},
|
||||
];
|
||||
|
||||
const result = generateExampleRow(fields);
|
||||
|
||||
expect(result).toStrictEqual([{ defaultField: 'Example' }]);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,18 @@
|
||||
import { isString } from '@sniptt/guards';
|
||||
|
||||
export const escapeCSVValue = (value: any) => {
|
||||
if (value == null) return '';
|
||||
|
||||
const stringValue = isString(value) ? value : JSON.stringify(value);
|
||||
|
||||
if (
|
||||
stringValue.includes(',') ||
|
||||
stringValue.includes('"') ||
|
||||
stringValue.includes('\n') ||
|
||||
stringValue.includes('\r')
|
||||
) {
|
||||
return `"${stringValue.replace(/"/g, '""')}"`;
|
||||
}
|
||||
|
||||
return stringValue;
|
||||
};
|
||||
@ -1,26 +0,0 @@
|
||||
import {
|
||||
SpreadsheetImportField,
|
||||
SpreadsheetImportFields,
|
||||
} from '@/spreadsheet-import/types';
|
||||
|
||||
const titleMap: Record<
|
||||
SpreadsheetImportField<string>['fieldType']['type'],
|
||||
string
|
||||
> = {
|
||||
checkbox: 'Boolean',
|
||||
select: 'Options',
|
||||
multiSelect: 'Options',
|
||||
input: 'Text',
|
||||
};
|
||||
|
||||
export const generateExampleRow = <T extends string>(
|
||||
fields: SpreadsheetImportFields<T>,
|
||||
) => [
|
||||
fields.reduce(
|
||||
(acc, field) => {
|
||||
acc[field.key as T] = field.example || titleMap[field.fieldType.type];
|
||||
return acc;
|
||||
},
|
||||
{} as Record<T, string>,
|
||||
),
|
||||
];
|
||||
Reference in New Issue
Block a user