mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-26 13:31:45 +03:00
Spreadsheet import front module (#2862)
* Spreadsheet import front module Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: Toledodev <rafael.toledo@engenharia.ufjf.br> Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com> * Automatically update table Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: Toledodev <rafael.toledo@engenharia.ufjf.br> Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com> * Add company import Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: Toledodev <rafael.toledo@engenharia.ufjf.br> Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com> * Fixes * Hide import options on custom objects --------- Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com> Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: Toledodev <rafael.toledo@engenharia.ufjf.br> Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com> Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
parent
7c40dc7b81
commit
306344a190
@ -179,4 +179,4 @@
|
||||
"msw": {
|
||||
"workerDirectory": "public"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -190,9 +190,7 @@ export const CommandMenu = () => {
|
||||
selectableListId="command-menu-list"
|
||||
selectableItemIds={[selectableItemIds]}
|
||||
hotkeyScope={AppHotkeyScope.CommandMenu}
|
||||
onEnter={(itemId) => {
|
||||
console.log(itemId);
|
||||
}}
|
||||
onEnter={(_itemId) => {}}
|
||||
>
|
||||
{!matchingCreateCommand.length &&
|
||||
!matchingNavigateCommand.length &&
|
||||
|
@ -0,0 +1,77 @@
|
||||
import { Company } from '@/companies/types/Company';
|
||||
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: '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,
|
||||
}));
|
||||
// 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 };
|
||||
};
|
@ -8,6 +8,7 @@ import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapTo
|
||||
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
||||
import { useGenerateCreateManyRecordMutation } from '@/object-record/hooks/useGenerateCreateManyRecordMutation';
|
||||
import { useGenerateCreateOneRecordMutation } from '@/object-record/hooks/useGenerateCreateOneRecordMutation';
|
||||
import { useGenerateFindManyRecordsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsQuery';
|
||||
import { useGenerateFindOneRecordQuery } from '@/object-record/hooks/useGenerateFindOneRecordQuery';
|
||||
@ -93,6 +94,10 @@ export const useObjectMetadataItem = (
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const createManyRecordsMutation = useGenerateCreateManyRecordMutation({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const updateOneRecordMutation = useGenerateUpdateOneRecordMutation({
|
||||
objectMetadataItem,
|
||||
});
|
||||
@ -118,6 +123,7 @@ export const useObjectMetadataItem = (
|
||||
createOneRecordMutation,
|
||||
updateOneRecordMutation,
|
||||
deleteOneRecordMutation,
|
||||
createManyRecordsMutation,
|
||||
mapToObjectRecordIdentifier,
|
||||
getObjectOrderByField,
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useSpreadsheetCompanyImport } from '@/companies/hooks/useSpreadsheetCompanyImport';
|
||||
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||
@ -8,6 +9,8 @@ import { RecordTable } from '@/object-record/record-table/components/RecordTable
|
||||
import { TableOptionsDropdownId } from '@/object-record/record-table/constants/TableOptionsDropdownId';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { TableOptionsDropdown } from '@/object-record/record-table/options/components/TableOptionsDropdown';
|
||||
import { useSpreadsheetPersonImport } from '@/people/hooks/useSpreadsheetPersonImport';
|
||||
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
|
||||
import { ViewBar } from '@/views/components/ViewBar';
|
||||
import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions';
|
||||
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
||||
@ -44,6 +47,9 @@ export const RecordTableContainer = ({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { openPersonSpreadsheetImport } = useSpreadsheetPersonImport();
|
||||
const { openCompanySpreadsheetImport } = useSpreadsheetCompanyImport();
|
||||
|
||||
const recordTableId = objectNamePlural ?? '';
|
||||
const viewBarId = objectNamePlural ?? '';
|
||||
|
||||
@ -67,26 +73,43 @@ export const RecordTableContainer = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleImport = () => {
|
||||
const openImport =
|
||||
recordTableId === 'companies'
|
||||
? openCompanySpreadsheetImport
|
||||
: openPersonSpreadsheetImport;
|
||||
openImport();
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<ViewBar
|
||||
viewBarId={viewBarId}
|
||||
optionsDropdownButton={
|
||||
<TableOptionsDropdown recordTableId={recordTableId} />
|
||||
}
|
||||
optionsDropdownScopeId={TableOptionsDropdownId}
|
||||
onViewFieldsChange={(viewFields) => {
|
||||
setTableColumns(
|
||||
mapViewFieldsToColumnDefinitions(viewFields, columnDefinitions),
|
||||
);
|
||||
}}
|
||||
onViewFiltersChange={(viewFilters) => {
|
||||
setTableFilters(mapViewFiltersToFilters(viewFilters));
|
||||
}}
|
||||
onViewSortsChange={(viewSorts) => {
|
||||
setTableSorts(mapViewSortsToSorts(viewSorts));
|
||||
}}
|
||||
/>
|
||||
<SpreadsheetImportProvider>
|
||||
<ViewBar
|
||||
viewBarId={viewBarId}
|
||||
optionsDropdownButton={
|
||||
<TableOptionsDropdown
|
||||
recordTableId={recordTableId}
|
||||
onImport={
|
||||
['companies', 'people'].includes(recordTableId)
|
||||
? handleImport
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
}
|
||||
optionsDropdownScopeId={TableOptionsDropdownId}
|
||||
onViewFieldsChange={(viewFields) => {
|
||||
setTableColumns(
|
||||
mapViewFieldsToColumnDefinitions(viewFields, columnDefinitions),
|
||||
);
|
||||
}}
|
||||
onViewFiltersChange={(viewFilters) => {
|
||||
setTableFilters(mapViewFiltersToFilters(viewFilters));
|
||||
}}
|
||||
onViewSortsChange={(viewSorts) => {
|
||||
setTableSorts(mapViewSortsToSorts(viewSorts));
|
||||
}}
|
||||
/>
|
||||
</SpreadsheetImportProvider>
|
||||
<RecordTableEffect recordTableId={recordTableId} viewBarId={viewBarId} />
|
||||
{
|
||||
<RecordTable
|
||||
|
@ -0,0 +1,73 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||
import { useGenerateEmptyRecord } from '@/object-record/hooks/useGenerateEmptyRecord';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useCreateManyRecords = <T>({
|
||||
objectNameSingular,
|
||||
}: ObjectMetadataItemIdentifier) => {
|
||||
const { triggerOptimisticEffects } = useOptimisticEffect({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { objectMetadataItem, createManyRecordsMutation } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { generateEmptyRecord } = useGenerateEmptyRecord({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const createManyRecords = async (data: Record<string, any>[]) => {
|
||||
const withIds = data.map((record) => ({
|
||||
...record,
|
||||
id: (record.id as string) ?? v4(),
|
||||
}));
|
||||
|
||||
withIds.forEach((record) => {
|
||||
triggerOptimisticEffects(
|
||||
`${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
generateEmptyRecord({ id: record.id }),
|
||||
);
|
||||
});
|
||||
|
||||
const createdObjects = await apolloClient.mutate({
|
||||
mutation: createManyRecordsMutation,
|
||||
variables: {
|
||||
data: withIds,
|
||||
},
|
||||
optimisticResponse: {
|
||||
[`create${capitalize(objectMetadataItem.namePlural)}`]: withIds.map(
|
||||
(record) => generateEmptyRecord({ id: record.id }),
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
if (!createdObjects.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const createdRecords =
|
||||
(createdObjects.data[
|
||||
`create${capitalize(objectMetadataItem.namePlural)}`
|
||||
] as T[]) ?? [];
|
||||
|
||||
createdRecords.forEach((record) => {
|
||||
triggerOptimisticEffects(
|
||||
`${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
record,
|
||||
);
|
||||
});
|
||||
|
||||
return createdRecords;
|
||||
};
|
||||
|
||||
return { createManyRecords };
|
||||
};
|
@ -0,0 +1,30 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
|
||||
import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useGenerateCreateManyRecordMutation = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
|
||||
|
||||
if (!objectMetadataItem) {
|
||||
return EMPTY_MUTATION;
|
||||
}
|
||||
|
||||
return gql`
|
||||
mutation Create${capitalize(
|
||||
objectMetadataItem.namePlural,
|
||||
)}($data: [${capitalize(objectMetadataItem.nameSingular)}CreateInput!]!) {
|
||||
create${capitalize(objectMetadataItem.namePlural)}(data: $data) {
|
||||
id
|
||||
${objectMetadataItem.fields
|
||||
.map((field) => mapFieldMetadataToGraphQLQuery(field))
|
||||
.join('\n')}
|
||||
}
|
||||
}`;
|
||||
};
|
@ -1,5 +1,10 @@
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
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';
|
||||
|
||||
@ -7,6 +12,11 @@ export type FieldPersonMapping = (typeof fieldsForPerson)[number]['key'];
|
||||
|
||||
export const useSpreadsheetPersonImport = () => {
|
||||
const { openSpreadsheetImport } = useSpreadsheetImport<FieldPersonMapping>();
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const { createManyRecords: createManyPeople } = useCreateManyRecords<Person>({
|
||||
objectNameSingular: 'person',
|
||||
});
|
||||
|
||||
const openPersonSpreadsheetImport = (
|
||||
options?: Omit<
|
||||
@ -16,35 +26,43 @@ export const useSpreadsheetPersonImport = () => {
|
||||
) => {
|
||||
openSpreadsheetImport({
|
||||
...options,
|
||||
onSubmit: async (_data) => {
|
||||
onSubmit: async (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,
|
||||
// }));
|
||||
// TODO : abstract this part for any object
|
||||
// try {
|
||||
// const result = await createManyPerson({
|
||||
// variables: {
|
||||
// data: createInputs,
|
||||
// },
|
||||
// refetchQueries: 'active',
|
||||
// });
|
||||
// if (result.errors) {
|
||||
// throw result.errors;
|
||||
// }
|
||||
// } catch (error: any) {
|
||||
// enqueueSnackBar(error?.message || 'Something went wrong', {
|
||||
// variant: 'error',
|
||||
// });
|
||||
// }
|
||||
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,
|
||||
}));
|
||||
// TODO: abstract this part for any object
|
||||
try {
|
||||
await createManyPeople(createInputs);
|
||||
} catch (error: any) {
|
||||
enqueueSnackBar(error?.message || 'Something went wrong', {
|
||||
variant: 'error',
|
||||
});
|
||||
}
|
||||
},
|
||||
fields: fieldsForPerson,
|
||||
});
|
||||
|
@ -74,7 +74,6 @@ export const useSetHotkeyScope = () =>
|
||||
}
|
||||
|
||||
scopesToSet.push(newHotkeyScope.scope);
|
||||
console.log(scopesToSet);
|
||||
set(internalHotkeysEnabledScopesState, scopesToSet);
|
||||
set(currentHotkeyScopeState, newHotkeyScope);
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user