diff --git a/front/package.json b/front/package.json index abba9916b..f471e0be6 100644 --- a/front/package.json +++ b/front/package.json @@ -179,4 +179,4 @@ "msw": { "workerDirectory": "public" } -} \ No newline at end of file +} diff --git a/front/src/modules/command-menu/components/CommandMenu.tsx b/front/src/modules/command-menu/components/CommandMenu.tsx index 9817a259a..558daf35b 100644 --- a/front/src/modules/command-menu/components/CommandMenu.tsx +++ b/front/src/modules/command-menu/components/CommandMenu.tsx @@ -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 && diff --git a/front/src/modules/companies/hooks/useSpreadsheetCompanyImport.ts b/front/src/modules/companies/hooks/useSpreadsheetCompanyImport.ts new file mode 100644 index 000000000..d6aff40df --- /dev/null +++ b/front/src/modules/companies/hooks/useSpreadsheetCompanyImport.ts @@ -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(); + const { enqueueSnackBar } = useSnackBar(); + + const { createManyRecords: createManyCompanies } = + useCreateManyRecords({ + objectNameSingular: 'company', + }); + + const openCompanySpreadsheetImport = ( + options?: Omit< + SpreadsheetOptions, + '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 }; +}; diff --git a/front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts b/front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts index 4dc0aa545..11765d2c6 100644 --- a/front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts +++ b/front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts @@ -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, }; diff --git a/front/src/modules/object-record/components/RecordTableContainer.tsx b/front/src/modules/object-record/components/RecordTableContainer.tsx index 982f10820..6a6aef9c9 100644 --- a/front/src/modules/object-record/components/RecordTableContainer.tsx +++ b/front/src/modules/object-record/components/RecordTableContainer.tsx @@ -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 ( - - } - optionsDropdownScopeId={TableOptionsDropdownId} - onViewFieldsChange={(viewFields) => { - setTableColumns( - mapViewFieldsToColumnDefinitions(viewFields, columnDefinitions), - ); - }} - onViewFiltersChange={(viewFilters) => { - setTableFilters(mapViewFiltersToFilters(viewFilters)); - }} - onViewSortsChange={(viewSorts) => { - setTableSorts(mapViewSortsToSorts(viewSorts)); - }} - /> + + + } + optionsDropdownScopeId={TableOptionsDropdownId} + onViewFieldsChange={(viewFields) => { + setTableColumns( + mapViewFieldsToColumnDefinitions(viewFields, columnDefinitions), + ); + }} + onViewFiltersChange={(viewFilters) => { + setTableFilters(mapViewFiltersToFilters(viewFilters)); + }} + onViewSortsChange={(viewSorts) => { + setTableSorts(mapViewSortsToSorts(viewSorts)); + }} + /> + { ({ + objectNameSingular, +}: ObjectMetadataItemIdentifier) => { + const { triggerOptimisticEffects } = useOptimisticEffect({ + objectNameSingular, + }); + + const { objectMetadataItem, createManyRecordsMutation } = + useObjectMetadataItem({ + objectNameSingular, + }); + + const { generateEmptyRecord } = useGenerateEmptyRecord({ + objectMetadataItem, + }); + + const apolloClient = useApolloClient(); + + const createManyRecords = async (data: Record[]) => { + 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 }; +}; diff --git a/front/src/modules/object-record/hooks/useGenerateCreateManyRecordMutation.ts b/front/src/modules/object-record/hooks/useGenerateCreateManyRecordMutation.ts new file mode 100644 index 000000000..63fa8a790 --- /dev/null +++ b/front/src/modules/object-record/hooks/useGenerateCreateManyRecordMutation.ts @@ -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')} + } + }`; +}; diff --git a/front/src/modules/people/hooks/useSpreadsheetPersonImport.ts b/front/src/modules/people/hooks/useSpreadsheetPersonImport.ts index 6954ea43d..7464bc961 100644 --- a/front/src/modules/people/hooks/useSpreadsheetPersonImport.ts +++ b/front/src/modules/people/hooks/useSpreadsheetPersonImport.ts @@ -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(); + const { enqueueSnackBar } = useSnackBar(); + + const { createManyRecords: createManyPeople } = useCreateManyRecords({ + 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, }); diff --git a/front/src/modules/ui/utilities/hotkey/hooks/useSetHotkeyScope.ts b/front/src/modules/ui/utilities/hotkey/hooks/useSetHotkeyScope.ts index d8dc4d370..beaf9715c 100644 --- a/front/src/modules/ui/utilities/hotkey/hooks/useSetHotkeyScope.ts +++ b/front/src/modules/ui/utilities/hotkey/hooks/useSetHotkeyScope.ts @@ -74,7 +74,6 @@ export const useSetHotkeyScope = () => } scopesToSet.push(newHotkeyScope.scope); - console.log(scopesToSet); set(internalHotkeysEnabledScopesState, scopesToSet); set(currentHotkeyScopeState, newHotkeyScope); },