add field validation + add other subfields import (#12444)
- Add some subfield imports : primaryLinkLabel / primaryPhoneCallingCode
/ additionalPhones
- Add validation rules for field and subfield
Comments
- Check other validations that can be done
- Refacto on subFieldKey ("...Label")
- Add global tests on validation step -
[issue](https://github.com/twentyhq/core-team-issues/issues/1067)
closes https://github.com/twentyhq/core-team-issues/issues/903
closes https://github.com/twentyhq/core-team-issues/issues/910
closes https://github.com/twentyhq/core-team-issues/issues/985
closes https://github.com/twentyhq/core-team-issues/issues/904
This commit is contained in:
@ -53,17 +53,20 @@ export const COMPOSITE_FIELD_IMPORT_LABELS = {
|
|||||||
primaryLinkUrlLabel:
|
primaryLinkUrlLabel:
|
||||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.LINKS.labelBySubField
|
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.LINKS.labelBySubField
|
||||||
.primaryLinkUrl,
|
.primaryLinkUrl,
|
||||||
|
primaryLinkLabelLabel:
|
||||||
|
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.LINKS.labelBySubField
|
||||||
|
.primaryLinkLabel,
|
||||||
secondaryLinksLabel:
|
secondaryLinksLabel:
|
||||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.LINKS.labelBySubField
|
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.LINKS.labelBySubField
|
||||||
.secondaryLinks,
|
.secondaryLinks,
|
||||||
} satisfies Partial<CompositeFieldLabels<FieldLinksValue>>,
|
} satisfies CompositeFieldLabels<FieldLinksValue>,
|
||||||
[FieldMetadataType.EMAILS]: {
|
[FieldMetadataType.EMAILS]: {
|
||||||
primaryEmailLabel:
|
primaryEmailLabel:
|
||||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.EMAILS.labelBySubField.primaryEmail,
|
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.EMAILS.labelBySubField.primaryEmail,
|
||||||
additionalEmailsLabel:
|
additionalEmailsLabel:
|
||||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.EMAILS.labelBySubField
|
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.EMAILS.labelBySubField
|
||||||
.additionalEmails,
|
.additionalEmails,
|
||||||
} satisfies Partial<CompositeFieldLabels<FieldEmailsValue>>,
|
} satisfies CompositeFieldLabels<FieldEmailsValue>,
|
||||||
[FieldMetadataType.PHONES]: {
|
[FieldMetadataType.PHONES]: {
|
||||||
primaryPhoneCountryCodeLabel:
|
primaryPhoneCountryCodeLabel:
|
||||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.PHONES.labelBySubField
|
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.PHONES.labelBySubField
|
||||||
@ -71,7 +74,13 @@ export const COMPOSITE_FIELD_IMPORT_LABELS = {
|
|||||||
primaryPhoneNumberLabel:
|
primaryPhoneNumberLabel:
|
||||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.PHONES.labelBySubField
|
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.PHONES.labelBySubField
|
||||||
.primaryPhoneNumber,
|
.primaryPhoneNumber,
|
||||||
} satisfies Partial<CompositeFieldLabels<FieldPhonesValue>>,
|
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]: {
|
[FieldMetadataType.RICH_TEXT_V2]: {
|
||||||
blocknoteLabel:
|
blocknoteLabel:
|
||||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.RICH_TEXT_V2.labelBySubField
|
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.RICH_TEXT_V2.labelBySubField
|
||||||
@ -79,7 +88,7 @@ export const COMPOSITE_FIELD_IMPORT_LABELS = {
|
|||||||
markdownLabel:
|
markdownLabel:
|
||||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.RICH_TEXT_V2.labelBySubField
|
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.RICH_TEXT_V2.labelBySubField
|
||||||
.markdown,
|
.markdown,
|
||||||
} satisfies Partial<CompositeFieldLabels<FieldRichTextV2Value>>,
|
} satisfies CompositeFieldLabels<FieldRichTextV2Value>,
|
||||||
[FieldMetadataType.ACTOR]: {
|
[FieldMetadataType.ACTOR]: {
|
||||||
sourceLabel:
|
sourceLabel:
|
||||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.ACTOR.labelBySubField.source,
|
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS.ACTOR.labelBySubField.source,
|
||||||
|
|||||||
@ -3,14 +3,10 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
|||||||
import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-import/constants/CompositeFieldImportLabels';
|
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 { 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';
|
||||||
|
|
||||||
type CompositeFieldType = keyof typeof COMPOSITE_FIELD_IMPORT_LABELS;
|
|
||||||
|
|
||||||
// Helper type for field validation type resolvers
|
|
||||||
type ValidationTypeResolver = (key: string, label: string) => FieldMetadataType;
|
|
||||||
|
|
||||||
export const useBuildAvailableFieldsForImport = () => {
|
export const useBuildAvailableFieldsForImport = () => {
|
||||||
const { getIcon } = useIcons();
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
@ -39,22 +35,21 @@ export const useBuildAvailableFieldsForImport = () => {
|
|||||||
const handleCompositeFieldWithLabels = (
|
const handleCompositeFieldWithLabels = (
|
||||||
fieldMetadataItem: FieldMetadataItem,
|
fieldMetadataItem: FieldMetadataItem,
|
||||||
fieldType: CompositeFieldType,
|
fieldType: CompositeFieldType,
|
||||||
validationTypeResolver?: ValidationTypeResolver,
|
|
||||||
) => {
|
) => {
|
||||||
Object.entries(COMPOSITE_FIELD_IMPORT_LABELS[fieldType]).forEach(
|
Object.entries(COMPOSITE_FIELD_IMPORT_LABELS[fieldType]).forEach(
|
||||||
([key, subFieldLabel]) => {
|
([subFieldKey, subFieldLabel]) => {
|
||||||
const label = `${fieldMetadataItem.label} / ${subFieldLabel}`;
|
const label = `${fieldMetadataItem.label} / ${subFieldLabel}`;
|
||||||
// Use the custom validation type if provided, otherwise use the field's type
|
|
||||||
const validationType = validationTypeResolver
|
|
||||||
? validationTypeResolver(key, subFieldLabel)
|
|
||||||
: fieldMetadataItem.type;
|
|
||||||
|
|
||||||
availableFieldsForImport.push(
|
availableFieldsForImport.push(
|
||||||
createBaseField(fieldMetadataItem, {
|
createBaseField(fieldMetadataItem, {
|
||||||
label,
|
label,
|
||||||
key: `${subFieldLabel} (${fieldMetadataItem.name})`,
|
key: `${subFieldLabel} (${fieldMetadataItem.name})`,
|
||||||
fieldValidationDefinitions:
|
fieldValidationDefinitions:
|
||||||
getSpreadSheetFieldValidationDefinitions(validationType, label),
|
getSpreadSheetFieldValidationDefinitions(
|
||||||
|
fieldMetadataItem.type,
|
||||||
|
label,
|
||||||
|
subFieldKey,
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -84,12 +79,6 @@ export const useBuildAvailableFieldsForImport = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Special validation type resolver for currency fields
|
|
||||||
const currencyValidationResolver: ValidationTypeResolver = (key) =>
|
|
||||||
key === 'amountMicrosLabel'
|
|
||||||
? FieldMetadataType.NUMBER
|
|
||||||
: FieldMetadataType.CURRENCY;
|
|
||||||
|
|
||||||
const fieldTypeHandlers: Record<
|
const fieldTypeHandlers: Record<
|
||||||
string,
|
string,
|
||||||
(fieldMetadataItem: FieldMetadataItem) => void
|
(fieldMetadataItem: FieldMetadataItem) => void
|
||||||
@ -134,7 +123,6 @@ export const useBuildAvailableFieldsForImport = () => {
|
|||||||
handleCompositeFieldWithLabels(
|
handleCompositeFieldWithLabels(
|
||||||
fieldMetadataItem,
|
fieldMetadataItem,
|
||||||
FieldMetadataType.CURRENCY,
|
FieldMetadataType.CURRENCY,
|
||||||
currencyValidationResolver,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[FieldMetadataType.ACTOR]: (fieldMetadataItem) => {
|
[FieldMetadataType.ACTOR]: (fieldMetadataItem) => {
|
||||||
|
|||||||
@ -39,6 +39,7 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
|
|||||||
fieldMetadataItem.isActive &&
|
fieldMetadataItem.isActive &&
|
||||||
(!fieldMetadataItem.isSystem || fieldMetadataItem.name === 'id') &&
|
(!fieldMetadataItem.isSystem || fieldMetadataItem.name === 'id') &&
|
||||||
fieldMetadataItem.name !== 'createdAt' &&
|
fieldMetadataItem.name !== 'createdAt' &&
|
||||||
|
fieldMetadataItem.name !== 'updatedAt' &&
|
||||||
(fieldMetadataItem.type !== FieldMetadataType.RELATION ||
|
(fieldMetadataItem.type !== FieldMetadataType.RELATION ||
|
||||||
fieldMetadataItem.relationDefinition?.direction ===
|
fieldMetadataItem.relationDefinition?.direction ===
|
||||||
RelationDefinitionType.MANY_TO_ONE),
|
RelationDefinitionType.MANY_TO_ONE),
|
||||||
|
|||||||
@ -0,0 +1,408 @@
|
|||||||
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
|
import { buildRecordFromImportedStructuredRow } from '@/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow';
|
||||||
|
import { ImportedStructuredRow } from '@/spreadsheet-import/types';
|
||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
describe('buildRecordFromImportedStructuredRow', () => {
|
||||||
|
it('should successfully build a record from imported structured row', () => {
|
||||||
|
const importedStructuredRow: ImportedStructuredRow<string> = {
|
||||||
|
booleanField: 'true',
|
||||||
|
numberField: '30',
|
||||||
|
multiSelectField: '["tag1", "tag2", "tag3"]',
|
||||||
|
relationField: 'company-123',
|
||||||
|
selectField: 'option1',
|
||||||
|
arrayField: '["item1", "item2", "item3"]',
|
||||||
|
jsonField: '{"key": "value", "nested": {"prop": "data"}}',
|
||||||
|
richTextField: 'Some rich text content',
|
||||||
|
dateField: '2023-12-25',
|
||||||
|
dateTimeField: '2023-12-25T10:30:00Z',
|
||||||
|
ratingField: '4',
|
||||||
|
'BlockNote (richTextField)': 'Rich content in blocknote format',
|
||||||
|
'Markdown (richTextField)': 'Content in markdown format',
|
||||||
|
'First Name (fullNameField)': 'John',
|
||||||
|
'Last Name (fullNameField)': 'Doe',
|
||||||
|
'Amount (currencyField)': '75',
|
||||||
|
'Currency (currencyField)': 'USD',
|
||||||
|
'Address 1 (addressField)': '123 Main St',
|
||||||
|
'Address 2 (addressField)': 'Apt 4B',
|
||||||
|
'City (addressField)': 'New York',
|
||||||
|
'Post Code (addressField)': '10001',
|
||||||
|
'State (addressField)': 'NY',
|
||||||
|
'Country (addressField)': 'USA',
|
||||||
|
'Primary Email (emailField)': 'john.doe@example.com',
|
||||||
|
'Additional Emails (emailField)':
|
||||||
|
'["john.doe+work@example.com", "j.doe@company.com"]',
|
||||||
|
'Primary Phone Number (phoneField)': '+1-555-0123',
|
||||||
|
'Primary Phone Country Code (phoneField)': 'US',
|
||||||
|
'Primary Phone Calling Code (phoneField)': '+1',
|
||||||
|
'Additional Phones (phoneField)':
|
||||||
|
'[{"number": "+1-555-0124", "callingCode": "+1", "countryCode": "US"}]',
|
||||||
|
'Link URL (linksField)': 'https://example.com',
|
||||||
|
'Link Label (linksField)': 'Example Website',
|
||||||
|
'Secondary Links (linksField)':
|
||||||
|
'[{"url": "https://github.com/user", "label": "GitHub"}]',
|
||||||
|
};
|
||||||
|
|
||||||
|
const fields: FieldMetadataItem[] = [
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: 'booleanField',
|
||||||
|
label: 'Boolean Field',
|
||||||
|
type: FieldMetadataType.BOOLEAN,
|
||||||
|
isNullable: true,
|
||||||
|
isActive: true,
|
||||||
|
isCustom: false,
|
||||||
|
isSystem: false,
|
||||||
|
createdAt: '2023-01-01',
|
||||||
|
updatedAt: '2023-01-01',
|
||||||
|
icon: 'IconCheck',
|
||||||
|
description: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
name: 'numberField',
|
||||||
|
label: 'Number Field',
|
||||||
|
type: FieldMetadataType.NUMBER,
|
||||||
|
isNullable: true,
|
||||||
|
isActive: true,
|
||||||
|
isCustom: false,
|
||||||
|
isSystem: false,
|
||||||
|
createdAt: '2023-01-01',
|
||||||
|
updatedAt: '2023-01-01',
|
||||||
|
icon: 'IconNumber',
|
||||||
|
description: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
name: 'multiSelectField',
|
||||||
|
label: 'Multi-Select Field',
|
||||||
|
type: FieldMetadataType.MULTI_SELECT,
|
||||||
|
isNullable: true,
|
||||||
|
isActive: true,
|
||||||
|
isCustom: false,
|
||||||
|
isSystem: false,
|
||||||
|
createdAt: '2023-01-01',
|
||||||
|
updatedAt: '2023-01-01',
|
||||||
|
icon: 'IconTag',
|
||||||
|
description: null,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
value: 'tag1',
|
||||||
|
label: 'Tag 1',
|
||||||
|
color: 'blue',
|
||||||
|
position: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
value: 'tag2',
|
||||||
|
label: 'Tag 2',
|
||||||
|
color: 'red',
|
||||||
|
position: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
value: 'tag3',
|
||||||
|
label: 'Tag 3',
|
||||||
|
color: 'green',
|
||||||
|
position: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6',
|
||||||
|
name: 'relationField',
|
||||||
|
label: 'Relation Field',
|
||||||
|
type: FieldMetadataType.RELATION,
|
||||||
|
isNullable: true,
|
||||||
|
isActive: true,
|
||||||
|
isCustom: false,
|
||||||
|
isSystem: false,
|
||||||
|
createdAt: '2023-01-01',
|
||||||
|
updatedAt: '2023-01-01',
|
||||||
|
icon: 'IconBuilding',
|
||||||
|
description: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '7',
|
||||||
|
name: 'fullNameField',
|
||||||
|
label: 'Full Name Field',
|
||||||
|
type: FieldMetadataType.FULL_NAME,
|
||||||
|
isNullable: true,
|
||||||
|
isActive: true,
|
||||||
|
isCustom: false,
|
||||||
|
isSystem: false,
|
||||||
|
createdAt: '2023-01-01',
|
||||||
|
updatedAt: '2023-01-01',
|
||||||
|
icon: 'IconUser',
|
||||||
|
description: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '8',
|
||||||
|
name: 'currencyField',
|
||||||
|
label: 'Currency Field',
|
||||||
|
type: FieldMetadataType.CURRENCY,
|
||||||
|
isNullable: true,
|
||||||
|
isActive: true,
|
||||||
|
isCustom: false,
|
||||||
|
isSystem: false,
|
||||||
|
createdAt: '2023-01-01',
|
||||||
|
updatedAt: '2023-01-01',
|
||||||
|
icon: 'IconCurrencyDollar',
|
||||||
|
description: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '9',
|
||||||
|
name: 'addressField',
|
||||||
|
label: 'Address Field',
|
||||||
|
type: FieldMetadataType.ADDRESS,
|
||||||
|
isNullable: true,
|
||||||
|
isActive: true,
|
||||||
|
isCustom: false,
|
||||||
|
isSystem: false,
|
||||||
|
createdAt: '2023-01-01',
|
||||||
|
updatedAt: '2023-01-01',
|
||||||
|
icon: 'IconMap',
|
||||||
|
description: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '10',
|
||||||
|
name: 'selectField',
|
||||||
|
label: 'Select Field',
|
||||||
|
type: FieldMetadataType.SELECT,
|
||||||
|
isNullable: true,
|
||||||
|
isActive: true,
|
||||||
|
isCustom: false,
|
||||||
|
isSystem: false,
|
||||||
|
createdAt: '2023-01-01',
|
||||||
|
updatedAt: '2023-01-01',
|
||||||
|
icon: 'IconTag',
|
||||||
|
description: null,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
value: 'option1',
|
||||||
|
label: 'Option 1',
|
||||||
|
color: 'blue',
|
||||||
|
position: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
value: 'option2',
|
||||||
|
label: 'Option 2',
|
||||||
|
color: 'red',
|
||||||
|
position: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '11',
|
||||||
|
name: 'arrayField',
|
||||||
|
label: 'Array Field',
|
||||||
|
type: FieldMetadataType.ARRAY,
|
||||||
|
isNullable: true,
|
||||||
|
isActive: true,
|
||||||
|
isCustom: false,
|
||||||
|
isSystem: false,
|
||||||
|
createdAt: '2023-01-01',
|
||||||
|
updatedAt: '2023-01-01',
|
||||||
|
icon: 'IconBracketsContain',
|
||||||
|
description: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '12',
|
||||||
|
name: 'jsonField',
|
||||||
|
label: 'JSON Field',
|
||||||
|
type: FieldMetadataType.RAW_JSON,
|
||||||
|
isNullable: true,
|
||||||
|
isActive: true,
|
||||||
|
isCustom: false,
|
||||||
|
isSystem: false,
|
||||||
|
createdAt: '2023-01-01',
|
||||||
|
updatedAt: '2023-01-01',
|
||||||
|
icon: 'IconBraces',
|
||||||
|
description: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '13',
|
||||||
|
name: 'phoneField',
|
||||||
|
label: 'Phone Field',
|
||||||
|
type: FieldMetadataType.PHONES,
|
||||||
|
isNullable: true,
|
||||||
|
isActive: true,
|
||||||
|
isCustom: false,
|
||||||
|
isSystem: false,
|
||||||
|
createdAt: '2023-01-01',
|
||||||
|
updatedAt: '2023-01-01',
|
||||||
|
icon: 'IconPhone',
|
||||||
|
description: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '14',
|
||||||
|
name: 'linksField',
|
||||||
|
label: 'Links Field',
|
||||||
|
type: FieldMetadataType.LINKS,
|
||||||
|
isNullable: true,
|
||||||
|
isActive: true,
|
||||||
|
isCustom: false,
|
||||||
|
isSystem: false,
|
||||||
|
createdAt: '2023-01-01',
|
||||||
|
updatedAt: '2023-01-01',
|
||||||
|
icon: 'IconWorld',
|
||||||
|
description: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '15',
|
||||||
|
name: 'createdBy',
|
||||||
|
label: 'Created by',
|
||||||
|
type: FieldMetadataType.ACTOR,
|
||||||
|
isNullable: true,
|
||||||
|
isActive: true,
|
||||||
|
isCustom: false,
|
||||||
|
isSystem: false,
|
||||||
|
createdAt: '2023-01-01',
|
||||||
|
updatedAt: '2023-01-01',
|
||||||
|
icon: 'IconUsers',
|
||||||
|
description: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '16',
|
||||||
|
name: 'richTextField',
|
||||||
|
label: 'Rich Text Field',
|
||||||
|
type: FieldMetadataType.RICH_TEXT_V2,
|
||||||
|
isNullable: true,
|
||||||
|
isActive: true,
|
||||||
|
isCustom: false,
|
||||||
|
isSystem: false,
|
||||||
|
createdAt: '2023-01-01',
|
||||||
|
updatedAt: '2023-01-01',
|
||||||
|
icon: 'IconTextEditor',
|
||||||
|
description: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '17',
|
||||||
|
name: 'dateField',
|
||||||
|
label: 'Date Field',
|
||||||
|
type: FieldMetadataType.DATE,
|
||||||
|
isNullable: true,
|
||||||
|
isActive: true,
|
||||||
|
isCustom: false,
|
||||||
|
isSystem: false,
|
||||||
|
createdAt: '2023-01-01',
|
||||||
|
updatedAt: '2023-01-01',
|
||||||
|
icon: 'IconCalendarEvent',
|
||||||
|
description: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '18',
|
||||||
|
name: 'dateTimeField',
|
||||||
|
label: 'Date Time Field',
|
||||||
|
type: FieldMetadataType.DATE_TIME,
|
||||||
|
isNullable: true,
|
||||||
|
isActive: true,
|
||||||
|
isCustom: false,
|
||||||
|
isSystem: false,
|
||||||
|
createdAt: '2023-01-01',
|
||||||
|
updatedAt: '2023-01-01',
|
||||||
|
icon: 'IconCalendarClock',
|
||||||
|
description: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '19',
|
||||||
|
name: 'ratingField',
|
||||||
|
label: 'Rating Field',
|
||||||
|
type: FieldMetadataType.RATING,
|
||||||
|
isNullable: true,
|
||||||
|
isActive: true,
|
||||||
|
isCustom: false,
|
||||||
|
isSystem: false,
|
||||||
|
createdAt: '2023-01-01',
|
||||||
|
updatedAt: '2023-01-01',
|
||||||
|
icon: 'IconStar',
|
||||||
|
description: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '20',
|
||||||
|
name: 'emailField',
|
||||||
|
label: 'Email Field',
|
||||||
|
type: FieldMetadataType.EMAILS,
|
||||||
|
isNullable: true,
|
||||||
|
isActive: true,
|
||||||
|
isCustom: false,
|
||||||
|
isSystem: false,
|
||||||
|
createdAt: '2023-01-01',
|
||||||
|
updatedAt: '2023-01-01',
|
||||||
|
icon: 'IconMail',
|
||||||
|
description: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = buildRecordFromImportedStructuredRow({
|
||||||
|
importedStructuredRow,
|
||||||
|
fields,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
emailField: {
|
||||||
|
primaryEmail: 'john.doe@example.com',
|
||||||
|
additionalEmails: ['john.doe+work@example.com', 'j.doe@company.com'],
|
||||||
|
},
|
||||||
|
booleanField: true,
|
||||||
|
numberField: 30,
|
||||||
|
multiSelectField: ['tag1', 'tag2', 'tag3'],
|
||||||
|
relationFieldId: 'company-123',
|
||||||
|
selectField: 'option1',
|
||||||
|
arrayField: ['item1', 'item2', 'item3'],
|
||||||
|
jsonField: { key: 'value', nested: { prop: 'data' } },
|
||||||
|
fullNameField: {
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
},
|
||||||
|
currencyField: {
|
||||||
|
amountMicros: 75000000,
|
||||||
|
currencyCode: 'USD',
|
||||||
|
},
|
||||||
|
addressField: {
|
||||||
|
addressStreet1: '123 Main St',
|
||||||
|
addressStreet2: 'Apt 4B',
|
||||||
|
addressCity: 'New York',
|
||||||
|
addressPostcode: '10001',
|
||||||
|
addressState: 'NY',
|
||||||
|
addressCountry: 'USA',
|
||||||
|
},
|
||||||
|
phoneField: {
|
||||||
|
primaryPhoneNumber: '+1-555-0123',
|
||||||
|
primaryPhoneCountryCode: 'US',
|
||||||
|
primaryPhoneCallingCode: '+1',
|
||||||
|
additionalPhones: [
|
||||||
|
{
|
||||||
|
number: '+1-555-0124',
|
||||||
|
callingCode: '+1',
|
||||||
|
countryCode: 'US',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
linksField: {
|
||||||
|
primaryLinkUrl: 'https://example.com',
|
||||||
|
primaryLinkLabel: 'Example Website',
|
||||||
|
secondaryLinks: [
|
||||||
|
{
|
||||||
|
url: 'https://github.com/user',
|
||||||
|
label: 'GitHub',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
createdBy: {
|
||||||
|
source: 'IMPORT',
|
||||||
|
context: {},
|
||||||
|
},
|
||||||
|
richTextField: {
|
||||||
|
blocknote: 'Rich content in blocknote format',
|
||||||
|
markdown: 'Content in markdown format',
|
||||||
|
},
|
||||||
|
dateField: '2023-12-25',
|
||||||
|
dateTimeField: '2023-12-25T10:30:00Z',
|
||||||
|
ratingField: '4',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -20,10 +20,67 @@ type BuildRecordFromImportedStructuredRowArgs = {
|
|||||||
importedStructuredRow: ImportedStructuredRow<any>;
|
importedStructuredRow: ImportedStructuredRow<any>;
|
||||||
fields: FieldMetadataItem[];
|
fields: FieldMetadataItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildRecordFromImportedStructuredRow = ({
|
export const buildRecordFromImportedStructuredRow = ({
|
||||||
fields,
|
fields,
|
||||||
importedStructuredRow,
|
importedStructuredRow,
|
||||||
}: BuildRecordFromImportedStructuredRowArgs) => {
|
}: BuildRecordFromImportedStructuredRowArgs) => {
|
||||||
|
const stringArrayJSONSchema = z
|
||||||
|
.preprocess((value) => {
|
||||||
|
try {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}, z.array(z.string()))
|
||||||
|
.catch([]);
|
||||||
|
|
||||||
|
const linkArrayJSONSchema = z
|
||||||
|
.preprocess(
|
||||||
|
(value) => {
|
||||||
|
try {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
z.array(
|
||||||
|
z.object({
|
||||||
|
label: z.string().nullable(),
|
||||||
|
url: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.catch([]);
|
||||||
|
|
||||||
|
const phoneArrayJSONSchema = z
|
||||||
|
.preprocess(
|
||||||
|
(value) => {
|
||||||
|
try {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
z.array(
|
||||||
|
z.object({
|
||||||
|
number: z.string(),
|
||||||
|
callingCode: z.string(),
|
||||||
|
countryCode: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.catch([]);
|
||||||
|
|
||||||
const recordToBuild: Record<string, any> = {};
|
const recordToBuild: Record<string, any> = {};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -37,9 +94,14 @@ export const buildRecordFromImportedStructuredRow = ({
|
|||||||
},
|
},
|
||||||
CURRENCY: { amountMicrosLabel, currencyCodeLabel },
|
CURRENCY: { amountMicrosLabel, currencyCodeLabel },
|
||||||
FULL_NAME: { firstNameLabel, lastNameLabel },
|
FULL_NAME: { firstNameLabel, lastNameLabel },
|
||||||
LINKS: { primaryLinkUrlLabel },
|
LINKS: { primaryLinkUrlLabel, primaryLinkLabelLabel, secondaryLinksLabel },
|
||||||
EMAILS: { primaryEmailLabel },
|
EMAILS: { primaryEmailLabel, additionalEmailsLabel },
|
||||||
PHONES: { primaryPhoneNumberLabel, primaryPhoneCountryCodeLabel },
|
PHONES: {
|
||||||
|
primaryPhoneNumberLabel,
|
||||||
|
primaryPhoneCountryCodeLabel,
|
||||||
|
primaryPhoneCallingCodeLabel,
|
||||||
|
additionalPhonesLabel,
|
||||||
|
},
|
||||||
RICH_TEXT_V2: { blocknoteLabel, markdownLabel },
|
RICH_TEXT_V2: { blocknoteLabel, markdownLabel },
|
||||||
} = COMPOSITE_FIELD_IMPORT_LABELS;
|
} = COMPOSITE_FIELD_IMPORT_LABELS;
|
||||||
|
|
||||||
@ -115,15 +177,23 @@ export const buildRecordFromImportedStructuredRow = ({
|
|||||||
case FieldMetadataType.LINKS: {
|
case FieldMetadataType.LINKS: {
|
||||||
if (
|
if (
|
||||||
isDefined(
|
isDefined(
|
||||||
importedStructuredRow[`${primaryLinkUrlLabel} (${field.name})`],
|
importedStructuredRow[`${primaryLinkUrlLabel} (${field.name})`] ||
|
||||||
|
importedStructuredRow[
|
||||||
|
`${primaryLinkLabelLabel} (${field.name})`
|
||||||
|
] ||
|
||||||
|
importedStructuredRow[`${secondaryLinksLabel} (${field.name})`],
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
recordToBuild[field.name] = {
|
recordToBuild[field.name] = {
|
||||||
primaryLinkLabel: '',
|
primaryLinkLabel: castToString(
|
||||||
|
importedStructuredRow[`${primaryLinkLabelLabel} (${field.name})`],
|
||||||
|
),
|
||||||
primaryLinkUrl: castToString(
|
primaryLinkUrl: castToString(
|
||||||
importedStructuredRow[`${primaryLinkUrlLabel} (${field.name})`],
|
importedStructuredRow[`${primaryLinkUrlLabel} (${field.name})`],
|
||||||
),
|
),
|
||||||
secondaryLinks: [],
|
secondaryLinks: linkArrayJSONSchema.parse(
|
||||||
|
importedStructuredRow[`${secondaryLinksLabel} (${field.name})`],
|
||||||
|
),
|
||||||
} satisfies FieldLinksValue;
|
} satisfies FieldLinksValue;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -136,7 +206,11 @@ export const buildRecordFromImportedStructuredRow = ({
|
|||||||
] ||
|
] ||
|
||||||
importedStructuredRow[
|
importedStructuredRow[
|
||||||
`${primaryPhoneNumberLabel} (${field.name})`
|
`${primaryPhoneNumberLabel} (${field.name})`
|
||||||
],
|
] ||
|
||||||
|
importedStructuredRow[
|
||||||
|
`${primaryPhoneCallingCodeLabel} (${field.name})`
|
||||||
|
] ||
|
||||||
|
importedStructuredRow[`${additionalPhonesLabel} (${field.name})`],
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
recordToBuild[field.name] = {
|
recordToBuild[field.name] = {
|
||||||
@ -150,7 +224,14 @@ export const buildRecordFromImportedStructuredRow = ({
|
|||||||
`${primaryPhoneNumberLabel} (${field.name})`
|
`${primaryPhoneNumberLabel} (${field.name})`
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
additionalPhones: null,
|
primaryPhoneCallingCode: castToString(
|
||||||
|
importedStructuredRow[
|
||||||
|
`${primaryPhoneCallingCodeLabel} (${field.name})`
|
||||||
|
],
|
||||||
|
),
|
||||||
|
additionalPhones: phoneArrayJSONSchema.parse(
|
||||||
|
importedStructuredRow[`${additionalPhonesLabel} (${field.name})`],
|
||||||
|
),
|
||||||
} satisfies FieldPhonesValue;
|
} satisfies FieldPhonesValue;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -177,13 +258,18 @@ export const buildRecordFromImportedStructuredRow = ({
|
|||||||
if (
|
if (
|
||||||
isDefined(
|
isDefined(
|
||||||
importedStructuredRow[`${primaryEmailLabel} (${field.name})`],
|
importedStructuredRow[`${primaryEmailLabel} (${field.name})`],
|
||||||
|
) ||
|
||||||
|
isDefined(
|
||||||
|
importedStructuredRow[`${additionalEmailsLabel} (${field.name})`],
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
recordToBuild[field.name] = {
|
recordToBuild[field.name] = {
|
||||||
primaryEmail: castToString(
|
primaryEmail: castToString(
|
||||||
importedStructuredRow[`${primaryEmailLabel} (${field.name})`],
|
importedStructuredRow[`${primaryEmailLabel} (${field.name})`],
|
||||||
),
|
),
|
||||||
additionalEmails: null,
|
additionalEmails: stringArrayJSONSchema.parse(
|
||||||
|
importedStructuredRow[`${additionalEmailsLabel} (${field.name})`],
|
||||||
|
),
|
||||||
} satisfies FieldEmailsValue;
|
} satisfies FieldEmailsValue;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -219,19 +305,6 @@ export const buildRecordFromImportedStructuredRow = ({
|
|||||||
break;
|
break;
|
||||||
case FieldMetadataType.ARRAY:
|
case FieldMetadataType.ARRAY:
|
||||||
case FieldMetadataType.MULTI_SELECT: {
|
case FieldMetadataType.MULTI_SELECT: {
|
||||||
const stringArrayJSONSchema = z
|
|
||||||
.preprocess((value) => {
|
|
||||||
try {
|
|
||||||
if (typeof value !== 'string') {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return JSON.parse(value);
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}, z.array(z.string()))
|
|
||||||
.catch([]);
|
|
||||||
|
|
||||||
recordToBuild[field.name] =
|
recordToBuild[field.name] =
|
||||||
stringArrayJSONSchema.parse(importedFieldValue);
|
stringArrayJSONSchema.parse(importedFieldValue);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -1,62 +1,216 @@
|
|||||||
import { FieldLinksValue } from '@/object-record/record-field/types/FieldMetadata';
|
import { emailSchema } from '@/object-record/record-field/validation-schemas/emailSchema';
|
||||||
import { SpreadsheetImportFieldValidationDefinition } from '@/spreadsheet-import/types';
|
import { SpreadsheetImportFieldValidationDefinition } from '@/spreadsheet-import/types';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
|
import { isDate, isString } from '@sniptt/guards';
|
||||||
import { absoluteUrlSchema, isDefined, isValidUuid } from 'twenty-shared/utils';
|
import { absoluteUrlSchema, isDefined, isValidUuid } from 'twenty-shared/utils';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
const getNumberValidationDefinition = (
|
||||||
|
fieldName: string,
|
||||||
|
): SpreadsheetImportFieldValidationDefinition => ({
|
||||||
|
rule: 'function',
|
||||||
|
isValid: (value: string) => !isNaN(+value),
|
||||||
|
errorMessage: `${fieldName} ${t`must be a number`}`,
|
||||||
|
level: 'error',
|
||||||
|
});
|
||||||
|
|
||||||
export const getSpreadSheetFieldValidationDefinitions = (
|
export const getSpreadSheetFieldValidationDefinitions = (
|
||||||
type: FieldMetadataType,
|
type: FieldMetadataType,
|
||||||
fieldName: string,
|
fieldName: string,
|
||||||
|
subFieldKey?: string,
|
||||||
): SpreadsheetImportFieldValidationDefinition[] => {
|
): SpreadsheetImportFieldValidationDefinition[] => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case FieldMetadataType.FULL_NAME:
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
rule: 'object',
|
|
||||||
isValid: ({
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
}: {
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
isDefined(firstName) &&
|
|
||||||
isDefined(lastName) &&
|
|
||||||
typeof firstName === 'string' &&
|
|
||||||
typeof lastName === 'string'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
errorMessage: fieldName + ' must be a full name',
|
|
||||||
level: 'error',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
case FieldMetadataType.NUMBER:
|
case FieldMetadataType.NUMBER:
|
||||||
return [
|
return [getNumberValidationDefinition(fieldName)];
|
||||||
{
|
case FieldMetadataType.UUID:
|
||||||
rule: 'function',
|
|
||||||
isValid: (value: string) => !isNaN(+value),
|
|
||||||
errorMessage: fieldName + ' is not valid',
|
|
||||||
level: 'error',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
case FieldMetadataType.RELATION:
|
case FieldMetadataType.RELATION:
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
rule: 'function',
|
rule: 'function',
|
||||||
isValid: (value: string) => isValidUuid(value),
|
isValid: (value: string) => isValidUuid(value),
|
||||||
errorMessage: fieldName + ' is not valid',
|
errorMessage: `${fieldName} ${t`is not a valid UUID`}`,
|
||||||
level: 'error',
|
level: 'error',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
case FieldMetadataType.CURRENCY:
|
||||||
|
switch (subFieldKey) {
|
||||||
|
case 'amountMicrosLabel':
|
||||||
|
return [getNumberValidationDefinition(fieldName)];
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
case FieldMetadataType.EMAILS:
|
||||||
|
switch (subFieldKey) {
|
||||||
|
case 'primaryEmailLabel':
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
rule: 'function',
|
||||||
|
isValid: (email: string) => emailSchema.safeParse(email).success,
|
||||||
|
errorMessage: `${fieldName} ${t`is not a valid email`}`,
|
||||||
|
level: 'error',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
case 'additionalEmailsLabel':
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
rule: 'function',
|
||||||
|
isValid: (stringifiedAdditionalEmails: string) => {
|
||||||
|
if (!isDefined(stringifiedAdditionalEmails)) return true;
|
||||||
|
try {
|
||||||
|
const additionalEmails = JSON.parse(
|
||||||
|
stringifiedAdditionalEmails,
|
||||||
|
);
|
||||||
|
return additionalEmails.every(
|
||||||
|
(email: string) => emailSchema.safeParse(email).success,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
errorMessage: `${fieldName} ${t`must be an array of valid emails`}`,
|
||||||
|
level: 'error',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
case FieldMetadataType.LINKS:
|
case FieldMetadataType.LINKS:
|
||||||
|
switch (subFieldKey) {
|
||||||
|
case 'primaryLinkUrlLabel':
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
rule: 'function',
|
||||||
|
isValid: (primaryLinkUrl: string) => {
|
||||||
|
if (!isDefined(primaryLinkUrl)) return true;
|
||||||
|
return absoluteUrlSchema.safeParse(primaryLinkUrl).success;
|
||||||
|
},
|
||||||
|
errorMessage: `${fieldName} ${t`is not a valid URL`}`,
|
||||||
|
level: 'error',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
case 'secondaryLinksLabel':
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
rule: 'function',
|
||||||
|
isValid: (stringifiedSecondaryLinks: string) => {
|
||||||
|
if (!isDefined(stringifiedSecondaryLinks)) return true;
|
||||||
|
try {
|
||||||
|
const secondaryLinks = JSON.parse(stringifiedSecondaryLinks);
|
||||||
|
return secondaryLinks.every((link: { url: string }) => {
|
||||||
|
if (!isDefined(link.url)) return true;
|
||||||
|
return absoluteUrlSchema.safeParse(link.url).success;
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
errorMessage: `${fieldName} ${t`must be an array of object with valid url and label (format: '[{"url":"valid.url", "label":"label value")}]'`}`,
|
||||||
|
level: 'error',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
case FieldMetadataType.DATE_TIME:
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
rule: 'object',
|
rule: 'function',
|
||||||
isValid: ({
|
isValid: (value: string) => {
|
||||||
primaryLinkUrl,
|
const date = new Date(value);
|
||||||
}: Pick<FieldLinksValue, 'primaryLinkUrl' | 'secondaryLinks'>) =>
|
return isDate(date) && !isNaN(date.getTime());
|
||||||
absoluteUrlSchema.safeParse(primaryLinkUrl).success,
|
},
|
||||||
errorMessage: fieldName + ' is not valid',
|
errorMessage: `${fieldName} ${t`is not a valid date time (format: '2021-12-01T00:00:00Z')`}`,
|
||||||
|
level: 'error',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
case FieldMetadataType.DATE:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
rule: 'function',
|
||||||
|
isValid: (value: string) => {
|
||||||
|
const date = new Date(value);
|
||||||
|
return isDate(date) && !isNaN(date.getTime());
|
||||||
|
},
|
||||||
|
errorMessage: `${fieldName} ${t`is not a valid date (format: '2021-12-01')`}`,
|
||||||
|
level: 'error',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
case FieldMetadataType.PHONES:
|
||||||
|
switch (subFieldKey) {
|
||||||
|
case 'primaryPhoneNumberLabel':
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
rule: 'regex',
|
||||||
|
value: '^[0-9]+$',
|
||||||
|
errorMessage: `${fieldName} ${t`must contain only numbers`}`,
|
||||||
|
level: 'error',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
case 'additionalPhonesLabel':
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
rule: 'function',
|
||||||
|
isValid: (stringifiedAdditionalPhones: string) => {
|
||||||
|
if (!isDefined(stringifiedAdditionalPhones)) return true;
|
||||||
|
try {
|
||||||
|
const additionalPhones = JSON.parse(
|
||||||
|
stringifiedAdditionalPhones,
|
||||||
|
);
|
||||||
|
return additionalPhones.every(
|
||||||
|
(phone: {
|
||||||
|
number: string;
|
||||||
|
callingCode: string;
|
||||||
|
countryCode: string;
|
||||||
|
}) =>
|
||||||
|
isDefined(phone.number) &&
|
||||||
|
/^[0-9]+$/.test(phone.number) &&
|
||||||
|
isDefined(phone.callingCode) &&
|
||||||
|
isDefined(phone.countryCode),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
errorMessage: `${fieldName} ${t`must be an array of object with valid phone, calling code and country code (format: '[{"number":"123456789", "callingCode":"+33", "countryCode":"FR"}]')`}`,
|
||||||
|
level: 'error',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
case FieldMetadataType.RAW_JSON:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
rule: 'function',
|
||||||
|
isValid: (value: string) => {
|
||||||
|
try {
|
||||||
|
JSON.parse(value);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
errorMessage: `${fieldName} ${t`is not a valid JSON`}`,
|
||||||
|
level: 'error',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
case FieldMetadataType.ARRAY:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
rule: 'function',
|
||||||
|
isValid: (value: string) => {
|
||||||
|
try {
|
||||||
|
const parsedValue = JSON.parse(value);
|
||||||
|
return (
|
||||||
|
Array.isArray(parsedValue) &&
|
||||||
|
parsedValue.every((item: any) => isString(item))
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
errorMessage: `${fieldName} ${t`is not a valid array`}`,
|
||||||
level: 'error',
|
level: 'error',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
} from '@/spreadsheet-import/types';
|
} from '@/spreadsheet-import/types';
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
|
||||||
|
import camelCase from 'lodash.camelcase';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { AppTooltip } from 'twenty-ui/display';
|
import { AppTooltip } from 'twenty-ui/display';
|
||||||
import { Checkbox, CheckboxVariant, Toggle } from 'twenty-ui/input';
|
import { Checkbox, CheckboxVariant, Toggle } from 'twenty-ui/input';
|
||||||
@ -66,6 +67,10 @@ const StyledSelectReadonlyValueContianer = styled.div`
|
|||||||
|
|
||||||
const SELECT_COLUMN_KEY = 'select-row';
|
const SELECT_COLUMN_KEY = 'select-row';
|
||||||
|
|
||||||
|
const formatSafeId = (columnKey: string) => {
|
||||||
|
return camelCase(columnKey.replace('(', '').replace(')', ''));
|
||||||
|
};
|
||||||
|
|
||||||
export const generateColumns = <T extends string>(
|
export const generateColumns = <T extends string>(
|
||||||
fields: SpreadsheetImportFields<T>,
|
fields: SpreadsheetImportFields<T>,
|
||||||
): Column<ImportedStructuredRow<T> & ImportedStructuredRowMetadata>[] => [
|
): Column<ImportedStructuredRow<T> & ImportedStructuredRowMetadata>[] => [
|
||||||
@ -110,13 +115,13 @@ export const generateColumns = <T extends string>(
|
|||||||
resizable: true,
|
resizable: true,
|
||||||
headerRenderer: () => (
|
headerRenderer: () => (
|
||||||
<StyledHeaderContainer>
|
<StyledHeaderContainer>
|
||||||
<StyledHeaderLabel id={`${column.key}`}>
|
<StyledHeaderLabel id={formatSafeId(column.key)}>
|
||||||
{column.label}
|
{column.label}
|
||||||
</StyledHeaderLabel>
|
</StyledHeaderLabel>
|
||||||
{column.description &&
|
{column.description &&
|
||||||
createPortal(
|
createPortal(
|
||||||
<AppTooltip
|
<AppTooltip
|
||||||
anchorSelect={`#${column.key}`}
|
anchorSelect={`#${formatSafeId(column.key)}`}
|
||||||
place="top"
|
place="top"
|
||||||
content={column.description}
|
content={column.description}
|
||||||
/>,
|
/>,
|
||||||
@ -168,7 +173,7 @@ export const generateColumns = <T extends string>(
|
|||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
component = (
|
component = (
|
||||||
<StyledToggleContainer
|
<StyledToggleContainer
|
||||||
id={`${columnKey}-${row.__index}`}
|
id={formatSafeId(`${columnKey}-${row.__index}`)}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}}
|
}}
|
||||||
@ -187,7 +192,9 @@ export const generateColumns = <T extends string>(
|
|||||||
break;
|
break;
|
||||||
case 'select':
|
case 'select':
|
||||||
component = (
|
component = (
|
||||||
<StyledDefaultContainer id={`${columnKey}-${row.__index}`}>
|
<StyledDefaultContainer
|
||||||
|
id={formatSafeId(`${columnKey}-${row.__index}`)}
|
||||||
|
>
|
||||||
{column.fieldType.options.find(
|
{column.fieldType.options.find(
|
||||||
(option) => option.value === row[columnKey as T],
|
(option) => option.value === row[columnKey as T],
|
||||||
)?.label || null}
|
)?.label || null}
|
||||||
@ -196,7 +203,9 @@ export const generateColumns = <T extends string>(
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
component = (
|
component = (
|
||||||
<StyledDefaultContainer id={`${columnKey}-${row.__index}`}>
|
<StyledDefaultContainer
|
||||||
|
id={formatSafeId(`${columnKey}-${row.__index}`)}
|
||||||
|
>
|
||||||
{row[columnKey]}
|
{row[columnKey]}
|
||||||
</StyledDefaultContainer>
|
</StyledDefaultContainer>
|
||||||
);
|
);
|
||||||
@ -208,7 +217,7 @@ export const generateColumns = <T extends string>(
|
|||||||
{component}
|
{component}
|
||||||
{createPortal(
|
{createPortal(
|
||||||
<AppTooltip
|
<AppTooltip
|
||||||
anchorSelect={`#${columnKey}-${row.__index}`}
|
anchorSelect={`#${formatSafeId(`${columnKey}-${row.__index}`)}`}
|
||||||
place="top"
|
place="top"
|
||||||
content={row.__errors?.[columnKey]?.message}
|
content={row.__errors?.[columnKey]?.message}
|
||||||
/>,
|
/>,
|
||||||
|
|||||||
Reference in New Issue
Block a user