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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const subFieldNames =
|
const subFieldNames = SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[
|
||||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[objectFilterDropdownSubMenuFieldType]
|
objectFilterDropdownSubMenuFieldType
|
||||||
.filterableSubFields;
|
].subFields.map((subField) => subField.subFieldName);
|
||||||
|
|
||||||
const subFieldsAreFilterable =
|
const subFieldsAreFilterable =
|
||||||
isDefined(fieldMetadataItemUsedInDropdown) &&
|
isDefined(fieldMetadataItemUsedInDropdown) &&
|
||||||
|
|||||||
@ -3,10 +3,11 @@ import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldTy
|
|||||||
|
|
||||||
export const getCompositeSubFieldLabel = (
|
export const getCompositeSubFieldLabel = (
|
||||||
compositeFieldType: CompositeFieldType,
|
compositeFieldType: CompositeFieldType,
|
||||||
subFieldName: (typeof SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS)[CompositeFieldType]['subFields'][number],
|
subFieldName: (typeof SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS)[CompositeFieldType]['subFields'][number]['subFieldName'],
|
||||||
): string => {
|
): string => {
|
||||||
return (
|
return (
|
||||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[compositeFieldType]
|
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[compositeFieldType].subFields.find(
|
||||||
.labelBySubField as any
|
(subField) => subField.subFieldName === subFieldName,
|
||||||
)[subFieldName];
|
)?.subFieldLabel || ''
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,16 +5,15 @@ export const isExpectedSubFieldName = <
|
|||||||
CompositeFieldTypeSettings extends
|
CompositeFieldTypeSettings extends
|
||||||
typeof SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS,
|
typeof SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS,
|
||||||
PossibleSubFieldsForGivenFieldType extends
|
PossibleSubFieldsForGivenFieldType extends
|
||||||
CompositeFieldTypeSettings[GivenFieldType]['subFields'][number],
|
CompositeFieldTypeSettings[GivenFieldType]['subFields'][number]['subFieldName'],
|
||||||
>(
|
>(
|
||||||
fieldMetadataType: GivenFieldType,
|
fieldMetadataType: GivenFieldType,
|
||||||
subFieldName: PossibleSubFieldsForGivenFieldType,
|
subFieldName: PossibleSubFieldsForGivenFieldType,
|
||||||
subFieldNameToCheck: string | null | undefined,
|
subFieldNameToCheck: string | null | undefined,
|
||||||
): subFieldNameToCheck is PossibleSubFieldsForGivenFieldType => {
|
): subFieldNameToCheck is PossibleSubFieldsForGivenFieldType => {
|
||||||
return (
|
return (
|
||||||
(
|
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[fieldMetadataType].subFields
|
||||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[fieldMetadataType]
|
.map((subField) => subField.subFieldName)
|
||||||
.subFields as string[]
|
.includes(subFieldName) && subFieldName === subFieldNameToCheck
|
||||||
).includes(subFieldName) && subFieldName === subFieldNameToCheck
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { COMPOSITE_FIELD_SUB_FIELD_LABELS } from '@/settings/data-model/constants/CompositeFieldSubFieldLabel';
|
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 { t } from '@lingui/core/macro';
|
||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
@ -59,9 +60,9 @@ export const generateCsv: GenerateExport = ({
|
|||||||
const keys = columnsToExportWithIdColumn.flatMap((col) => {
|
const keys = columnsToExportWithIdColumn.flatMap((col) => {
|
||||||
const column = {
|
const column = {
|
||||||
field: `${col.metadata.fieldName}${col.type === 'RELATION' ? 'Id' : ''}`,
|
field: `${col.metadata.fieldName}${col.type === 'RELATION' ? 'Id' : ''}`,
|
||||||
title: [col.label, col.type === 'RELATION' ? 'Id' : null]
|
title: escapeCSVValue(
|
||||||
.filter(isDefined)
|
`${col.label}${col.type === 'RELATION' ? ' Id' : ''}`,
|
||||||
.join(' '),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
const columnType = col.type;
|
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 { 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 { AvailableFieldForImport } from '@/object-record/spreadsheet-import/types/AvailableFieldForImport';
|
||||||
import { getSpreadSheetFieldValidationDefinitions } from '@/object-record/spreadsheet-import/utils/getSpreadSheetFieldValidationDefinitions';
|
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 { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType';
|
||||||
import { useIcons } from 'twenty-ui/display';
|
import { useIcons } from 'twenty-ui/display';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
@ -36,8 +36,9 @@ export const useBuildAvailableFieldsForImport = () => {
|
|||||||
fieldMetadataItem: FieldMetadataItem,
|
fieldMetadataItem: FieldMetadataItem,
|
||||||
fieldType: CompositeFieldType,
|
fieldType: CompositeFieldType,
|
||||||
) => {
|
) => {
|
||||||
Object.entries(COMPOSITE_FIELD_IMPORT_LABELS[fieldType]).forEach(
|
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[fieldType].subFields.forEach(
|
||||||
([subFieldKey, subFieldLabel]) => {
|
({ subFieldName, subFieldLabel, isImportable }) => {
|
||||||
|
if (!isImportable) return;
|
||||||
const label = `${fieldMetadataItem.label} / ${subFieldLabel}`;
|
const label = `${fieldMetadataItem.label} / ${subFieldLabel}`;
|
||||||
|
|
||||||
availableFieldsForImport.push(
|
availableFieldsForImport.push(
|
||||||
@ -48,7 +49,7 @@ export const useBuildAvailableFieldsForImport = () => {
|
|||||||
getSpreadSheetFieldValidationDefinitions(
|
getSpreadSheetFieldValidationDefinitions(
|
||||||
fieldMetadataItem.type,
|
fieldMetadataItem.type,
|
||||||
label,
|
label,
|
||||||
subFieldKey,
|
subFieldName,
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,11 +2,12 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
|
|||||||
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
|
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
|
||||||
import { useBuildAvailableFieldsForImport } from '@/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport';
|
import { useBuildAvailableFieldsForImport } from '@/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport';
|
||||||
import { buildRecordFromImportedStructuredRow } from '@/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow';
|
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 { useOpenSpreadsheetImportDialog } from '@/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog';
|
||||||
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
|
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
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 = (
|
export const useOpenObjectRecordsSpreadsheetImportDialog = (
|
||||||
objectNameSingular: string,
|
objectNameSingular: string,
|
||||||
@ -30,22 +31,20 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
|
|||||||
'fields' | 'isOpen' | 'onClose'
|
'fields' | 'isOpen' | 'onClose'
|
||||||
>,
|
>,
|
||||||
) => {
|
) => {
|
||||||
const availableFieldMetadataItems = objectMetadataItem.fields
|
//All fields that can be imported (included matchable and auto-filled)
|
||||||
.filter(
|
const availableFieldMetadataItemsToImport =
|
||||||
(fieldMetadataItem) =>
|
spreadsheetImportFilterAvailableFieldMetadataItems(
|
||||||
fieldMetadataItem.isActive &&
|
objectMetadataItem.fields,
|
||||||
(!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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const availableFields = buildAvailableFieldsForImport(
|
const availableFieldMetadataItemsForMatching =
|
||||||
availableFieldMetadataItems,
|
availableFieldMetadataItemsToImport.filter(
|
||||||
|
(fieldMetadataItem) =>
|
||||||
|
fieldMetadataItem.type !== FieldMetadataType.ACTOR,
|
||||||
|
);
|
||||||
|
|
||||||
|
const availableFieldsForMatching = buildAvailableFieldsForImport(
|
||||||
|
availableFieldMetadataItemsForMatching,
|
||||||
);
|
);
|
||||||
|
|
||||||
openSpreadsheetImportDialog({
|
openSpreadsheetImportDialog({
|
||||||
@ -55,7 +54,7 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
|
|||||||
const fieldMapping: Record<string, any> =
|
const fieldMapping: Record<string, any> =
|
||||||
buildRecordFromImportedStructuredRow({
|
buildRecordFromImportedStructuredRow({
|
||||||
importedStructuredRow: record,
|
importedStructuredRow: record,
|
||||||
fields: availableFieldMetadataItems,
|
fields: availableFieldMetadataItemsToImport,
|
||||||
});
|
});
|
||||||
|
|
||||||
return fieldMapping;
|
return fieldMapping;
|
||||||
@ -70,8 +69,8 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fields: availableFields,
|
fields: availableFieldsForMatching,
|
||||||
availableFieldMetadataItems,
|
availableFieldMetadataItems: availableFieldMetadataItemsToImport,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {
|
|||||||
FieldPhonesValue,
|
FieldPhonesValue,
|
||||||
FieldRichTextV2Value,
|
FieldRichTextV2Value,
|
||||||
} from '@/object-record/record-field/types/FieldMetadata';
|
} 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 { ImportedStructuredRow } from '@/spreadsheet-import/types';
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
@ -85,25 +85,35 @@ export const buildRecordFromImportedStructuredRow = ({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
ADDRESS: {
|
ADDRESS: {
|
||||||
addressCityLabel,
|
addressCity: addressCityLabel,
|
||||||
addressCountryLabel,
|
addressCountry: addressCountryLabel,
|
||||||
addressPostcodeLabel,
|
addressPostcode: addressPostcodeLabel,
|
||||||
addressStateLabel,
|
addressState: addressStateLabel,
|
||||||
addressStreet1Label,
|
addressStreet1: addressStreet1Label,
|
||||||
addressStreet2Label,
|
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: {
|
PHONES: {
|
||||||
primaryPhoneNumberLabel,
|
primaryPhoneNumber: primaryPhoneNumberLabel,
|
||||||
primaryPhoneCountryCodeLabel,
|
primaryPhoneCountryCode: primaryPhoneCountryCodeLabel,
|
||||||
primaryPhoneCallingCodeLabel,
|
primaryPhoneCallingCode: primaryPhoneCallingCodeLabel,
|
||||||
additionalPhonesLabel,
|
additionalPhones: additionalPhonesLabel,
|
||||||
},
|
},
|
||||||
RICH_TEXT_V2: { blocknoteLabel, markdownLabel },
|
RICH_TEXT_V2: { blocknote: blocknoteLabel, markdown: markdownLabel },
|
||||||
} = COMPOSITE_FIELD_IMPORT_LABELS;
|
} = COMPOSITE_FIELD_SUB_FIELD_LABELS;
|
||||||
|
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
const importedFieldValue = importedStructuredRow[field.name];
|
const importedFieldValue = importedStructuredRow[field.name];
|
||||||
@ -274,10 +284,18 @@ export const buildRecordFromImportedStructuredRow = ({
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case FieldMetadataType.UUID:
|
||||||
|
if (
|
||||||
|
isDefined(importedFieldValue) &&
|
||||||
|
isNonEmptyString(importedFieldValue)
|
||||||
|
) {
|
||||||
|
recordToBuild[field.name] = importedFieldValue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case FieldMetadataType.RELATION:
|
case FieldMetadataType.RELATION:
|
||||||
if (
|
if (
|
||||||
isDefined(importedFieldValue) &&
|
isDefined(importedFieldValue) &&
|
||||||
(isNonEmptyString(importedFieldValue) || importedFieldValue !== false)
|
isNonEmptyString(importedFieldValue)
|
||||||
) {
|
) {
|
||||||
recordToBuild[field.name + 'Id'] = importedFieldValue;
|
recordToBuild[field.name + 'Id'] = importedFieldValue;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,14 +35,14 @@ export const getSpreadSheetFieldValidationDefinitions = (
|
|||||||
];
|
];
|
||||||
case FieldMetadataType.CURRENCY:
|
case FieldMetadataType.CURRENCY:
|
||||||
switch (subFieldKey) {
|
switch (subFieldKey) {
|
||||||
case 'amountMicrosLabel':
|
case 'amountMicros':
|
||||||
return [getNumberValidationDefinition(fieldName)];
|
return [getNumberValidationDefinition(fieldName)];
|
||||||
default:
|
default:
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
case FieldMetadataType.EMAILS:
|
case FieldMetadataType.EMAILS:
|
||||||
switch (subFieldKey) {
|
switch (subFieldKey) {
|
||||||
case 'primaryEmailLabel':
|
case 'primaryEmail':
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
rule: 'function',
|
rule: 'function',
|
||||||
@ -51,7 +51,7 @@ export const getSpreadSheetFieldValidationDefinitions = (
|
|||||||
level: 'error',
|
level: 'error',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
case 'additionalEmailsLabel':
|
case 'additionalEmails':
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
rule: 'function',
|
rule: 'function',
|
||||||
@ -77,7 +77,7 @@ export const getSpreadSheetFieldValidationDefinitions = (
|
|||||||
}
|
}
|
||||||
case FieldMetadataType.LINKS:
|
case FieldMetadataType.LINKS:
|
||||||
switch (subFieldKey) {
|
switch (subFieldKey) {
|
||||||
case 'primaryLinkUrlLabel':
|
case 'primaryLinkUrl':
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
rule: 'function',
|
rule: 'function',
|
||||||
@ -89,7 +89,7 @@ export const getSpreadSheetFieldValidationDefinitions = (
|
|||||||
level: 'error',
|
level: 'error',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
case 'secondaryLinksLabel':
|
case 'secondaryLinks':
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
rule: 'function',
|
rule: 'function',
|
||||||
@ -139,7 +139,7 @@ export const getSpreadSheetFieldValidationDefinitions = (
|
|||||||
];
|
];
|
||||||
case FieldMetadataType.PHONES:
|
case FieldMetadataType.PHONES:
|
||||||
switch (subFieldKey) {
|
switch (subFieldKey) {
|
||||||
case 'primaryPhoneNumberLabel':
|
case 'primaryPhoneNumber':
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
rule: 'regex',
|
rule: 'regex',
|
||||||
@ -148,7 +148,7 @@ export const getSpreadSheetFieldValidationDefinitions = (
|
|||||||
level: 'error',
|
level: 'error',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
case 'additionalPhonesLabel':
|
case 'additionalPhones':
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
rule: 'function',
|
rule: 'function',
|
||||||
|
|||||||
@ -1,15 +1,19 @@
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
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 = (
|
export const getSubFieldOptionKey = (
|
||||||
fieldMetadataItem: FieldMetadataItem,
|
fieldMetadataItem: FieldMetadataItem,
|
||||||
subFieldName: string,
|
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 = (
|
const subFieldLabel =
|
||||||
(COMPOSITE_FIELD_IMPORT_LABELS as any)[fieldMetadataItem.type] as any
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[fieldMetadataItem.type][subFieldName];
|
||||||
)[subFieldNameLabelKey];
|
|
||||||
|
|
||||||
const subFieldKey = `${subFieldLabel} (${fieldMetadataItem.name})`;
|
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(
|
export const ALL_SUB_FIELDS = COMPOSITE_FIELD_TYPES.flatMap(
|
||||||
(compositeFieldType) =>
|
(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';
|
} from 'twenty-ui/display';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
type CompositeSubFieldConfig<T> = {
|
||||||
|
subFieldName: keyof T;
|
||||||
|
subFieldLabel: string;
|
||||||
|
isImportable: boolean;
|
||||||
|
isFilterable: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type SettingsCompositeFieldTypeConfig<T> = SettingsFieldTypeConfig<T> & {
|
export type SettingsCompositeFieldTypeConfig<T> = SettingsFieldTypeConfig<T> & {
|
||||||
subFields: (keyof T)[];
|
subFields: CompositeSubFieldConfig<T>[];
|
||||||
filterableSubFields: (keyof T)[];
|
exampleValues: [T, T, T];
|
||||||
labelBySubField: Record<keyof T, string>;
|
|
||||||
exampleValue: T;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type SettingsCompositeFieldTypeConfigArray = Record<
|
type SettingsCompositeFieldTypeConfigArray = Record<
|
||||||
@ -41,215 +46,415 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
|
|||||||
[FieldMetadataType.CURRENCY]: {
|
[FieldMetadataType.CURRENCY]: {
|
||||||
label: 'Currency',
|
label: 'Currency',
|
||||||
Icon: IllustrationIconCurrency,
|
Icon: IllustrationIconCurrency,
|
||||||
subFields: ['amountMicros', 'currencyCode'],
|
subFields: [
|
||||||
filterableSubFields: ['amountMicros', 'currencyCode'],
|
{
|
||||||
labelBySubField: {
|
subFieldName: 'amountMicros',
|
||||||
amountMicros:
|
subFieldLabel:
|
||||||
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.CURRENCY]
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.CURRENCY]
|
||||||
.amountMicros,
|
.amountMicros,
|
||||||
currencyCode:
|
isImportable: true,
|
||||||
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.CURRENCY]
|
isFilterable: true,
|
||||||
.currencyCode,
|
},
|
||||||
},
|
{
|
||||||
exampleValue: {
|
subFieldName: 'currencyCode',
|
||||||
amountMicros: 2000000000,
|
subFieldLabel:
|
||||||
currencyCode: CurrencyCode.USD,
|
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',
|
category: 'Basic',
|
||||||
} as const satisfies SettingsCompositeFieldTypeConfig<FieldCurrencyValue>,
|
} as const satisfies SettingsCompositeFieldTypeConfig<FieldCurrencyValue>,
|
||||||
[FieldMetadataType.EMAILS]: {
|
[FieldMetadataType.EMAILS]: {
|
||||||
label: 'Emails',
|
label: 'Emails',
|
||||||
Icon: IllustrationIconMail,
|
Icon: IllustrationIconMail,
|
||||||
subFields: ['primaryEmail', 'additionalEmails'],
|
subFields: [
|
||||||
filterableSubFields: ['primaryEmail', 'additionalEmails'],
|
{
|
||||||
labelBySubField: {
|
subFieldName: 'primaryEmail',
|
||||||
primaryEmail: 'Primary Email',
|
subFieldLabel:
|
||||||
additionalEmails: 'Additional Emails',
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.EMAILS]
|
||||||
},
|
.primaryEmail,
|
||||||
exampleValue: {
|
isImportable: true,
|
||||||
primaryEmail: 'john@twenty.com',
|
isFilterable: true,
|
||||||
additionalEmails: [
|
},
|
||||||
'tim@twenty.com',
|
{
|
||||||
'timapple@twenty.com',
|
subFieldName: 'additionalEmails',
|
||||||
'johnappletim@twenty.com',
|
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',
|
category: 'Basic',
|
||||||
} as const satisfies SettingsCompositeFieldTypeConfig<FieldEmailsValue>,
|
} as const satisfies SettingsCompositeFieldTypeConfig<FieldEmailsValue>,
|
||||||
[FieldMetadataType.LINKS]: {
|
[FieldMetadataType.LINKS]: {
|
||||||
label: 'Links',
|
label: 'Links',
|
||||||
Icon: IllustrationIconLink,
|
Icon: IllustrationIconLink,
|
||||||
exampleValue: {
|
subFields: [
|
||||||
primaryLinkUrl: 'twenty.com',
|
{
|
||||||
primaryLinkLabel: '',
|
subFieldName: 'primaryLinkUrl',
|
||||||
secondaryLinks: [{ url: 'twenty.com', label: 'Twenty' }],
|
subFieldLabel:
|
||||||
},
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.LINKS]
|
||||||
category: 'Basic',
|
.primaryLinkUrl,
|
||||||
subFields: ['primaryLinkUrl', 'primaryLinkLabel', 'secondaryLinks'],
|
isImportable: true,
|
||||||
filterableSubFields: [
|
isFilterable: true,
|
||||||
'primaryLinkUrl',
|
},
|
||||||
'primaryLinkLabel',
|
{
|
||||||
'secondaryLinks',
|
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: {
|
exampleValues: [
|
||||||
primaryLinkUrl:
|
{
|
||||||
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.LINKS]
|
primaryLinkUrl: 'twenty.com',
|
||||||
.primaryLinkUrl,
|
primaryLinkLabel: '',
|
||||||
primaryLinkLabel:
|
secondaryLinks: [{ url: 'twenty.com', label: 'Twenty' }],
|
||||||
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.LINKS]
|
},
|
||||||
.primaryLinkLabel,
|
{
|
||||||
secondaryLinks:
|
primaryLinkUrl: 'github.com/twentyhq/twenty',
|
||||||
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.LINKS]
|
primaryLinkLabel: 'Twenty Repo',
|
||||||
.secondaryLinks,
|
secondaryLinks: [{ url: 'twenty.com', label: '' }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
primaryLinkUrl: 'react.dev',
|
||||||
|
primaryLinkLabel: '',
|
||||||
|
secondaryLinks: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
category: 'Basic',
|
||||||
} as const satisfies SettingsCompositeFieldTypeConfig<FieldLinksValue>,
|
} as const satisfies SettingsCompositeFieldTypeConfig<FieldLinksValue>,
|
||||||
[FieldMetadataType.PHONES]: {
|
[FieldMetadataType.PHONES]: {
|
||||||
label: 'Phones',
|
label: 'Phones',
|
||||||
Icon: IllustrationIconPhone,
|
Icon: IllustrationIconPhone,
|
||||||
exampleValue: {
|
|
||||||
primaryPhoneCallingCode: '+33',
|
|
||||||
primaryPhoneCountryCode: 'FR',
|
|
||||||
primaryPhoneNumber: '789012345',
|
|
||||||
additionalPhones: [
|
|
||||||
{ number: '617272323', callingCode: '+33', countryCode: 'FR' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
subFields: [
|
subFields: [
|
||||||
'primaryPhoneNumber',
|
{
|
||||||
'primaryPhoneCountryCode',
|
subFieldName: 'primaryPhoneCallingCode',
|
||||||
'primaryPhoneCallingCode',
|
subFieldLabel:
|
||||||
'additionalPhones',
|
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: [
|
exampleValues: [
|
||||||
'primaryPhoneNumber',
|
{
|
||||||
'primaryPhoneCallingCode',
|
primaryPhoneCallingCode: '+33',
|
||||||
'additionalPhones',
|
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',
|
category: 'Basic',
|
||||||
} as const satisfies SettingsCompositeFieldTypeConfig<FieldPhonesValue>,
|
} as const satisfies SettingsCompositeFieldTypeConfig<FieldPhonesValue>,
|
||||||
[FieldMetadataType.FULL_NAME]: {
|
[FieldMetadataType.FULL_NAME]: {
|
||||||
label: 'Full Name',
|
label: 'Full Name',
|
||||||
Icon: IllustrationIconUser,
|
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',
|
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>,
|
} as const satisfies SettingsCompositeFieldTypeConfig<FieldFullNameValue>,
|
||||||
[FieldMetadataType.ADDRESS]: {
|
[FieldMetadataType.ADDRESS]: {
|
||||||
label: 'Address',
|
label: 'Address',
|
||||||
Icon: IllustrationIconMap,
|
Icon: IllustrationIconMap,
|
||||||
subFields: [
|
subFields: [
|
||||||
'addressStreet1',
|
{
|
||||||
'addressStreet2',
|
subFieldName: 'addressStreet1',
|
||||||
'addressCity',
|
subFieldLabel:
|
||||||
'addressState',
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ADDRESS]
|
||||||
'addressCountry',
|
.addressStreet1,
|
||||||
'addressPostcode',
|
isImportable: true,
|
||||||
'addressLat',
|
isFilterable: true,
|
||||||
'addressLng',
|
},
|
||||||
|
{
|
||||||
|
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: [
|
exampleValues: [
|
||||||
'addressStreet1',
|
{
|
||||||
'addressStreet2',
|
addressStreet1: '456 Oak Street',
|
||||||
'addressCity',
|
addressStreet2: '',
|
||||||
'addressState',
|
addressCity: 'Springfield',
|
||||||
'addressCountry',
|
addressState: 'California',
|
||||||
'addressPostcode',
|
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',
|
category: 'Basic',
|
||||||
} as const satisfies SettingsCompositeFieldTypeConfig<FieldAddressValue>,
|
} as const satisfies SettingsCompositeFieldTypeConfig<FieldAddressValue>,
|
||||||
[FieldMetadataType.ACTOR]: {
|
[FieldMetadataType.ACTOR]: {
|
||||||
label: 'Actor',
|
label: 'Actor',
|
||||||
Icon: IllustrationIconSetting,
|
Icon: IllustrationIconSetting,
|
||||||
category: 'Basic',
|
category: 'Basic',
|
||||||
subFields: ['source', 'name'],
|
subFields: [
|
||||||
filterableSubFields: ['source', 'name'],
|
{
|
||||||
labelBySubField: {
|
subFieldName: 'source',
|
||||||
source: COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ACTOR].source,
|
subFieldLabel:
|
||||||
name: COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ACTOR].name,
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ACTOR].source,
|
||||||
workspaceMemberId:
|
isImportable: true,
|
||||||
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ACTOR]
|
isFilterable: true,
|
||||||
.workspaceMemberId,
|
},
|
||||||
context:
|
{
|
||||||
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ACTOR].context,
|
subFieldName: 'name',
|
||||||
},
|
subFieldLabel:
|
||||||
exampleValue: {
|
COMPOSITE_FIELD_SUB_FIELD_LABELS[FieldMetadataType.ACTOR].name,
|
||||||
source: 'IMPORT',
|
isImportable: true,
|
||||||
name: 'name',
|
isFilterable: true,
|
||||||
workspaceMemberId: 'id',
|
},
|
||||||
context: { provider: ConnectedAccountProvider.GOOGLE },
|
{
|
||||||
},
|
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>,
|
} as const satisfies SettingsCompositeFieldTypeConfig<FieldActorValue>,
|
||||||
[FieldMetadataType.RICH_TEXT_V2]: {
|
[FieldMetadataType.RICH_TEXT_V2]: {
|
||||||
label: 'Rich Text',
|
label: 'Rich Text',
|
||||||
Icon: IllustrationIconText,
|
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',
|
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 SettingsCompositeFieldTypeConfig<FieldRichTextV2Value>,
|
||||||
} as const satisfies SettingsCompositeFieldTypeConfigArray;
|
} as const satisfies SettingsCompositeFieldTypeConfigArray;
|
||||||
|
|||||||
@ -15,7 +15,6 @@ import {
|
|||||||
import { DEFAULT_DATE_VALUE } from '@/settings/data-model/constants/DefaultDateValue';
|
import { DEFAULT_DATE_VALUE } from '@/settings/data-model/constants/DefaultDateValue';
|
||||||
import { SettingsFieldTypeCategoryType } from '@/settings/data-model/types/SettingsFieldTypeCategoryType';
|
import { SettingsFieldTypeCategoryType } from '@/settings/data-model/types/SettingsFieldTypeCategoryType';
|
||||||
import { SettingsNonCompositeFieldType } from '@/settings/data-model/types/SettingsNonCompositeFieldType';
|
import { SettingsNonCompositeFieldType } from '@/settings/data-model/types/SettingsNonCompositeFieldType';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
|
||||||
import {
|
import {
|
||||||
IconComponent,
|
IconComponent,
|
||||||
IllustrationIconArray,
|
IllustrationIconArray,
|
||||||
@ -31,13 +30,14 @@ import {
|
|||||||
IllustrationIconToggle,
|
IllustrationIconToggle,
|
||||||
IllustrationIconUid,
|
IllustrationIconUid,
|
||||||
} from 'twenty-ui/display';
|
} from 'twenty-ui/display';
|
||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
DEFAULT_DATE_VALUE.setFullYear(DEFAULT_DATE_VALUE.getFullYear() + 2);
|
DEFAULT_DATE_VALUE.setFullYear(DEFAULT_DATE_VALUE.getFullYear() + 2);
|
||||||
|
|
||||||
export type SettingsFieldTypeConfig<T> = {
|
export type SettingsFieldTypeConfig<T> = {
|
||||||
label: string;
|
label: string;
|
||||||
Icon: IconComponent;
|
Icon: IconComponent;
|
||||||
exampleValue?: T;
|
exampleValues?: [T, T, T];
|
||||||
category: SettingsFieldTypeCategoryType;
|
category: SettingsFieldTypeCategoryType;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -52,44 +52,59 @@ export const SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS: SettingsNonCompositeFiel
|
|||||||
[FieldMetadataType.UUID]: {
|
[FieldMetadataType.UUID]: {
|
||||||
label: 'Unique ID',
|
label: 'Unique ID',
|
||||||
Icon: IllustrationIconUid,
|
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',
|
category: 'Advanced',
|
||||||
} as const satisfies SettingsFieldTypeConfig<FieldUUidValue>,
|
} as const satisfies SettingsFieldTypeConfig<FieldUUidValue>,
|
||||||
[FieldMetadataType.TEXT]: {
|
[FieldMetadataType.TEXT]: {
|
||||||
label: 'Text',
|
label: 'Text',
|
||||||
Icon: IllustrationIconText,
|
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.',
|
'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',
|
category: 'Basic',
|
||||||
} as const satisfies SettingsFieldTypeConfig<FieldTextValue>,
|
} as const satisfies SettingsFieldTypeConfig<FieldTextValue>,
|
||||||
[FieldMetadataType.NUMERIC]: {
|
[FieldMetadataType.NUMERIC]: {
|
||||||
label: 'Numeric',
|
label: 'Numeric',
|
||||||
Icon: IllustrationIconNumbers,
|
Icon: IllustrationIconNumbers,
|
||||||
exampleValue: 2000,
|
exampleValues: [2000, 3000, 4000],
|
||||||
category: 'Basic',
|
category: 'Basic',
|
||||||
} as const satisfies SettingsFieldTypeConfig<FieldNumberValue>,
|
} as const satisfies SettingsFieldTypeConfig<FieldNumberValue>,
|
||||||
[FieldMetadataType.NUMBER]: {
|
[FieldMetadataType.NUMBER]: {
|
||||||
label: 'Number',
|
label: 'Number',
|
||||||
Icon: IllustrationIconNumbers,
|
Icon: IllustrationIconNumbers,
|
||||||
exampleValue: 2000,
|
exampleValues: [2000, 3000, 4000],
|
||||||
category: 'Basic',
|
category: 'Basic',
|
||||||
} as const satisfies SettingsFieldTypeConfig<FieldNumberValue>,
|
} as const satisfies SettingsFieldTypeConfig<FieldNumberValue>,
|
||||||
[FieldMetadataType.BOOLEAN]: {
|
[FieldMetadataType.BOOLEAN]: {
|
||||||
label: 'True/False',
|
label: 'True/False',
|
||||||
Icon: IllustrationIconToggle,
|
Icon: IllustrationIconToggle,
|
||||||
exampleValue: true,
|
exampleValues: [true, false, true],
|
||||||
category: 'Basic',
|
category: 'Basic',
|
||||||
} as const satisfies SettingsFieldTypeConfig<FieldBooleanValue>,
|
} as const satisfies SettingsFieldTypeConfig<FieldBooleanValue>,
|
||||||
[FieldMetadataType.DATE_TIME]: {
|
[FieldMetadataType.DATE_TIME]: {
|
||||||
label: 'Date and Time',
|
label: 'Date and Time',
|
||||||
Icon: IllustrationIconCalendarTime,
|
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',
|
category: 'Basic',
|
||||||
} as const satisfies SettingsFieldTypeConfig<FieldDateTimeValue>,
|
} as const satisfies SettingsFieldTypeConfig<FieldDateTimeValue>,
|
||||||
[FieldMetadataType.DATE]: {
|
[FieldMetadataType.DATE]: {
|
||||||
label: 'Date',
|
label: 'Date',
|
||||||
Icon: IllustrationIconCalendarEvent,
|
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',
|
category: 'Basic',
|
||||||
} as const satisfies SettingsFieldTypeConfig<FieldDateValue>,
|
} as const satisfies SettingsFieldTypeConfig<FieldDateValue>,
|
||||||
[FieldMetadataType.SELECT]: {
|
[FieldMetadataType.SELECT]: {
|
||||||
@ -110,19 +125,19 @@ export const SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS: SettingsNonCompositeFiel
|
|||||||
[FieldMetadataType.RATING]: {
|
[FieldMetadataType.RATING]: {
|
||||||
label: 'Rating',
|
label: 'Rating',
|
||||||
Icon: IllustrationIconStar,
|
Icon: IllustrationIconStar,
|
||||||
exampleValue: 'RATING_3',
|
exampleValues: ['RATING_3', 'RATING_4', 'RATING_5'],
|
||||||
category: 'Basic',
|
category: 'Basic',
|
||||||
} as const satisfies SettingsFieldTypeConfig<FieldRatingValue>,
|
} as const satisfies SettingsFieldTypeConfig<FieldRatingValue>,
|
||||||
[FieldMetadataType.RAW_JSON]: {
|
[FieldMetadataType.RAW_JSON]: {
|
||||||
label: 'JSON',
|
label: 'JSON',
|
||||||
Icon: IllustrationIconJson,
|
Icon: IllustrationIconJson,
|
||||||
exampleValue: { key: 'value' },
|
exampleValues: [{ key: 'value1' }, { key: 'value2', key2: 'value2' }, {}],
|
||||||
category: 'Advanced',
|
category: 'Advanced',
|
||||||
} as const satisfies SettingsFieldTypeConfig<FieldJsonValue>,
|
} as const satisfies SettingsFieldTypeConfig<FieldJsonValue>,
|
||||||
[FieldMetadataType.ARRAY]: {
|
[FieldMetadataType.ARRAY]: {
|
||||||
label: 'Array',
|
label: 'Array',
|
||||||
Icon: IllustrationIconArray,
|
Icon: IllustrationIconArray,
|
||||||
category: 'Advanced',
|
category: 'Advanced',
|
||||||
exampleValue: ['value1', 'value2'],
|
exampleValues: [['value1', 'value2'], ['value3'], []],
|
||||||
} as const satisfies SettingsFieldTypeConfig<FieldArrayValue>,
|
} as const satisfies SettingsFieldTypeConfig<FieldArrayValue>,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -51,7 +51,7 @@ describe('getFieldPreviewValue', () => {
|
|||||||
// Then
|
// Then
|
||||||
expect(result).toBe(2000);
|
expect(result).toBe(2000);
|
||||||
expect(result).toBe(
|
expect(result).toBe(
|
||||||
getSettingsFieldTypeConfig(FieldMetadataType.NUMBER).exampleValue,
|
getSettingsFieldTypeConfig(FieldMetadataType.NUMBER).exampleValues?.[0],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export const getAddressFieldPreviewValue = ({
|
|||||||
FieldMetadataType.ADDRESS,
|
FieldMetadataType.ADDRESS,
|
||||||
);
|
);
|
||||||
|
|
||||||
const placeholderDefaultValue = addressFieldTypeConfig.exampleValue;
|
const placeholderDefaultValue = addressFieldTypeConfig.exampleValues?.[0];
|
||||||
|
|
||||||
const addressCountry =
|
const addressCountry =
|
||||||
fieldMetadataItem.defaultValue?.addressCountry &&
|
fieldMetadataItem.defaultValue?.addressCountry &&
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export const getCurrencyFieldPreviewValue = ({
|
|||||||
FieldMetadataType.CURRENCY,
|
FieldMetadataType.CURRENCY,
|
||||||
);
|
);
|
||||||
|
|
||||||
const placeholderDefaultValue = currencyFieldTypeConfig.exampleValue;
|
const placeholderDefaultValue = currencyFieldTypeConfig.exampleValues?.[0];
|
||||||
|
|
||||||
return currencyFieldDefaultValueSchema
|
return currencyFieldDefaultValueSchema
|
||||||
.transform((value) => ({
|
.transform((value) => ({
|
||||||
|
|||||||
@ -31,10 +31,10 @@ export const getFieldPreviewValue = ({
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
isDefined(fieldTypeConfig) &&
|
isDefined(fieldTypeConfig) &&
|
||||||
'exampleValue' in fieldTypeConfig &&
|
'exampleValues' in fieldTypeConfig &&
|
||||||
isDefined(fieldTypeConfig.exampleValue)
|
isDefined(fieldTypeConfig.exampleValues?.[0])
|
||||||
) {
|
) {
|
||||||
return fieldTypeConfig.exampleValue;
|
return fieldTypeConfig.exampleValues?.[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export const getPhonesFieldPreviewValue = ({
|
|||||||
FieldMetadataType.PHONES,
|
FieldMetadataType.PHONES,
|
||||||
);
|
);
|
||||||
|
|
||||||
const placeholderDefaultValue = phonesFieldTypeConfig.exampleValue;
|
const placeholderDefaultValue = phonesFieldTypeConfig.exampleValues?.[0];
|
||||||
const primaryPhoneCountryCode =
|
const primaryPhoneCountryCode =
|
||||||
fieldMetadataItem.defaultValue?.primaryPhoneCountryCode &&
|
fieldMetadataItem.defaultValue?.primaryPhoneCountryCode &&
|
||||||
fieldMetadataItem.defaultValue.primaryPhoneCountryCode !== ''
|
fieldMetadataItem.defaultValue.primaryPhoneCountryCode !== ''
|
||||||
|
|||||||
@ -5,9 +5,10 @@ import { COMPOSITE_FIELD_TYPES } from '@/settings/data-model/types/CompositeFiel
|
|||||||
export const isValidSubFieldName = (
|
export const isValidSubFieldName = (
|
||||||
subFieldName: string,
|
subFieldName: string,
|
||||||
): subFieldName is CompositeFieldSubFieldName => {
|
): subFieldName is CompositeFieldSubFieldName => {
|
||||||
const allSubFields = COMPOSITE_FIELD_TYPES.flatMap(
|
const allSubFields = COMPOSITE_FIELD_TYPES.flatMap((compositeFieldType) =>
|
||||||
(compositeFieldType) =>
|
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[compositeFieldType].subFields.map(
|
||||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[compositeFieldType].subFields,
|
(subField) => subField.subFieldName,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return allSubFields.includes(subFieldName as any);
|
return allSubFields.includes(subFieldName as any);
|
||||||
|
|||||||
@ -58,8 +58,8 @@ export const MatchColumnSelectSubFieldSelectDropdownContent = ({
|
|||||||
const fieldMetadataItemSettings =
|
const fieldMetadataItemSettings =
|
||||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[fieldMetadataItem.type];
|
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[fieldMetadataItem.type];
|
||||||
|
|
||||||
const subFieldNamesThatExistInOptions = fieldMetadataItemSettings.subFields
|
const subFieldsThatExistInOptions = fieldMetadataItemSettings.subFields
|
||||||
.filter((subFieldName) => {
|
.filter(({ subFieldName }) => {
|
||||||
const optionKey = getSubFieldOptionKey(fieldMetadataItem, subFieldName);
|
const optionKey = getSubFieldOptionKey(fieldMetadataItem, subFieldName);
|
||||||
|
|
||||||
const correspondingOption = options.find(
|
const correspondingOption = options.find(
|
||||||
@ -68,7 +68,7 @@ export const MatchColumnSelectSubFieldSelectDropdownContent = ({
|
|||||||
|
|
||||||
return isDefined(correspondingOption);
|
return isDefined(correspondingOption);
|
||||||
})
|
})
|
||||||
.filter((subFieldName) =>
|
.filter(({ subFieldName }) =>
|
||||||
getCompositeSubFieldLabel(
|
getCompositeSubFieldLabel(
|
||||||
fieldMetadataItem.type as CompositeFieldType,
|
fieldMetadataItem.type as CompositeFieldType,
|
||||||
subFieldName,
|
subFieldName,
|
||||||
@ -96,7 +96,7 @@ export const MatchColumnSelectSubFieldSelectDropdownContent = ({
|
|||||||
/>
|
/>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
<DropdownMenuItemsContainer hasMaxHeight>
|
||||||
{subFieldNamesThatExistInOptions.map((subFieldName) => (
|
{subFieldsThatExistInOptions.map(({ subFieldName }) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={subFieldName}
|
key={subFieldName}
|
||||||
onClick={() => handleSubFieldSelect(subFieldName)}
|
onClick={() => handleSubFieldSelect(subFieldName)}
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export const SpreadsheetMaxRecordImportCapacity = 2000;
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { ReactSpreadsheetImportContextProvider } from '@/spreadsheet-import/components/ReactSpreadsheetImportContextProvider';
|
import { ReactSpreadsheetImportContextProvider } from '@/spreadsheet-import/components/ReactSpreadsheetImportContextProvider';
|
||||||
import { SpreadSheetImportModalWrapper } from '@/spreadsheet-import/components/SpreadSheetImportModalWrapper';
|
import { SpreadSheetImportModalWrapper } from '@/spreadsheet-import/components/SpreadSheetImportModalWrapper';
|
||||||
import { SPREADSHEET_IMPORT_MODAL_ID } from '@/spreadsheet-import/constants/SpreadsheetImportModalId';
|
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 { SpreadsheetImportStepperContainer } from '@/spreadsheet-import/steps/components/SpreadsheetImportStepperContainer';
|
||||||
import { SpreadsheetImportDialogOptions as SpreadsheetImportProps } from '@/spreadsheet-import/types';
|
import { SpreadsheetImportDialogOptions as SpreadsheetImportProps } from '@/spreadsheet-import/types';
|
||||||
|
|
||||||
@ -19,7 +20,7 @@ export const defaultSpreadsheetImportProps: Partial<
|
|||||||
dateFormat: 'yyyy-mm-dd', // ISO 8601,
|
dateFormat: 'yyyy-mm-dd', // ISO 8601,
|
||||||
parseRaw: true,
|
parseRaw: true,
|
||||||
selectHeader: false,
|
selectHeader: false,
|
||||||
maxRecords: 2000,
|
maxRecords: SpreadsheetMaxRecordImportCapacity,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const SpreadsheetImport = <T extends string>(
|
export const SpreadsheetImport = <T extends string>(
|
||||||
|
|||||||
@ -3,7 +3,9 @@ import { useState } from 'react';
|
|||||||
import { useDropzone } from 'react-dropzone';
|
import { useDropzone } from 'react-dropzone';
|
||||||
import { read, WorkBook } from 'xlsx-ugnis';
|
import { read, WorkBook } from 'xlsx-ugnis';
|
||||||
|
|
||||||
|
import { SpreadsheetMaxRecordImportCapacity } from '@/spreadsheet-import/constants/SpreadsheetMaxRecordImportCapacity';
|
||||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
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 { readFileAsync } from '@/spreadsheet-import/utils/readFilesAsync';
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
@ -83,6 +85,23 @@ const StyledText = styled.span`
|
|||||||
padding: 16px;
|
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 = {
|
type DropZoneProps = {
|
||||||
onContinue: (data: WorkBook, file: File) => void;
|
onContinue: (data: WorkBook, file: File) => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
@ -95,6 +114,8 @@ export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => {
|
|||||||
|
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
|
||||||
|
const { downloadSample } = useDownloadFakeRecords();
|
||||||
|
|
||||||
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
|
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
|
||||||
noClick: true,
|
noClick: true,
|
||||||
noKeyboard: true,
|
noKeyboard: true,
|
||||||
@ -157,6 +178,12 @@ export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => {
|
|||||||
<Trans>Upload .xlsx, .xls or .csv file</Trans>
|
<Trans>Upload .xlsx, .xls or .csv file</Trans>
|
||||||
</StyledText>
|
</StyledText>
|
||||||
<MainButton onClick={open} title={t`Select file`} />
|
<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>
|
</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 { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||||
|
|
||||||
import { stepBarInternalState } from '@/ui/navigation/step-bar/states/stepBarInternalState';
|
import { stepBarInternalState } from '@/ui/navigation/step-bar/states/stepBarInternalState';
|
||||||
|
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
|
||||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||||
|
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||||
import { SpreadsheetImportStepperContainer } from '../SpreadsheetImportStepperContainer';
|
import { SpreadsheetImportStepperContainer } from '../SpreadsheetImportStepperContainer';
|
||||||
|
|
||||||
const meta: Meta<typeof SpreadsheetImportStepperContainer> = {
|
const meta: Meta<typeof SpreadsheetImportStepperContainer> = {
|
||||||
@ -14,6 +16,8 @@ const meta: Meta<typeof SpreadsheetImportStepperContainer> = {
|
|||||||
ComponentWithRecoilScopeDecorator,
|
ComponentWithRecoilScopeDecorator,
|
||||||
SnackBarDecorator,
|
SnackBarDecorator,
|
||||||
I18nFrontDecorator,
|
I18nFrontDecorator,
|
||||||
|
ObjectMetadataItemsDecorator,
|
||||||
|
ContextStoreDecorator,
|
||||||
],
|
],
|
||||||
parameters: {
|
parameters: {
|
||||||
initialRecoilState: {
|
initialRecoilState: {
|
||||||
|
|||||||
@ -8,7 +8,9 @@ import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/Spre
|
|||||||
import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
|
import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
|
||||||
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
|
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
|
||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
|
||||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||||
|
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||||
|
|
||||||
const meta: Meta<typeof UploadStep> = {
|
const meta: Meta<typeof UploadStep> = {
|
||||||
@ -18,6 +20,8 @@ const meta: Meta<typeof UploadStep> = {
|
|||||||
layout: 'fullscreen',
|
layout: 'fullscreen',
|
||||||
},
|
},
|
||||||
decorators: [
|
decorators: [
|
||||||
|
ObjectMetadataItemsDecorator,
|
||||||
|
ContextStoreDecorator,
|
||||||
(Story) => (
|
(Story) => (
|
||||||
<RecoilRoot
|
<RecoilRoot
|
||||||
initializeState={({ set }) => {
|
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