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:
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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 };
|
||||
};
|
||||
@ -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;
|
||||
Reference in New Issue
Block a user