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
13 changed files with 241 additions and 663 deletions

View File

@ -1,137 +0,0 @@
import { ReactNode } from 'react';
import { gql } from '@apollo/client';
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 { useSpreadsheetCompanyImport } from '../useSpreadsheetCompanyImport';
const companyId = 'cb2e9f4b-20c3-4759-9315-4ffeecfaf71a';
jest.mock('uuid', () => ({
v4: jest.fn(() => companyId),
}));
jest.mock('@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery', () => ({
useMapFieldMetadataToGraphQLQuery: () => () => '\n',
}));
const companyMocks = [
{
request: {
query: gql`
mutation CreateCompanies($data: [CompanyCreateInput!]!) {
createCompanies(data: $data) {
id
}
}
`,
variables: {
data: [
{
name: 'Example Company',
domainName: 'example.com',
idealCustomerProfile: true,
address: undefined,
employees: undefined,
id: companyId,
},
],
},
},
result: jest.fn(() => ({
data: {
createCompanies: [
{
id: companyId,
},
],
},
})),
},
];
const fakeCsv = () => {
const csvContent = 'name\nExample Company';
const blob = new Blob([csvContent], { type: 'text/csv' });
return new File([blob], 'fakeData.csv', { type: 'text/csv' });
};
const Wrapper = ({ children }: { children: ReactNode }) => (
<RecoilRoot>
<MockedProvider mocks={companyMocks} addTypename={false}>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
{children}
</SnackBarProviderScope>
</MockedProvider>
</RecoilRoot>
);
describe('useSpreadsheetCompanyImport', () => {
it('should work as expected', async () => {
const { result } = renderHook(
() => {
const spreadsheetImport = useRecoilValue(spreadsheetImportState);
const { openCompanySpreadsheetImport } = useSpreadsheetCompanyImport();
return { openCompanySpreadsheetImport, spreadsheetImport };
},
{
wrapper: Wrapper,
},
);
const { spreadsheetImport, openCompanySpreadsheetImport } = result.current;
expect(spreadsheetImport.isOpen).toBe(false);
expect(spreadsheetImport.options).toBeNull();
await act(async () => {
openCompanySpreadsheetImport();
});
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: [
{
id: companyId,
name: 'Example Company',
domainName: 'example.com',
idealCustomerProfile: true,
address: undefined,
employees: undefined,
},
],
invalidData: [],
all: [
{
id: companyId,
name: 'Example Company',
domainName: 'example.com',
__index: 'cbc3985f-dde9-46d1-bae2-c124141700ac',
idealCustomerProfile: true,
address: undefined,
employees: undefined,
},
],
},
fakeCsv(),
);
});
await waitFor(() => {
expect(companyMocks[0].result).toHaveBeenCalled();
});
});
});

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;