Custom object import csv (#3756)

* poc custom object import csv

* fix fullname

* lint

* add relation Ids, fix label full name, add simple test

* mock missing fields?

* - fix test

* validate uuid, fix key in column dropdown, don't save non set composite fields, allow only import relations where toRelationMetadata
This commit is contained in:
brendanlaschke 2024-02-06 16:22:39 +01:00 committed by GitHub
parent 0096e60489
commit 7b8fffc3b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 241 additions and 663 deletions

View File

@ -1,83 +0,0 @@
import { Company } from '@/companies/types/Company';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport';
import { SpreadsheetOptions } from '@/spreadsheet-import/types';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { fieldsForCompany } from '../utils/fieldsForCompany';
export type FieldCompanyMapping = (typeof fieldsForCompany)[number]['key'];
export const useSpreadsheetCompanyImport = () => {
const { openSpreadsheetImport } = useSpreadsheetImport<FieldCompanyMapping>();
const { enqueueSnackBar } = useSnackBar();
const { createManyRecords: createManyCompanies } =
useCreateManyRecords<Company>({
objectNameSingular: CoreObjectNameSingular.Company,
});
const openCompanySpreadsheetImport = (
options?: Omit<
SpreadsheetOptions<FieldCompanyMapping>,
'fields' | 'isOpen' | 'onClose'
>,
) => {
openSpreadsheetImport({
...options,
onSubmit: async (data) => {
// TODO: Add better type checking in spreadsheet import later
const createInputs = data.validData.map(
(company) =>
({
name: company.name as string | undefined,
domainName: company.domainName as string | undefined,
...(company.linkedinUrl
? {
linkedinLink: {
label: 'linkedinUrl',
url: company.linkedinUrl as string | undefined,
},
}
: {}),
...(company.annualRecurringRevenue
? {
annualRecurringRevenue: {
amountMicros: Number(company.annualRecurringRevenue),
currencyCode: 'USD',
},
}
: {}),
idealCustomerProfile:
company.idealCustomerProfile &&
['true', true].includes(company.idealCustomerProfile),
...(company.xUrl
? {
xLink: {
label: 'xUrl',
url: company.xUrl as string | undefined,
},
}
: {}),
address: company.address as string | undefined,
employees: company.employees
? Number(company.employees)
: undefined,
}) as Company,
);
// TODO: abstract this part for any object
try {
await createManyCompanies(createInputs);
} catch (error: any) {
enqueueSnackBar(error?.message || 'Something went wrong', {
variant: 'error',
});
}
},
fields: fieldsForCompany,
});
};
return { openCompanySpreadsheetImport };
};

View File

@ -1,124 +0,0 @@
import {
IconBrandLinkedin,
IconBrandX,
IconBuildingSkyscraper,
IconMail,
IconMap,
IconMoneybag,
IconTarget,
IconUsers,
} from '@/ui/display/icon';
export const fieldsForCompany = [
{
icon: IconBuildingSkyscraper,
label: 'Name',
key: 'name',
alternateMatches: ['name', 'company name', 'company'],
fieldType: {
type: 'input',
},
example: 'Tim',
},
{
icon: IconMail,
label: 'Domain name',
key: 'domainName',
alternateMatches: ['domain', 'domain name'],
fieldType: {
type: 'input',
},
example: 'apple.dev',
},
{
icon: IconBrandLinkedin,
label: 'Linkedin URL',
key: 'linkedinUrl',
alternateMatches: ['linkedIn', 'linkedin', 'linkedin url'],
fieldType: {
type: 'input',
},
example: 'https://www.linkedin.com/in/apple',
},
{
icon: IconMoneybag,
label: 'ARR',
key: 'annualRecurringRevenue',
alternateMatches: [
'arr',
'annual revenue',
'revenue',
'recurring revenue',
'annual recurring revenue',
],
fieldType: {
type: 'input',
},
validation: [
{
regex: /^(\d+)?$/,
errorMessage: 'Annual recurring revenue must be a number',
level: 'error',
},
],
example: '1000000',
},
{
icon: IconTarget,
label: 'ICP',
key: 'idealCustomerProfile',
alternateMatches: [
'icp',
'ideal profile',
'ideal customer profile',
'ideal customer',
],
fieldType: {
type: 'input',
},
validation: [
{
regex: /^(true|false)?$/,
errorMessage: 'Ideal custoner profile must be a boolean',
level: 'error',
},
],
example: 'true/false',
},
{
icon: IconBrandX,
label: 'x URL',
key: 'xUrl',
alternateMatches: ['x', 'twitter', 'twitter url', 'x url'],
fieldType: {
type: 'input',
},
example: 'https://x.com/tim_cook',
},
{
icon: IconMap,
label: 'Address',
key: 'address',
fieldType: {
type: 'input',
},
example: 'Maple street',
},
{
icon: IconUsers,
label: 'Employees',
key: 'employees',
alternateMatches: ['employees', 'total employees', 'number of employees'],
fieldType: {
type: 'input',
},
validation: [
{
regex: /^\d+$/,
errorMessage: 'Employees must be a number',
level: 'error',
},
],
example: '150',
},
] as const;

View File

@ -5,8 +5,8 @@ import { Key } from 'ts-key-enum';
import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId';
import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard';
import { useRecordIndexOptionsForTable } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForTable';
import { useRecordIndexOptionsImport } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsImport';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { useSpreadsheetRecordImport } from '@/object-record/spreadsheet-import/useSpreadsheetRecordImport';
import {
IconBaselineDensitySmall,
IconChevronLeft,
@ -116,7 +116,8 @@ export const RecordIndexOptionsDropdownContent = ({
? handleBoardFieldVisibilityChange
: handleColumnVisibilityChange;
const { handleImport } = useRecordIndexOptionsImport({ objectNameSingular });
const { openRecordSpreadsheetImport } =
useSpreadsheetRecordImport(objectNameSingular);
return (
<>
@ -141,13 +142,11 @@ export const RecordIndexOptionsDropdownContent = ({
LeftIcon={IconTag}
text="Fields"
/>
{handleImport && (
<MenuItem
onClick={() => handleImport()}
LeftIcon={IconFileImport}
text="Import"
/>
)}
<MenuItem
onClick={() => openRecordSpreadsheetImport()}
LeftIcon={IconFileImport}
text="Import"
/>
</DropdownMenuItemsContainer>
</>
)}

View File

@ -1,23 +0,0 @@
import { useSpreadsheetCompanyImport } from '@/companies/hooks/useSpreadsheetCompanyImport';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useSpreadsheetPersonImport } from '@/people/hooks/useSpreadsheetPersonImport';
type useRecordIndexOptionsImportParams = {
objectNameSingular: string;
};
export const useRecordIndexOptionsImport = ({
objectNameSingular,
}: useRecordIndexOptionsImportParams) => {
const { openPersonSpreadsheetImport } = useSpreadsheetPersonImport();
const { openCompanySpreadsheetImport } = useSpreadsheetCompanyImport();
const handleImport =
CoreObjectNameSingular.Company === objectNameSingular
? openCompanySpreadsheetImport
: CoreObjectNameSingular.Person === objectNameSingular
? openPersonSpreadsheetImport
: undefined;
return { handleImport };
};

View File

@ -4,10 +4,11 @@ import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook, waitFor } from '@testing-library/react';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { spreadsheetImportState } from '@/spreadsheet-import/states/spreadsheetImportState';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { useSpreadsheetCompanyImport } from '../useSpreadsheetCompanyImport';
import { useSpreadsheetRecordImport } from '../useSpreadsheetRecordImport';
const companyId = 'cb2e9f4b-20c3-4759-9315-4ffeecfaf71a';
@ -32,11 +33,11 @@ const companyMocks = [
variables: {
data: [
{
name: 'Example Company',
address: 'test',
domainName: 'example.com',
employees: 0,
idealCustomerProfile: true,
address: undefined,
employees: undefined,
name: 'Example Company',
id: companyId,
},
],
@ -75,21 +76,23 @@ describe('useSpreadsheetCompanyImport', () => {
const { result } = renderHook(
() => {
const spreadsheetImport = useRecoilValue(spreadsheetImportState);
const { openCompanySpreadsheetImport } = useSpreadsheetCompanyImport();
return { openCompanySpreadsheetImport, spreadsheetImport };
const { openRecordSpreadsheetImport } = useSpreadsheetRecordImport(
CoreObjectNameSingular.Company,
);
return { openRecordSpreadsheetImport, spreadsheetImport };
},
{
wrapper: Wrapper,
},
);
const { spreadsheetImport, openCompanySpreadsheetImport } = result.current;
const { spreadsheetImport, openRecordSpreadsheetImport } = result.current;
expect(spreadsheetImport.isOpen).toBe(false);
expect(spreadsheetImport.options).toBeNull();
await act(async () => {
openCompanySpreadsheetImport();
openRecordSpreadsheetImport();
});
const { spreadsheetImport: updatedImport } = result.current;
@ -109,8 +112,8 @@ describe('useSpreadsheetCompanyImport', () => {
name: 'Example Company',
domainName: 'example.com',
idealCustomerProfile: true,
address: undefined,
employees: undefined,
address: 'test',
employees: '0',
},
],
invalidData: [],
@ -121,8 +124,8 @@ describe('useSpreadsheetCompanyImport', () => {
domainName: 'example.com',
__index: 'cbc3985f-dde9-46d1-bae2-c124141700ac',
idealCustomerProfile: true,
address: undefined,
employees: undefined,
address: 'test',
employees: '0',
},
],
},

View File

@ -0,0 +1,166 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
import { getSpreadSheetValidation } from '@/object-record/spreadsheet-import/util/getSpreadSheetValidation';
import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport';
import { SpreadsheetOptions, Validation } from '@/spreadsheet-import/types';
import { useIcons } from '@/ui/display/icon/hooks/useIcons';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { FieldMetadataType } from '~/generated-metadata/graphql';
const firstName = 'Firstname';
const lastName = 'Lastname';
export const useSpreadsheetRecordImport = (objectNameSingular: string) => {
const { openSpreadsheetImport } = useSpreadsheetImport<any>();
const { enqueueSnackBar } = useSnackBar();
const { getIcon } = useIcons();
const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
const fields = objectMetadataItem.fields
.filter(
(x) =>
x.isActive &&
!x.isSystem &&
x.name !== 'createdAt' &&
(x.type !== FieldMetadataType.Relation || x.toRelationMetadata),
)
.sort((a, b) => a.name.localeCompare(b.name));
const templateFields: {
icon: IconComponent;
label: string;
key: string;
fieldType: {
type: 'input' | 'checkbox';
};
validations?: Validation[];
}[] = [];
for (const field of fields) {
if (field.type === FieldMetadataType.FullName) {
templateFields.push({
icon: getIcon(field.icon),
label: `${firstName} (${field.label})`,
key: `${firstName} (${field.name})`,
fieldType: {
type: 'input',
},
validations: getSpreadSheetValidation(
field.type,
`${firstName} (${field.label})`,
),
});
templateFields.push({
icon: getIcon(field.icon),
label: `${lastName} (${field.label})`,
key: `${lastName} (${field.name})`,
fieldType: {
type: 'input',
},
validations: getSpreadSheetValidation(
field.type,
`${lastName} (${field.label})`,
),
});
} else if (field.type === FieldMetadataType.Relation) {
templateFields.push({
icon: getIcon(field.icon),
label: field.label + ' (ID)',
key: field.name,
fieldType: {
type: 'input',
},
validations: getSpreadSheetValidation(
field.type,
field.label + ' (ID)',
),
});
} else {
templateFields.push({
icon: getIcon(field.icon),
label: field.label,
key: field.name,
fieldType: {
type: 'input',
},
validations: getSpreadSheetValidation(field.type, field.label),
});
}
}
const { createManyRecords } = useCreateManyRecords({
objectNameSingular,
});
const openRecordSpreadsheetImport = (
options?: Omit<SpreadsheetOptions<any>, 'fields' | 'isOpen' | 'onClose'>,
) => {
openSpreadsheetImport({
...options,
onSubmit: async (data) => {
const createInputs = data.validData.map((record) => {
const fieldMapping: Record<string, any> = {};
for (const field of fields) {
const value = record[field.name];
switch (field.type) {
case FieldMetadataType.Boolean:
fieldMapping[field.name] = value === 'true' || value === true;
break;
case FieldMetadataType.Number:
case FieldMetadataType.Numeric:
fieldMapping[field.name] = Number(value);
break;
case FieldMetadataType.Currency:
if (value !== undefined) {
fieldMapping[field.name] = {
amountMicros: Number(value),
currencyCode: 'USD',
};
}
break;
case FieldMetadataType.Link:
if (value !== undefined) {
fieldMapping[field.name] = {
label: field.name,
url: value || null,
};
}
break;
case FieldMetadataType.Relation:
if (value) {
fieldMapping[field.name + 'Id'] = value;
}
break;
case FieldMetadataType.FullName:
if (
record[`${firstName} (${field.name})`] ||
record[`${lastName} (${field.name})`]
) {
fieldMapping[field.name] = {
firstName: record[`${firstName} (${field.name})`] || '',
lastName: record[`${lastName} (${field.name})`] || '',
};
}
break;
default:
fieldMapping[field.name] = value;
break;
}
}
return fieldMapping;
});
try {
await createManyRecords(createInputs);
} catch (error: any) {
enqueueSnackBar(error?.message || 'Something went wrong', {
variant: 'error',
});
}
},
fields: templateFields,
});
};
return { openRecordSpreadsheetImport };
};

View File

@ -0,0 +1,42 @@
import { isValidPhoneNumber } from 'libphonenumber-js';
import { isValidUuid } from '@/object-record/spreadsheet-import/util/isValidUuid';
import { Validation } from '@/spreadsheet-import/types';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const getSpreadSheetValidation = (
type: FieldMetadataType,
fieldName: string,
): Validation[] => {
switch (type) {
case FieldMetadataType.Number:
return [
{
rule: 'regex',
value: '^d+$',
errorMessage: fieldName + ' must be a number',
level: 'error',
},
];
case FieldMetadataType.Phone:
return [
{
rule: 'function',
isValid: (value: string) => isValidPhoneNumber(value),
errorMessage: fieldName + ' is not valid',
level: 'error',
},
];
case FieldMetadataType.Relation:
return [
{
rule: 'function',
isValid: (value: string) => isValidUuid(value),
errorMessage: fieldName + ' is not valid',
level: 'error',
},
];
default:
return [];
}
};

View File

@ -0,0 +1,5 @@
export const isValidUuid = (value: string) => {
return /^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(
value,
);
};

View File

@ -1,124 +0,0 @@
import { gql } from '@apollo/client';
export const query = gql`
mutation CreatePeople($data: [PersonCreateInput!]!) {
createPeople(data: $data) {
id
opportunities {
edges {
node {
id
}
}
}
xLink {
label
url
}
id
pointOfContactForOpportunities {
edges {
node {
id
}
}
}
createdAt
company {
id
}
city
email
activityTargets {
edges {
node {
id
}
}
}
jobTitle
favorites {
edges {
node {
id
}
}
}
attachments {
edges {
node {
id
}
}
}
name {
firstName
lastName
}
phone
linkedinLink {
label
url
}
updatedAt
avatarUrl
companyId
}
}
`;
export const personId = 'cb2e9f4b-20c3-4759-9315-4ffeecfaf71a';
export const variables = {
data: [
{
id: personId,
name: { firstName: 'Sheldon', lastName: ' Cooper' },
email: undefined,
jobTitle: undefined,
phone: undefined,
city: undefined,
},
],
};
export const responseData = [
{
opportunities: {
edges: [],
},
xLink: {
label: '',
url: '',
},
pointOfContactForOpportunities: {
edges: [],
},
createdAt: '',
company: {
id: '',
},
city: '',
email: '',
activityTargets: {
edges: [],
},
jobTitle: '',
favorites: {
edges: [],
},
attachments: {
edges: [],
},
name: variables.data[0].name,
phone: '',
linkedinLink: {
label: '',
url: '',
},
updatedAt: '',
avatarUrl: '',
companyId: '',
id: personId,
},
];

View File

@ -1,107 +0,0 @@
import { ReactNode } from 'react';
import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook, waitFor } from '@testing-library/react';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { spreadsheetImportState } from '@/spreadsheet-import/states/spreadsheetImportState';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import {
personId,
query,
responseData,
variables,
} from '../__mocks__/useSpreadsheetPersonImport';
import { useSpreadsheetPersonImport } from '../useSpreadsheetPersonImport';
jest.mock('uuid', () => ({
v4: jest.fn(() => personId),
}));
const mocks = [
{
request: {
query,
variables,
},
result: jest.fn(() => ({
data: {
createPeople: responseData,
},
})),
},
];
const Wrapper = ({ children }: { children: ReactNode }) => (
<RecoilRoot>
<MockedProvider mocks={mocks} addTypename={false}>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
{children}
</SnackBarProviderScope>
</MockedProvider>
</RecoilRoot>
);
const fakeCsv = () => {
const csvContent = 'firstname, lastname\nSheldon, Cooper';
const blob = new Blob([csvContent], { type: 'text/csv' });
return new File([blob], 'fakeData.csv', { type: 'text/csv' });
};
describe('useSpreadsheetPersonImport', () => {
it('should work as expected', async () => {
const { result } = renderHook(
() => {
const spreadsheetImport = useRecoilValue(spreadsheetImportState);
const { openPersonSpreadsheetImport } = useSpreadsheetPersonImport();
return { openPersonSpreadsheetImport, spreadsheetImport };
},
{
wrapper: Wrapper,
},
);
const { spreadsheetImport, openPersonSpreadsheetImport } = result.current;
expect(spreadsheetImport.isOpen).toBe(false);
expect(spreadsheetImport.options).toBeNull();
await act(async () => {
openPersonSpreadsheetImport();
});
const { spreadsheetImport: updatedImport } = result.current;
expect(updatedImport.isOpen).toBe(true);
expect(updatedImport.options).toHaveProperty('onSubmit');
expect(updatedImport.options?.onSubmit).toBeInstanceOf(Function);
expect(updatedImport.options).toHaveProperty('fields');
expect(Array.isArray(updatedImport.options?.fields)).toBe(true);
act(() => {
updatedImport.options?.onSubmit(
{
validData: [
{
firstName: 'Sheldon',
lastName: ' Cooper',
},
],
invalidData: [],
all: [
{
firstName: 'Sheldon',
lastName: ' Cooper',
__index: 'cbc3985f-dde9-46d1-bae2-c124141700ac',
},
],
},
fakeCsv(),
);
});
await waitFor(() => {
expect(mocks[0].result).toHaveBeenCalled();
});
});
});

View File

@ -1,77 +0,0 @@
import { v4 } from 'uuid';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
import { Person } from '@/people/types/Person';
import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport';
import { SpreadsheetOptions } from '@/spreadsheet-import/types';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { fieldsForPerson } from '../utils/fieldsForPerson';
export type FieldPersonMapping = (typeof fieldsForPerson)[number]['key'];
export const useSpreadsheetPersonImport = () => {
const { openSpreadsheetImport } = useSpreadsheetImport<FieldPersonMapping>();
const { enqueueSnackBar } = useSnackBar();
const { createManyRecords: createManyPeople } = useCreateManyRecords<Person>({
objectNameSingular: CoreObjectNameSingular.Person,
});
const openPersonSpreadsheetImport = (
options?: Omit<
SpreadsheetOptions<FieldPersonMapping>,
'fields' | 'isOpen' | 'onClose'
>,
) => {
openSpreadsheetImport({
...options,
onSubmit: async (data) => {
// TODO: Add better type checking in spreadsheet import later
const createInputs = data.validData.map(
(person) =>
({
id: v4(),
name: {
firstName: person.firstName as string | undefined,
lastName: person.lastName as string | undefined,
},
email: person.email as string | undefined,
...(person.linkedinUrl
? {
linkedinLink: {
label: 'linkedinUrl',
url: person.linkedinUrl as string | undefined,
},
}
: {}),
...(person.xUrl
? {
xLink: {
label: 'xUrl',
url: person.xUrl as string | undefined,
},
}
: {}),
jobTitle: person.jobTitle as string | undefined,
phone: person.phone as string | undefined,
city: person.city as string | undefined,
}) as Person,
);
// TODO: abstract this part for any object
try {
await createManyPeople(createInputs);
} catch (error: any) {
enqueueSnackBar(error?.message || 'Something went wrong', {
variant: 'error',
});
}
},
fields: fieldsForPerson,
});
};
return { openPersonSpreadsheetImport };
};

View File

@ -1,100 +0,0 @@
import { isValidPhoneNumber } from 'libphonenumber-js';
import { Fields } from '@/spreadsheet-import/types';
import {
IconBrandLinkedin,
IconBrandX,
IconBriefcase,
IconMail,
IconMap,
IconUser,
} from '@/ui/display/icon';
export const fieldsForPerson = [
{
icon: IconUser,
label: 'Firstname',
key: 'firstName',
alternateMatches: ['first name', 'first', 'firstname'],
fieldType: {
type: 'input',
},
example: 'Tim',
},
{
icon: IconUser,
label: 'Lastname',
key: 'lastName',
alternateMatches: ['last name', 'last', 'lastname'],
fieldType: {
type: 'input',
},
example: 'Cook',
},
{
icon: IconMail,
label: 'Email',
key: 'email',
alternateMatches: ['email', 'mail'],
fieldType: {
type: 'input',
},
example: 'tim@apple.dev',
},
{
icon: IconBrandLinkedin,
label: 'Linkedin URL',
key: 'linkedinUrl',
alternateMatches: ['linkedIn', 'linkedin', 'linkedin url'],
fieldType: {
type: 'input',
},
example: 'https://www.linkedin.com/in/timcook',
},
{
icon: IconBrandX,
label: 'X URL',
key: 'xUrl',
alternateMatches: ['x', 'x url'],
fieldType: {
type: 'input',
},
example: 'https://x.com/tim_cook',
},
{
icon: IconBriefcase,
label: 'Job title',
key: 'jobTitle',
alternateMatches: ['job', 'job title'],
fieldType: {
type: 'input',
},
example: 'CEO',
},
{
icon: IconBriefcase,
label: 'Phone',
key: 'phone',
fieldType: {
type: 'input',
},
example: '+1234567890',
validations: [
{
rule: 'function',
isValid: (value: string) => isValidPhoneNumber(value),
errorMessage: 'phone is not valid',
level: 'error',
},
],
},
{
icon: IconMap,
label: 'City',
key: 'city',
fieldType: {
type: 'input',
},
example: 'Seattle',
},
] as Fields<string>;

View File

@ -129,9 +129,8 @@ export const MatchColumnSelect = ({
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
{options?.map((option) => (
<>
<React.Fragment key={option.label}>
<MenuItemSelect
key={option.label}
selected={value?.label === option.label}
onClick={() => handleChange(option)}
disabled={
@ -152,9 +151,11 @@ export const MatchColumnSelect = ({
/>,
document.body,
)}
</>
</React.Fragment>
))}
{options?.length === 0 && <MenuItem text="No result" />}
{options?.length === 0 && (
<MenuItem key="No result" text="No result" />
)}
</DropdownMenuItemsContainer>
</DropdownMenu>
</StyledFloatingDropdown>,