Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spreadsheet import front module #2862

Merged
merged 7 commits into from
Dec 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion front/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,4 @@
"msw": {
"workerDirectory": "public"
}
}
}
4 changes: 1 addition & 3 deletions front/src/modules/command-menu/components/CommandMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand Down
77 changes: 77 additions & 0 deletions front/src/modules/companies/hooks/useSpreadsheetCompanyImport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Company } from '@/companies/types/Company';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is acceptable at this stage to implement a company and a person version but we will want to re-implement it for any standard or custom object in a more abstract way

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense

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 };
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -93,6 +94,10 @@ export const useObjectMetadataItem = (
objectMetadataItem,
});

const createManyRecordsMutation = useGenerateCreateManyRecordMutation({
objectMetadataItem,
});

const updateOneRecordMutation = useGenerateUpdateOneRecordMutation({
objectMetadataItem,
});
Expand All @@ -118,6 +123,7 @@ export const useObjectMetadataItem = (
createOneRecordMutation,
updateOneRecordMutation,
deleteOneRecordMutation,
createManyRecordsMutation,
mapToObjectRecordIdentifier,
getObjectOrderByField,
};
Expand Down
59 changes: 41 additions & 18 deletions front/src/modules/object-record/components/RecordTableContainer.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -44,6 +47,9 @@ export const RecordTableContainer = ({
objectNameSingular,
});

const { openPersonSpreadsheetImport } = useSpreadsheetPersonImport();
const { openCompanySpreadsheetImport } = useSpreadsheetCompanyImport();

const recordTableId = objectNamePlural ?? '';
const viewBarId = objectNamePlural ?? '';

Expand All @@ -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
Expand Down
73 changes: 73 additions & 0 deletions front/src/modules/object-record/hooks/useCreateManyRecords.ts
Original file line number Diff line number Diff line change
@@ -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>[]) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very nice!

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 };
};
Original file line number Diff line number Diff line change
@@ -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')}
}
}`;
};
Loading
Loading