feat: wip import resource UI

This commit is contained in:
Ahmed Bouhuolia
2024-03-20 04:55:35 +02:00
parent aba06991d4
commit 1d8cec5069
21 changed files with 291 additions and 118 deletions

View File

@@ -1,5 +1,6 @@
// @ts-nocheck
import React from 'react';
import { Ref, useCallback } from 'react';
import clsx from 'classnames';
import {
Accept,
@@ -226,6 +227,7 @@ export const Dropzone = (_props: DropzoneProps) => {
});
const isIdle = !isDragAccept && !isDragReject;
assignRef(openRef, open);
return (
<DropzoneProvider
@@ -264,3 +266,26 @@ Dropzone.displayName = '@mantine/dropzone/Dropzone';
Dropzone.Accept = DropzoneAccept;
Dropzone.Idle = DropzoneIdle;
Dropzone.Reject = DropzoneReject;
type PossibleRef<T> = Ref<T> | undefined;
export function assignRef<T>(ref: PossibleRef<T>, value: T) {
if (typeof ref === 'function') {
ref(value);
} else if (typeof ref === 'object' && ref !== null && 'current' in ref) {
(ref as React.MutableRefObject<T>).current = value;
}
}
export function mergeRefs<T>(...refs: PossibleRef<T>[]) {
return (node: T | null) => {
refs.forEach((ref) => assignRef(ref, node));
};
}
export function useMergedRef<T>(...refs: PossibleRef<T>[]) {
return useCallback(mergeRefs(...refs), refs);
}

View File

@@ -30,7 +30,7 @@ export function Stack(props: StackProps) {
const StackStyled = styled(Box)`
display: flex;
flex-direction: column;
align-items: align;
align-items: ${(props: StackProps) => props.align};
justify-content: justify;
gap: ${(props: StackProps) => props.spacing}px;
`;

View File

@@ -81,8 +81,8 @@ export function Stepper({
active > _children.length - 1 ? completedContent : stepContent;
return (
<Box>
<StepsItems>{items}</StepsItems>
<Box className={classNames?.root}>
<StepsItems className={classNames?.items}>{items}</StepsItems>
<StepsContent className={classNames?.content}>{content} </StepsContent>
</Box>
);

View File

@@ -1,42 +1,20 @@
// @ts-nocheck
import { Button } from '@blueprintjs/core';
import { Field } from 'formik';
import { Box, Group, Icon, Stack } from '@/components';
import { Dropzone } from '@/components/Dropzone';
import { MIME_TYPES } from '@/components/Dropzone/mine-types';
import { Box, Group, Stack } from '@/components';
import styles from './ImportDropzone.module.css';
import { ImportDropzoneField } from './ImportDropzoneFile';
export function ImportDropzone() {
return (
<Stack spacing={0}>
<Field id={'file'} name={'file'} type="file">
{({ form: { setFieldValue } }) => (
<Dropzone
onDrop={(files) => setFieldValue('file', files[0])}
onReject={(files) => console.log('rejected files', files)}
maxSize={5 * 1024 ** 2}
accept={[MIME_TYPES.csv, MIME_TYPES.xls, MIME_TYPES.xlsx]}
classNames={{ content: styles.dropzoneContent }}
>
<Stack spacing={10} className={styles.content}>
<Box className={styles.iconWrap}>
<Icon icon="download" iconSize={26} />
</Box>
<Stack spacing={4}>
<h4 className={styles.title}>
Drag images here or click to select files
</h4>
<span className={styles.subtitle}>
Drag and Drop file here or Choose file
</span>
</Stack>
<Box>
<Button intent="none" minimal outlined>
Upload File
</Button>
</Box>
</Stack>
</Dropzone>
{({ form }) => (
<ImportDropzoneField
value={form.file}
onChange={(file) => {
form.setFieldValue('file', file);
}}
/>
)}
</Field>

View File

@@ -0,0 +1,80 @@
// @ts-nocheck
import { useRef } from 'react';
import { Button, Intent } from '@blueprintjs/core';
import { Box, Icon, Stack } from '@/components';
import { Dropzone, DropzoneProps } from '@/components/Dropzone';
import { MIME_TYPES } from '@/components/Dropzone/mine-types';
import styles from './ImportDropzone.module.css';
import { useUncontrolled } from '@/hooks/useUncontrolled';
interface ImportDropzoneFieldProps {
initialValue?: File;
value?: File;
onChange?: (file: File) => void;
dropzoneProps?: DropzoneProps;
}
export function ImportDropzoneField({
initialValue,
value,
onChange,
dropzoneProps,
}: ImportDropzoneFieldProps) {
const [localValue, handleChange] = useUncontrolled({
value,
initialValue,
finalValue: null,
onChange,
});
const openRef = useRef<() => void>(null);
const handleRemove = () => {
handleChange(null);
};
return (
<Dropzone
onDrop={(files) => handleChange(files[0])}
onReject={(files) => console.log('rejected files', files)}
maxSize={5 * 1024 ** 2}
accept={[MIME_TYPES.csv, MIME_TYPES.xls, MIME_TYPES.xlsx]}
classNames={{ content: styles.dropzoneContent }}
activateOnClick={false}
openRef={openRef}
{...dropzoneProps}
>
<Stack spacing={12} align="center" className={styles.content}>
<Box className={styles.iconWrap}>
<Icon icon="download" iconSize={26} />
</Box>
{localValue ? (
<Stack spacing={6} justify="center" align="center">
<h4 className={styles.title}>{localValue.name}</h4>
<Button small minimal intent={Intent.DANGER} onClick={handleRemove}>
Remove
</Button>
</Stack>
) : (
<Stack spacing={4} align="center">
<h4 className={styles.title}>
Drag images here or click to select files
</h4>
<span className={styles.subtitle}>
Drag and Drop file here or Choose file
</span>
</Stack>
)}
<Button
intent="none"
onClick={() => openRef.current?.()}
style={{ pointerEvents: 'all' }}
minimal
outlined
>
{localValue ? 'Replace File' : 'Upload File'}
</Button>
</Stack>
</Dropzone>
);
}

View File

@@ -0,0 +1,5 @@
.root{
}

View File

@@ -0,0 +1,9 @@
import styles from './ImportFileUploadStep.module.scss';
interface ImportFileContainerProps {
children: React.ReactNode;
}
export function ImportFileContainer({ children }: ImportFileContainerProps) {
return <div className={styles.content}>{children}</div>;
}

View File

@@ -0,0 +1,26 @@
// @ts-nocheck
import clsx from 'classnames';
import { Group } from '@/components';
import { CLASSES } from '@/constants';
import { Button, Intent } from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import styles from './ImportFileActions.module.scss';
import { useImportFileContext } from './ImportFileProvider';
export function ImportFileUploadFooterActions() {
const { isSubmitting } = useFormikContext();
const { setStep } = useImportFileContext();
const handleCancelBtnClick = () => {};
return (
<div className={clsx(CLASSES.PAGE_FORM_FLOATING_ACTIONS, styles.root)}>
<Group spacing={10}>
<Button onClick={handleCancelBtnClick}>Cancel</Button>
<Button type="submit" intent={Intent.PRIMARY} loading={isSubmitting}>
Next
</Button>
</Group>
</div>
);
}

View File

@@ -1,6 +1,11 @@
.table {
width: 100%;
margin-top: 1.8rem;
margin-top: 1.4rem;
th.label,
td.label{
width: 32% !important;
}
thead{
th{
@@ -8,14 +13,19 @@
padding-top: 8px;
padding-bottom: 8px;
color: #738091;
}
th.label{
width: 32%;
font-weight: 500;
}
}
tbody{
tr td {
vertical-align: middle;
}
tr td{
:global(.bp4-popover-target .bp4-button){
max-width: 250px;
}
}
}
}

View File

@@ -1,32 +1,36 @@
import { useMemo } from 'react';
import clsx from 'classnames';
import { Button, Intent } from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import { FSelect, Group } from '@/components';
import { ImportFileMappingForm } from './ImportFileMappingForm';
import { useImportFileContext } from './ImportFileProvider';
import styles from './ImportFileMapping.module.scss';
import { CLASSES } from '@/constants';
import { Button, Intent } from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import { ImportFileContainer } from './ImportFileContainer';
import styles from './ImportFileMapping.module.scss';
import { ImportStepperStep } from './_types';
export function ImportFileMapping() {
return (
<ImportFileMappingForm>
<p>
Review and map the column headers in your csv/xlsx file with the
Bigcapital fields.
</p>
<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={'label'}>Bigcapital Fields</th>
<th className={'field'}>Sheet Column Headers</th>
</tr>
</thead>
<tbody>
<ImportFileMappingFields />
</tbody>
</table>
<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>
@@ -43,14 +47,14 @@ function ImportFileMappingFields() {
const columns = entityColumns.map((column, index) => (
<tr key={index}>
<td className={'label'}>{column.name}</td>
<td className={'field'}>
<td className={styles.label}>{column.name}</td>
<td className={styles.field}>
<FSelect
name={column.key}
items={items}
fill
popoverProps={{ minimal: false }}
minimal={false}
popoverProps={{ minimal: true }}
minimal={true}
fill={true}
/>
</td>
</tr>
@@ -60,14 +64,19 @@ function ImportFileMappingFields() {
function ImportFileMappingFloatingActions() {
const { isSubmitting } = useFormikContext();
const { setStep } = useImportFileContext();
const handleCancelBtnClick = () => {
setStep(ImportStepperStep.Upload);
};
return (
<div className={clsx(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<Group>
<Group spacing={10}>
<Button onClick={handleCancelBtnClick}>Cancel</Button>
<Button type="submit" intent={Intent.PRIMARY} loading={isSubmitting}>
Next
</Button>
<Button>Cancel</Button>
</Group>
</div>
);

View File

@@ -1,6 +1,7 @@
// @ts-nocheck
import { Button, Callout, Intent, Text } from '@blueprintjs/core';
import clsx from 'classnames';
import { useHistory } from 'react-router-dom';
import {
ImportFilePreviewBootProvider,
useImportFilePreviewBootContext,
@@ -9,7 +10,7 @@ import { useImportFileContext } from './ImportFileProvider';
import { AppToaster, Card, Group } from '@/components';
import { useImportFileProcess } from '@/hooks/query/import';
import { CLASSES } from '@/constants';
import { useHistory } from 'react-router-dom';
import { ImportStepperStep } from './_types';
export function ImportFilePreview() {
const { importId } = useImportFileContext();
@@ -81,7 +82,7 @@ function ImportFilePreviewContent() {
}
function ImportFilePreviewFloatingActions() {
const { importId } = useImportFileContext();
const { importId, setStep } = useImportFileContext();
const { importPreview } = useImportFilePreviewBootContext();
const { mutateAsync: importFile, isLoading: isImportFileLoading } =
useImportFileProcess();
@@ -102,9 +103,14 @@ function ImportFilePreviewFloatingActions() {
})
.catch((error) => {});
};
const handleCancelBtnClick = () => {
setStep(ImportStepperStep.Mapping);
};
return (
<div className={clsx(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<Group>
<Group spacing={10}>
<Button onClick={handleCancelBtnClick}>Cancel</Button>
<Button
type="submit"
intent={Intent.PRIMARY}
@@ -114,7 +120,6 @@ function ImportFilePreviewFloatingActions() {
>
Import
</Button>
<Button>Cancel</Button>
</Group>
</div>
);

View File

@@ -5,6 +5,7 @@ import { Intent } from '@blueprintjs/core';
import { Formik, Form, FormikHelpers } from 'formik';
import * as Yup from 'yup';
import { useImportFileContext } from './ImportFileProvider';
import { ImportStepperStep } from './_types';
const initialValues = {
file: null,
@@ -22,9 +23,14 @@ interface ImportFileUploadValues {
file: File | null;
}
export function ImportFileUploadForm({ children }: ImportFileUploadFormProps) {
export function ImportFileUploadForm({
children,
formikProps,
formProps,
}: ImportFileUploadFormProps) {
const { mutateAsync: uploadImportFile } = useImportFileUpload();
const { setStep, setSheetColumns, setEntityColumns, setImportId } = useImportFileContext();
const { setStep, setSheetColumns, setEntityColumns, setImportId } =
useImportFileContext();
const handleSubmit = (
values: ImportFileUploadValues,
@@ -42,7 +48,7 @@ export function ImportFileUploadForm({ children }: ImportFileUploadFormProps) {
setImportId(data.import.import_id);
setSheetColumns(data.sheet_columns);
setEntityColumns(data.resource_columns);
setStep(1);
setStep(ImportStepperStep.Mapping);
setSubmitting(false);
})
.catch((error) => {
@@ -55,8 +61,9 @@ export function ImportFileUploadForm({ children }: ImportFileUploadFormProps) {
initialValues={initialValues}
onSubmit={handleSubmit}
validationSchema={validationSchema}
{...formikProps}
>
<Form>{children}</Form>
<Form {...formProps}>{children}</Form>
</Formik>
);
}

View File

@@ -0,0 +1,16 @@
.root {
margin-top: 2.2rem
}
.content {
flex: 1;
padding: 32px 20px;
min-width: 660px;
max-width: 760px;
width: 75%;
margin-left: auto;
margin-right: auto;
}
.form {
}

View File

@@ -1,3 +0,0 @@
.root {
margin-top: 2.2rem
}

View File

@@ -1,40 +1,26 @@
// @ts-nocheck
import clsx from 'classnames';
import { Group, Stack } from '@/components';
import { Stack } from '@/components';
import { ImportDropzone } from './ImportDropzone';
import { ImportSampleDownload } from './ImportSampleDownload';
import { CLASSES } from '@/constants';
import { Button, Intent } from '@blueprintjs/core';
import { ImportFileUploadForm } from './ImportFileUploadForm';
import { useFormikContext } from 'formik';
import { ImportFileUploadFooterActions } from './ImportFileFooterActions';
import { ImportFileContainer } from './ImportFileContainer';
export function ImportFileUploadStep() {
return (
<ImportFileUploadForm>
<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>
<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>
<Stack spacing={40}>
<ImportDropzone />
<ImportSampleDownload />
</Stack>
</ImportFileContainer>
<Stack spacing={40}>
<ImportDropzone />
<ImportSampleDownload />
</Stack>
<ImportFileUploadFooterActions />
</ImportFileUploadForm>
);
}
function ImportFileUploadFooterActions() {
const { isSubmitting } = useFormikContext();
return (
<div className={clsx(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<Group spacing={10}>
<Button type="submit" intent={Intent.PRIMARY} loading={isSubmitting}>
Next
</Button>
<Button>Cancel</Button>
</Group>
</div>
);
}

View File

@@ -1,10 +1,5 @@
.root{
padding: 32px 40px;
min-width: 700px;
max-width: 800px;
width: 75%;
margin-left: auto;
margin-right: auto;
}
.rootWrap {
max-width: 1800px;

View File

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

View File

@@ -1,3 +1,18 @@
.content {
margin-top: 2.4rem;
margin-top: 0;
margin-bottom: 0;
border-top: 1px solid #DCE0E5;
}
.root {
}
.items {
padding: 32px 20px;
min-width: 660px;
max-width: 760px;
width: 75%;
margin-left: auto;
margin-right: auto;
}

View File

@@ -2,16 +2,22 @@
import { Stepper } from '@/components/Stepper';
import { ImportFileUploadStep } from './ImportFileUploadStep';
import styles from './ImportStepper.module.scss';
import { useImportFileContext } from './ImportFileProvider';
import { ImportFileMapping } from './ImportFileMapping';
import { ImportFilePreview } from './ImportFilePreview';
import styles from './ImportStepper.module.scss';
export function ImportStepper() {
const { step } = useImportFileContext();
return (
<Stepper active={step} classNames={{ content: styles.content }}>
<Stepper
active={step}
classNames={{
content: styles.content,
items: styles.items,
}}
>
<Stepper.Step label={'File Upload'}>
<ImportFileUploadStep />
</Stepper.Step>

View File

@@ -0,0 +1,5 @@
export enum ImportStepperStep {
Upload = 0,
Mapping = 1,
Preview = 2,
}