fix: import resource imporements

This commit is contained in:
Ahmed Bouhuolia
2024-03-27 04:01:01 +02:00
parent 973d1832bd
commit ad4e51d81d
59 changed files with 1508 additions and 211 deletions

View File

@@ -0,0 +1,25 @@
// @ts-nocheck
import { DashboardInsider } from '@/components';
import { ImportView } from '../Import/ImportView';
import { useHistory } from 'react-router-dom';
export default function AccountsImport() {
const history = useHistory();
const handleCancelBtnClick = () => {
history.push('/accounts');
};
const handleImportSuccess = () => {
history.push('/accounts');
};
return (
<DashboardInsider name={'import-accounts'}>
<ImportView
resource={'accounts'}
onCancelClick={handleCancelBtnClick}
onImportSuccess={handleImportSuccess}
/>
</DashboardInsider>
);
}

View File

@@ -7,6 +7,7 @@ import {
NavbarDivider,
Alignment,
} from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import {
Icon,
DashboardActionsBar,
@@ -48,6 +49,8 @@ function AccountTransactionsActionsBar({
const addMoneyInOptions = useMemo(() => getAddMoneyInOptions(), []);
const addMoneyOutOptions = useMemo(() => getAddMoneyOutOptions(), []);
const history = useHistory();
// Handle money in form
const handleMoneyInFormTransaction = (account) => {
openDialog('money-in', {
@@ -64,6 +67,11 @@ function AccountTransactionsActionsBar({
account_name: account.name,
});
};
// Handle import button click.
const handleImportBtnClick = () => {
history.push(`/cashflow-accounts/${accountId}/import`);
};
// Refresh cashflow infinity transactions hook.
const { refresh } = useRefreshCashflowTransactionsInfinity();
@@ -106,6 +114,7 @@ function AccountTransactionsActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />}
text={<T id={'import'} />}
onClick={handleImportBtnClick}
/>
<NavbarDivider />
<DashboardRowsHeightButton

View File

@@ -0,0 +1,32 @@
// @ts-nocheck
import { DashboardInsider } from '@/components';
import { ImportView } from '@/containers/Import/ImportView';
import { useHistory, useParams } from 'react-router-dom';
export default function ImportUncategorizedTransactions() {
const history = useHistory();
const params = useParams();
const handleImportSuccess = () => {
history.push(
`/cashflow-accounts/${params.id}/transactions?filter=uncategorized`,
);
};
const handleCnacelBtnClick = () => {
history.push(
`/cashflow-accounts/${params.id}/transactions?filter=uncategorized`,
);
};
return (
<DashboardInsider name={'import-uncategorized-bank-transactions'}>
<ImportView
resource={'uncategorized_cashflow_transaction'}
params={{ accountId: params.id }}
onImportSuccess={handleImportSuccess}
onCancelClick={handleCnacelBtnClick}
sampleFileName={'sample_bank_transactions'}
/>
</DashboardInsider>
);
}

View File

@@ -0,0 +1,25 @@
// @ts-nocheck
import { DashboardInsider } from '@/components';
import { ImportView } from '../Import/ImportView';
import { useHistory } from 'react-router-dom';
export default function CustomersImport() {
const history = useHistory();
const handleImportSuccess = () => {
history.push('/customers');
};
const handleCancelBtnClick = () => {
history.push('/customers');
};
return (
<DashboardInsider name={'import-customers'}>
<ImportView
resource={'customers'}
onImportSuccess={handleImportSuccess}
onCancelClick={handleCancelBtnClick}
exampleTitle="Customers Example"
/>
</DashboardInsider>
);
}

View File

@@ -95,6 +95,11 @@ function CustomerActionsBar({
addSetting('customers', 'tableSize', size);
};
// Handle import button click.
const handleImportBtnClick = () => {
history.push('/customers/import');
};
return (
<DashboardActionsBar>
<NavbarGroup>
@@ -142,6 +147,7 @@ function CustomerActionsBar({
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />}
onClick={handleImportBtnClick}
text={<T id={'import'} />}
/>
<Button

View File

@@ -9,9 +9,11 @@ import { useImportFileContext } from './ImportFileProvider';
export function ImportFileUploadFooterActions() {
const { isSubmitting } = useFormikContext();
const { setStep } = useImportFileContext();
const { onCancelClick } = useImportFileContext();
const handleCancelBtnClick = () => {};
const handleCancelBtnClick = () => {
onCancelClick && onCancelClick();
};
return (
<div className={clsx(CLASSES.PAGE_FORM_FLOATING_ACTIONS, styles.root)}>

View File

@@ -7,33 +7,38 @@ import { ImportFileMappingForm } from './ImportFileMappingForm';
import { EntityColumn, useImportFileContext } from './ImportFileProvider';
import { CLASSES } from '@/constants';
import { ImportFileContainer } from './ImportFileContainer';
import styles from './ImportFileMapping.module.scss';
import { ImportStepperStep } from './_types';
import { ImportFileMapBootProvider } from './ImportFileMappingBoot';
import styles from './ImportFileMapping.module.scss';
export function ImportFileMapping() {
const { importId } = useImportFileContext();
return (
<ImportFileMappingForm>
<ImportFileContainer>
<p>
Review and map the column headers in your csv/xlsx file with the
Bigcapital fields.
</p>
<ImportFileMapBootProvider importId={importId}>
<ImportFileMappingForm>
<ImportFileContainer>
<p>
Review and map the column headers in your csv/xlsx file with the
Bigcapital fields.
</p>
<table className={clsx('bp4-html-table', styles.table)}>
<thead>
<tr>
<th className={styles.label}>Bigcapital Fields</th>
<th className={styles.field}>Sheet Column Headers</th>
</tr>
</thead>
<tbody>
<ImportFileMappingFields />
</tbody>
</table>
</ImportFileContainer>
<table className={clsx('bp4-html-table', styles.table)}>
<thead>
<tr>
<th className={styles.label}>Bigcapital Fields</th>
<th className={styles.field}>Sheet Column Headers</th>
</tr>
</thead>
<tbody>
<ImportFileMappingFields />
</tbody>
</table>
</ImportFileContainer>
<ImportFileMappingFloatingActions />
</ImportFileMappingForm>
<ImportFileMappingFloatingActions />
</ImportFileMappingForm>
</ImportFileMapBootProvider>
);
}
@@ -67,6 +72,7 @@ function ImportFileMappingFields() {
</tr>
);
const columns = entityColumns.map(columnMapper);
return <>{columns}</>;
}
@@ -81,7 +87,7 @@ function ImportFileMappingFloatingActions() {
return (
<div className={clsx(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<Group spacing={10}>
<Button onClick={handleCancelBtnClick}>Cancel</Button>
<Button onClick={handleCancelBtnClick}>Back</Button>
<Button type="submit" intent={Intent.PRIMARY} loading={isSubmitting}>
Next
</Button>

View File

@@ -0,0 +1,58 @@
import { Spinner } from '@blueprintjs/core';
import React, { createContext, useContext } from 'react';
import { Box } from '@/components';
import { useImportFileMeta } from '@/hooks/query/import';
interface ImportFileMapBootContextValue {}
const ImportFileMapBootContext = createContext<ImportFileMapBootContextValue>(
{} as ImportFileMapBootContextValue,
);
export const useImportFileMapBootContext = () => {
const context = useContext<ImportFileMapBootContextValue>(
ImportFileMapBootContext,
);
if (!context) {
throw new Error(
'useImportFileMapBootContext must be used within an ImportFileMapBootProvider',
);
}
return context;
};
interface ImportFileMapBootProps {
importId: string;
children: React.ReactNode;
}
export const ImportFileMapBootProvider = ({
importId,
children,
}: ImportFileMapBootProps) => {
const {
data: importFile,
isLoading: isImportFileLoading,
isFetching: isImportFileFetching,
} = useImportFileMeta(importId, {
enabled: Boolean(importId),
});
const value = {
importFile,
isImportFileLoading,
isImportFileFetching,
};
return (
<ImportFileMapBootContext.Provider value={value}>
{isImportFileLoading ? (
<Box style={{ padding: '2rem', textAlign: 'center' }}>
<Spinner size={26} />
</Box>
) : (
<>{children}</>
)}
</ImportFileMapBootContext.Provider>
);
};

View File

@@ -6,6 +6,8 @@ import { useImportFileContext } from './ImportFileProvider';
import { useMemo } from 'react';
import { isEmpty, lowerCase } from 'lodash';
import { AppToaster } from '@/components';
import { useImportFileMapBootContext } from './ImportFileMappingBoot';
import { transformToForm } from '@/utils';
interface ImportFileMappingFormProps {
children: React.ReactNode;
@@ -58,10 +60,23 @@ const transformValueToReq = (value: ImportFileMappingFormValues) => {
return { mapping };
};
const transformResToFormValues = (value: { from: string; to: string }[]) => {
return value?.reduce((acc, map) => {
acc[map.to] = map.from;
return acc;
}, {});
};
const useImportFileMappingInitialValues = () => {
const { importFile } = useImportFileMapBootContext();
const { entityColumns, sheetColumns } = useImportFileContext();
return useMemo(
const initialResValues = useMemo(
() => transformResToFormValues(importFile?.map || []),
[importFile?.map],
);
const initialValues = useMemo(
() =>
entityColumns.reduce((acc, { key, name }) => {
const _name = lowerCase(name);
@@ -75,4 +90,12 @@ const useImportFileMappingInitialValues = () => {
}, {}),
[entityColumns, sheetColumns],
);
return useMemo(
() => ({
...transformToForm(initialResValues, initialValues),
...initialValues,
}),
[initialValues, initialResValues],
);
};

View File

@@ -1,7 +1,6 @@
// @ts-nocheck
import { Button, Callout, Intent, Text } from '@blueprintjs/core';
import clsx from 'classnames';
import { useHistory } from 'react-router-dom';
import {
ImportFilePreviewBootProvider,
useImportFilePreviewBootContext,
@@ -144,12 +143,12 @@ function ImportFilePreviewUnmapped() {
}
function ImportFilePreviewFloatingActions() {
const { importId, setStep } = useImportFileContext();
const { importId, setStep, onImportSuccess, onImportFailed } =
useImportFileContext();
const { importPreview } = useImportFilePreviewBootContext();
const { mutateAsync: importFile, isLoading: isImportFileLoading } =
useImportFileProcess();
const history = useHistory();
const isValidToImport = importPreview?.createdCount > 0;
const handleSubmitBtn = () => {
@@ -161,9 +160,11 @@ function ImportFilePreviewFloatingActions() {
importPreview.createdCount
} of ${10} has imported successfully.`,
});
history.push('/accounts');
onImportSuccess && onImportSuccess();
})
.catch((error) => {});
.catch((error) => {
onImportFailed && onImportFailed();
});
};
const handleCancelBtnClick = () => {
setStep(ImportStepperStep.Mapping);

View File

@@ -33,12 +33,36 @@ interface ImportFileContextValue {
setImportId: Dispatch<SetStateAction<string>>;
resource: string;
description?: string;
params: Record<string, any>;
onImportSuccess?: () => void;
onImportFailed?: () => void;
onCancelClick?: () => void;
sampleFileName?: string;
exampleDownload?: boolean;
exampleTitle?: string;
exampleDescription?: string;
}
interface ImportFileProviderProps {
resource: string;
description?: string;
params: Record<string, any>;
onImportSuccess?: () => void;
onImportFailed?: () => void;
onCancelClick?: () => void;
children: React.ReactNode;
sampleFileName?: string;
exampleDownload?: boolean;
exampleTitle?: string;
exampleDescription?: string;
}
const ExampleDescription =
'You can download the sample file to obtain detailed information about the data fields used during the import.';
const ExampleTitle = 'Table Example';
const ImportFileContext = createContext<ImportFileContextValue>(
{} as ImportFileContextValue,
);
@@ -57,6 +81,16 @@ export const useImportFileContext = () => {
export const ImportFileProvider = ({
resource,
children,
description,
params,
onImportFailed,
onImportSuccess,
onCancelClick,
sampleFileName,
exampleDownload = true,
exampleTitle = ExampleTitle,
exampleDescription = ExampleDescription,
}: ImportFileProviderProps) => {
const [sheetColumns, setSheetColumns] = useState<SheetColumn[]>([]);
const [entityColumns, setEntityColumns] = useState<SheetColumn[]>([]);
@@ -82,6 +116,18 @@ export const ImportFileProvider = ({
setImportId,
resource,
description,
params,
onImportSuccess,
onImportFailed,
onCancelClick,
sampleFileName,
exampleDownload,
exampleTitle,
exampleDescription,
};
return (

View File

@@ -29,7 +29,7 @@ export function ImportFileUploadForm({
formProps,
}: ImportFileUploadFormProps) {
const { mutateAsync: uploadImportFile } = useImportFileUpload();
const { setStep, setSheetColumns, setEntityColumns, setImportId } =
const { resource, params, setStep, setSheetColumns, setEntityColumns, setImportId } =
useImportFileContext();
const handleSubmit = (
@@ -41,7 +41,8 @@ export function ImportFileUploadForm({
setSubmitting(true);
const formData = new FormData();
formData.append('file', values.file);
formData.append('resource', 'Account');
formData.append('resource', resource);
formData.append('params', JSON.stringify(params));
uploadImportFile(formData)
.then(({ data }) => {

View File

@@ -1,22 +1,31 @@
// @ts-nocheck
import { Classes } from '@blueprintjs/core';
import { Stack } from '@/components';
import { ImportDropzone } from './ImportDropzone';
import { ImportSampleDownload } from './ImportSampleDownload';
import { ImportFileUploadForm } from './ImportFileUploadForm';
import { ImportFileUploadFooterActions } from './ImportFileFooterActions';
import { ImportFileContainer } from './ImportFileContainer';
import { useImportFileContext } from './ImportFileProvider';
export function ImportFileUploadStep() {
const { exampleDownload } = useImportFileContext();
return (
<ImportFileUploadForm>
<ImportFileContainer>
<p style={{ marginBottom: 18 }}>
Download a sample file and compare it to your import file to ensure
you have the file perfect for the import.
<p
className={Classes.TEXT_MUTED}
style={{ marginBottom: 18, lineHeight: 1.6 }}
>
Download a sample file and compare it with your import file to ensure
it is properly formatted. It's not necessary for the columns to be in
the same order, you can map them later.
</p>
<Stack spacing={40}>
<ImportDropzone />
<ImportSampleDownload />
{exampleDownload && <ImportSampleDownload />}
</Stack>
</ImportFileContainer>

View File

@@ -1,18 +0,0 @@
// @ts-nocheck
import { ImportStepper } from './ImportStepper';
import { Box, DashboardInsider } from '@/components';
import { ImportFileProvider } from './ImportFileProvider';
import styles from './ImportPage.module.scss';
export default function ImportPage() {
return (
<DashboardInsider>
<Box className={styles.root}>
<ImportFileProvider resource="account">
<ImportStepper />
</ImportFileProvider>
</Box>
</DashboardInsider>
);
}

View File

@@ -1,23 +1,64 @@
// @ts-nocheck
import { Box, Group } from '@/components';
import { Button } from '@blueprintjs/core';
import { AppToaster, Box, Group } from '@/components';
import {
Button,
Intent,
Menu,
MenuItem,
Popover,
PopoverInteractionKind,
} from '@blueprintjs/core';
import styles from './ImportSampleDownload.module.scss';
import { useSampleSheetImport } from '@/hooks/query/import';
import { useImportFileContext } from './ImportFileProvider';
export function ImportSampleDownload() {
const { resource, sampleFileName, exampleTitle, exampleDescription } =
useImportFileContext();
const { mutateAsync: downloadSample } = useSampleSheetImport();
// Handle download button click.
const handleDownloadBtnClick = (format) => () => {
downloadSample({
filename: sampleFileName || `sample-${resource}`,
resource,
format: format,
})
.then(() => {
AppToaster.show({
intent: Intent.SUCCESS,
message: 'The sample sheet has been downloaded successfully.',
});
})
.catch((error) => {});
};
return (
<Group className={styles.root} noWrap>
<Box>
<h3 className={styles.title}>Table Example</h3>
<p className={styles.description}>
Download a sample file and compare it to your import file to ensure
you have the file perfect for the import.
</p>
<h3 className={styles.title}>{exampleTitle}</h3>
<p className={styles.description}>{exampleDescription}</p>
</Box>
<Box className={styles.buttonWrap}>
<Button minimal outlined>
Download File
</Button>
<Popover
content={
<Menu>
<MenuItem onClick={handleDownloadBtnClick('csv')} text={'CSV'} />
<MenuItem
onClick={handleDownloadBtnClick('xlsx')}
text={'XLSX'}
/>
</Menu>
}
interactionKind={PopoverInteractionKind.CLICK}
placement="bottom-start"
minimal
>
<Button minimal outlined>
Download File
</Button>
</Popover>
</Box>
</Group>
);

View File

@@ -0,0 +1,28 @@
// @ts-nocheck
import { ImportStepper } from './ImportStepper';
import { Box } from '@/components';
import { ImportFileProvider } from './ImportFileProvider';
import styles from './ImportView.module.scss';
interface ImportViewProps {
resource: string;
description?: string;
params: Record<string, any>;
onImportSuccess?: () => void;
onImportFailed?: () => void;
onCancelClick?: () => void;
sampleFileName?: string;
exampleDownload?: boolean;
exampleTitle?: string;
exampleDescription?: string;
}
export function ImportView({ ...props }: ImportViewProps) {
return (
<Box className={styles.root}>
<ImportFileProvider {...props}>
<ImportStepper />
</ImportFileProvider>
</Box>
);
}

View File

@@ -0,0 +1 @@
export * from './ImportView';

View File

@@ -0,0 +1,11 @@
// @ts-nocheck
import { DashboardInsider } from '@/components';
import { ImportView } from '../Import/ImportView';
export default function ItemsImport() {
return (
<DashboardInsider name={'import-items'}>
<ImportView resource={'items'} />
</DashboardInsider>
);
}

View File

@@ -0,0 +1,26 @@
// @ts-nocheck
import { useHistory } from 'react-router-dom';
import { DashboardInsider } from '@/components';
import { ImportView } from '../Import/ImportView';
export default function VendorsImport() {
const history = useHistory();
const handleImportSuccess = () => {
history.push('/vendors');
};
const handleImportBtnClick = () => {
history.push('/vendors');
};
return (
<DashboardInsider name={'import-vendors'}>
<ImportView
resource={'vendors'}
onImportSuccess={handleImportSuccess}
onCancelClick={handleImportBtnClick}
exampleTitle='Vendors Example'
/>
</DashboardInsider>
);
}

View File

@@ -83,6 +83,10 @@ function VendorActionsBar({
const handleTableRowSizeChange = (size) => {
addSetting('vendors', 'tableSize', size);
};
// Handle import button success.
const handleImportBtnSuccess = () => {
history.push('/vendors/import');
};
return (
<DashboardActionsBar>
<NavbarGroup>
@@ -128,6 +132,7 @@ function VendorActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />}
text={<T id={'import'} />}
onClick={handleImportBtnSuccess}
/>
<Button
className={Classes.MINIMAL}