Import company and person from csv file (#1236)
* feat: wip implement back-end call csv import * fix: rebase IconBrandTwitter missing * feat: person and company csv import * fix: test & clean * fix: clean & test
This commit is contained in:
@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const INSERT_MANY_PERSON = gql`
|
||||
mutation InsertManyPerson($data: [PersonCreateManyInput!]!) {
|
||||
createManyPerson(data: $data) {
|
||||
count
|
||||
}
|
||||
}
|
||||
`;
|
||||
72
front/src/modules/people/hooks/useSpreadsheetPersonImport.ts
Normal file
72
front/src/modules/people/hooks/useSpreadsheetPersonImport.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport';
|
||||
import { SpreadsheetOptions } from '@/spreadsheet-import/types';
|
||||
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
|
||||
import { useUpsertEntityTableItems } from '@/ui/table/hooks/useUpsertEntityTableItems';
|
||||
import { useUpsertTableRowIds } from '@/ui/table/hooks/useUpsertTableRowIds';
|
||||
import {
|
||||
GetPeopleDocument,
|
||||
useInsertManyPersonMutation,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import { fieldsForPerson } from '../utils/fieldsForPerson';
|
||||
|
||||
export type FieldPersonMapping = (typeof fieldsForPerson)[number]['key'];
|
||||
|
||||
export function useSpreadsheetPersonImport() {
|
||||
const { openSpreadsheetImport } = useSpreadsheetImport<FieldPersonMapping>();
|
||||
const upsertEntityTableItems = useUpsertEntityTableItems();
|
||||
const upsertTableRowIds = useUpsertTableRowIds();
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const [createManyPerson] = useInsertManyPersonMutation();
|
||||
|
||||
const openPersonSpreadsheetImport = (
|
||||
options?: Omit<
|
||||
SpreadsheetOptions<FieldPersonMapping>,
|
||||
'fields' | 'isOpen' | 'onClose'
|
||||
>,
|
||||
) => {
|
||||
openSpreadsheetImport({
|
||||
...options,
|
||||
async onSubmit(data) {
|
||||
// TODO: Add better type checking in spreadsheet import later
|
||||
const createInputs = data.validData.map((person) => ({
|
||||
id: uuidv4(),
|
||||
firstName: person.firstName as string | undefined,
|
||||
lastName: person.lastName as string | undefined,
|
||||
email: person.email as string | undefined,
|
||||
linkedinUrl: person.linkedinUrl as string | undefined,
|
||||
xUrl: person.xUrl as string | undefined,
|
||||
jobTitle: person.jobTitle as string | undefined,
|
||||
phone: person.phone as string | undefined,
|
||||
city: person.city as string | undefined,
|
||||
}));
|
||||
|
||||
try {
|
||||
const result = await createManyPerson({
|
||||
variables: {
|
||||
data: createInputs,
|
||||
},
|
||||
refetchQueries: [GetPeopleDocument],
|
||||
});
|
||||
|
||||
if (result.errors) {
|
||||
throw result.errors;
|
||||
}
|
||||
|
||||
upsertTableRowIds(createInputs.map((person) => person.id));
|
||||
upsertEntityTableItems(createInputs);
|
||||
} catch (error: any) {
|
||||
enqueueSnackBar(error?.message || 'Something went wrong', {
|
||||
variant: 'error',
|
||||
});
|
||||
}
|
||||
},
|
||||
fields: fieldsForPerson,
|
||||
});
|
||||
};
|
||||
|
||||
return { openPersonSpreadsheetImport };
|
||||
}
|
||||
@ -3,6 +3,7 @@ import { useMemo } from 'react';
|
||||
import { peopleViewFields } from '@/people/constants/peopleViewFields';
|
||||
import { usePersonTableContextMenuEntries } from '@/people/hooks/usePeopleTableContextMenuEntries';
|
||||
import { usePersonTableActionBarEntries } from '@/people/hooks/usePersonTableActionBarEntries';
|
||||
import { useSpreadsheetPersonImport } from '@/people/hooks/useSpreadsheetPersonImport';
|
||||
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
|
||||
import { sortsOrderByScopedState } from '@/ui/filter-n-sort/states/sortScopedState';
|
||||
import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause';
|
||||
@ -35,6 +36,7 @@ export function PeopleTable() {
|
||||
);
|
||||
const [updateEntityMutation] = useUpdateOnePersonMutation();
|
||||
const upsertEntityTableItem = useUpsertEntityTableItem();
|
||||
const { openPersonSpreadsheetImport } = useSpreadsheetPersonImport();
|
||||
|
||||
const objectId = 'person';
|
||||
const { handleViewsChange } = useTableViews({ objectId });
|
||||
@ -56,6 +58,10 @@ export function PeopleTable() {
|
||||
const { setContextMenuEntries } = usePersonTableContextMenuEntries();
|
||||
const { setActionBarEntries } = usePersonTableActionBarEntries();
|
||||
|
||||
function handleImport() {
|
||||
openPersonSpreadsheetImport();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<GenericEntityTableData
|
||||
@ -81,6 +87,7 @@ export function PeopleTable() {
|
||||
onColumnsChange={handleColumnsChange}
|
||||
onSortsUpdate={currentViewId ? updateSorts : undefined}
|
||||
onViewsChange={handleViewsChange}
|
||||
onImport={handleImport}
|
||||
updateEntityMutation={({
|
||||
variables,
|
||||
}: {
|
||||
|
||||
120
front/src/modules/people/utils/fieldsForPerson.tsx
Normal file
120
front/src/modules/people/utils/fieldsForPerson.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import { isValidPhoneNumber } from 'libphonenumber-js';
|
||||
|
||||
import {
|
||||
IconBrandLinkedin,
|
||||
IconBrandTwitter,
|
||||
IconBriefcase,
|
||||
IconMail,
|
||||
IconMap,
|
||||
IconUser,
|
||||
} from '@/ui/icon';
|
||||
|
||||
export const fieldsForPerson = [
|
||||
{
|
||||
icon: <IconUser />,
|
||||
label: 'Firstname',
|
||||
key: 'firstName',
|
||||
alternateMatches: ['first name', 'first', 'firstname'],
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
},
|
||||
example: 'Tim',
|
||||
validations: [
|
||||
{
|
||||
rule: 'required',
|
||||
errorMessage: 'Firstname is required',
|
||||
level: 'error',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <IconUser />,
|
||||
label: 'Lastname',
|
||||
key: 'lastName',
|
||||
alternateMatches: ['last name', 'last', 'lastname'],
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
},
|
||||
example: 'Cook',
|
||||
validations: [
|
||||
{
|
||||
rule: 'required',
|
||||
errorMessage: 'Lastname is required',
|
||||
level: 'error',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <IconMail />,
|
||||
label: 'Email',
|
||||
key: 'email',
|
||||
alternateMatches: ['email', 'mail'],
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
},
|
||||
example: 'tim@apple.dev',
|
||||
validations: [
|
||||
{
|
||||
rule: 'required',
|
||||
errorMessage: 'email is required',
|
||||
level: 'error',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <IconBrandLinkedin />,
|
||||
label: 'Linkedin URL',
|
||||
key: 'linkedinUrl',
|
||||
alternateMatches: ['linkedIn', 'linkedin', 'linkedin url'],
|
||||
fieldType: {
|
||||
type: 'input',
|
||||
},
|
||||
example: 'https://www.linkedin.com/in/timcook',
|
||||
},
|
||||
{
|
||||
icon: <IconBrandTwitter />,
|
||||
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 const;
|
||||
Reference in New Issue
Block a user