mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-22 15:50:32 +00:00
feat(webapp): import resource UI
This commit is contained in:
@@ -20,11 +20,11 @@
|
|||||||
"@testing-library/jest-dom": "^4.2.4",
|
"@testing-library/jest-dom": "^4.2.4",
|
||||||
"@testing-library/react": "^9.4.0",
|
"@testing-library/react": "^9.4.0",
|
||||||
"@testing-library/user-event": "^7.2.1",
|
"@testing-library/user-event": "^7.2.1",
|
||||||
"@tiptap/extension-color": "latest",
|
|
||||||
"@tiptap/extension-text-style": "2.1.13",
|
|
||||||
"@tiptap/core": "2.1.13",
|
"@tiptap/core": "2.1.13",
|
||||||
"@tiptap/pm": "2.1.13",
|
"@tiptap/extension-color": "latest",
|
||||||
"@tiptap/extension-list-item": "2.1.13",
|
"@tiptap/extension-list-item": "2.1.13",
|
||||||
|
"@tiptap/extension-text-style": "2.1.13",
|
||||||
|
"@tiptap/pm": "2.1.13",
|
||||||
"@tiptap/react": "2.1.13",
|
"@tiptap/react": "2.1.13",
|
||||||
"@tiptap/starter-kit": "2.1.13",
|
"@tiptap/starter-kit": "2.1.13",
|
||||||
"@types/jest": "^26.0.15",
|
"@types/jest": "^26.0.15",
|
||||||
@@ -38,9 +38,9 @@
|
|||||||
"@types/react-redux": "^7.1.24",
|
"@types/react-redux": "^7.1.24",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@types/react-transition-group": "^4.4.5",
|
"@types/react-transition-group": "^4.4.5",
|
||||||
|
"@types/socket.io-client": "^3.0.0",
|
||||||
"@types/styled-components": "^5.1.25",
|
"@types/styled-components": "^5.1.25",
|
||||||
"@types/yup": "^0.29.13",
|
"@types/yup": "^0.29.13",
|
||||||
"@types/socket.io-client": "^3.0.0",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^2.10.0",
|
"@typescript-eslint/eslint-plugin": "^2.10.0",
|
||||||
"@typescript-eslint/parser": "^2.10.0",
|
"@typescript-eslint/parser": "^2.10.0",
|
||||||
"@welldone-software/why-did-you-render": "^6.0.0-rc.1",
|
"@welldone-software/why-did-you-render": "^6.0.0-rc.1",
|
||||||
@@ -69,10 +69,9 @@
|
|||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"moment-timezone": "^0.5.33",
|
"moment-timezone": "^0.5.33",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"prop-types": "15.8.1",
|
|
||||||
"plaid": "^9.3.0",
|
"plaid": "^9.3.0",
|
||||||
"plaid-threads": "^11.4.3",
|
"plaid-threads": "^11.4.3",
|
||||||
"react-plaid-link": "^3.2.1",
|
"prop-types": "15.8.1",
|
||||||
"query-string": "^7.1.1",
|
"query-string": "^7.1.1",
|
||||||
"ramda": "^0.27.1",
|
"ramda": "^0.27.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -82,11 +81,13 @@
|
|||||||
"react-dev-utils": "^11.0.4",
|
"react-dev-utils": "^11.0.4",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-dropzone": "^11.0.1",
|
"react-dropzone": "^11.0.1",
|
||||||
|
"react-dropzone-esm": "^15.0.1",
|
||||||
"react-error-boundary": "^3.0.2",
|
"react-error-boundary": "^3.0.2",
|
||||||
"react-error-overlay": "^6.0.9",
|
"react-error-overlay": "^6.0.9",
|
||||||
"react-hotkeys-hook": "^3.0.3",
|
"react-hotkeys-hook": "^3.0.3",
|
||||||
"react-intl-universal": "^2.4.7",
|
"react-intl-universal": "^2.4.7",
|
||||||
"react-loadable": "^5.5.0",
|
"react-loadable": "^5.5.0",
|
||||||
|
"react-plaid-link": "^3.2.1",
|
||||||
"react-query": "^3.6.0",
|
"react-query": "^3.6.0",
|
||||||
"react-query-devtools": "^2.1.1",
|
"react-query-devtools": "^2.1.1",
|
||||||
"react-redux": "^7.2.9",
|
"react-redux": "^7.2.9",
|
||||||
@@ -112,10 +113,10 @@
|
|||||||
"rtl-detect": "^1.0.3",
|
"rtl-detect": "^1.0.3",
|
||||||
"sass": "^1.68.0",
|
"sass": "^1.68.0",
|
||||||
"semver": "6.3.0",
|
"semver": "6.3.0",
|
||||||
|
"socket.io-client": "^4.7.4",
|
||||||
"style-loader": "0.23.1",
|
"style-loader": "0.23.1",
|
||||||
"styled-components": "^5.3.1",
|
"styled-components": "^5.3.1",
|
||||||
"stylis-rtlcss": "^2.1.1",
|
"stylis-rtlcss": "^2.1.1",
|
||||||
"socket.io-client": "^4.7.4",
|
|
||||||
"typescript": "^4.8.3",
|
"typescript": "^4.8.3",
|
||||||
"yup": "^0.28.1"
|
"yup": "^0.28.1"
|
||||||
},
|
},
|
||||||
|
|||||||
12
packages/webapp/src/components/Dropzone/Dropzone.module.css
Normal file
12
packages/webapp/src/components/Dropzone/Dropzone.module.css
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
|
||||||
|
.root {
|
||||||
|
padding: 20px;
|
||||||
|
border: 2px dotted #c5cbd3;
|
||||||
|
border-radius: 6px;
|
||||||
|
min-height: 200px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #fff;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
266
packages/webapp/src/components/Dropzone/Dropzone.tsx
Normal file
266
packages/webapp/src/components/Dropzone/Dropzone.tsx
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import clsx from 'classnames';
|
||||||
|
import {
|
||||||
|
Accept,
|
||||||
|
DropEvent,
|
||||||
|
FileError,
|
||||||
|
FileRejection,
|
||||||
|
FileWithPath,
|
||||||
|
useDropzone,
|
||||||
|
} from 'react-dropzone-esm';
|
||||||
|
import { DropzoneProvider } from './DropzoneProvider';
|
||||||
|
import { DropzoneAccept, DropzoneIdle, DropzoneReject } from './DropzoneStatus';
|
||||||
|
import { Box } from '../Layout';
|
||||||
|
import styles from './Dropzone.module.css';
|
||||||
|
import { CloudLoadingIndicator } from '../Indicator';
|
||||||
|
|
||||||
|
export type DropzoneStylesNames = 'root' | 'inner';
|
||||||
|
export type DropzoneVariant = 'filled' | 'light';
|
||||||
|
export type DropzoneCssVariables = {
|
||||||
|
root:
|
||||||
|
| '--dropzone-radius'
|
||||||
|
| '--dropzone-accept-color'
|
||||||
|
| '--dropzone-accept-bg'
|
||||||
|
| '--dropzone-reject-color'
|
||||||
|
| '--dropzone-reject-bg';
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DropzoneProps {
|
||||||
|
/** Key of `theme.colors` or any valid CSS color to set colors of `Dropzone.Accept`, `theme.primaryColor` by default */
|
||||||
|
acceptColor?: MantineColor;
|
||||||
|
|
||||||
|
/** Key of `theme.colors` or any valid CSS color to set colors of `Dropzone.Reject`, `'red'` by default */
|
||||||
|
rejectColor?: MantineColor;
|
||||||
|
|
||||||
|
/** Key of `theme.radius` or any valid CSS value to set `border-radius`, numbers are converted to rem, `theme.defaultRadius` by default */
|
||||||
|
radius?: MantineRadius;
|
||||||
|
|
||||||
|
/** Determines whether files capturing should be disabled, `false` by default */
|
||||||
|
disabled?: boolean;
|
||||||
|
|
||||||
|
/** Called when any files are dropped to the dropzone */
|
||||||
|
onDropAny?: (files: FileWithPath[], fileRejections: FileRejection[]) => void;
|
||||||
|
|
||||||
|
/** Called when valid files are dropped to the dropzone */
|
||||||
|
onDrop: (files: FileWithPath[]) => void;
|
||||||
|
|
||||||
|
/** Called when dropped files do not meet file restrictions */
|
||||||
|
onReject?: (fileRejections: FileRejection[]) => void;
|
||||||
|
|
||||||
|
/** Determines whether a loading overlay should be displayed over the dropzone, `false` by default */
|
||||||
|
loading?: boolean;
|
||||||
|
|
||||||
|
/** Mime types of the files that dropzone can accepts. By default, dropzone accepts all file types. */
|
||||||
|
accept?: Accept | string[];
|
||||||
|
|
||||||
|
/** A ref function which when called opens the file system file picker */
|
||||||
|
openRef?: React.ForwardedRef<() => void | undefined>;
|
||||||
|
|
||||||
|
/** Determines whether multiple files can be dropped to the dropzone or selected from file system picker, `true` by default */
|
||||||
|
multiple?: boolean;
|
||||||
|
|
||||||
|
/** Maximum file size in bytes */
|
||||||
|
maxSize?: number;
|
||||||
|
|
||||||
|
/** Name of the form control. Submitted with the form as part of a name/value pair. */
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
/** Maximum number of files that can be picked at once */
|
||||||
|
maxFiles?: number;
|
||||||
|
|
||||||
|
/** Set to autofocus the root element */
|
||||||
|
autoFocus?: boolean;
|
||||||
|
|
||||||
|
/** If `false`, disables click to open the native file selection dialog */
|
||||||
|
activateOnClick?: boolean;
|
||||||
|
|
||||||
|
/** If `false`, disables drag 'n' drop */
|
||||||
|
activateOnDrag?: boolean;
|
||||||
|
|
||||||
|
/** If `false`, disables Space/Enter to open the native file selection dialog. Note that it also stops tracking the focus state. */
|
||||||
|
activateOnKeyboard?: boolean;
|
||||||
|
|
||||||
|
/** If `false`, stops drag event propagation to parents */
|
||||||
|
dragEventsBubbling?: boolean;
|
||||||
|
|
||||||
|
/** Called when the `dragenter` event occurs */
|
||||||
|
onDragEnter?: (event: React.DragEvent<HTMLElement>) => void;
|
||||||
|
|
||||||
|
/** Called when the `dragleave` event occurs */
|
||||||
|
onDragLeave?: (event: React.DragEvent<HTMLElement>) => void;
|
||||||
|
|
||||||
|
/** Called when the `dragover` event occurs */
|
||||||
|
onDragOver?: (event: React.DragEvent<HTMLElement>) => void;
|
||||||
|
|
||||||
|
/** Called when user closes the file selection dialog with no selection */
|
||||||
|
onFileDialogCancel?: () => void;
|
||||||
|
|
||||||
|
/** Called when user opens the file selection dialog */
|
||||||
|
onFileDialogOpen?: () => void;
|
||||||
|
|
||||||
|
/** If `false`, allow dropped items to take over the current browser window */
|
||||||
|
preventDropOnDocument?: boolean;
|
||||||
|
|
||||||
|
/** Set to true to use the File System Access API to open the file picker instead of using an <input type="file"> click event, defaults to true */
|
||||||
|
useFsAccessApi?: boolean;
|
||||||
|
|
||||||
|
/** Use this to provide a custom file aggregator */
|
||||||
|
getFilesFromEvent?: (
|
||||||
|
event: DropEvent,
|
||||||
|
) => Promise<Array<File | DataTransferItem>>;
|
||||||
|
|
||||||
|
/** Custom validation function. It must return null if there's no errors. */
|
||||||
|
validator?: <T extends File>(file: T) => FileError | FileError[] | null;
|
||||||
|
|
||||||
|
/** Determines whether pointer events should be enabled on the inner element, `false` by default */
|
||||||
|
enablePointerEvents?: boolean;
|
||||||
|
|
||||||
|
/** Props passed down to the Loader component */
|
||||||
|
loaderProps?: LoaderProps;
|
||||||
|
|
||||||
|
/** Props passed down to the internal Input component */
|
||||||
|
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DropzoneFactory = Factory<{
|
||||||
|
props: DropzoneProps;
|
||||||
|
ref: HTMLDivElement;
|
||||||
|
stylesNames: DropzoneStylesNames;
|
||||||
|
vars: DropzoneCssVariables;
|
||||||
|
staticComponents: {
|
||||||
|
Accept: typeof DropzoneAccept;
|
||||||
|
Idle: typeof DropzoneIdle;
|
||||||
|
Reject: typeof DropzoneReject;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const defaultProps: Partial<DropzoneProps> = {
|
||||||
|
loading: false,
|
||||||
|
multiple: true,
|
||||||
|
maxSize: Infinity,
|
||||||
|
autoFocus: false,
|
||||||
|
activateOnClick: true,
|
||||||
|
activateOnDrag: true,
|
||||||
|
dragEventsBubbling: true,
|
||||||
|
activateOnKeyboard: true,
|
||||||
|
useFsAccessApi: true,
|
||||||
|
variant: 'light',
|
||||||
|
rejectColor: 'red',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Dropzone = (_props: DropzoneProps) => {
|
||||||
|
const {
|
||||||
|
// classNames,
|
||||||
|
// className,
|
||||||
|
// style,
|
||||||
|
// styles,
|
||||||
|
// unstyled,
|
||||||
|
// vars,
|
||||||
|
radius,
|
||||||
|
disabled,
|
||||||
|
loading,
|
||||||
|
multiple,
|
||||||
|
maxSize,
|
||||||
|
accept,
|
||||||
|
children,
|
||||||
|
onDropAny,
|
||||||
|
onDrop,
|
||||||
|
onReject,
|
||||||
|
openRef,
|
||||||
|
name,
|
||||||
|
maxFiles,
|
||||||
|
autoFocus,
|
||||||
|
activateOnClick,
|
||||||
|
activateOnDrag,
|
||||||
|
dragEventsBubbling,
|
||||||
|
activateOnKeyboard,
|
||||||
|
onDragEnter,
|
||||||
|
onDragLeave,
|
||||||
|
onDragOver,
|
||||||
|
onFileDialogCancel,
|
||||||
|
onFileDialogOpen,
|
||||||
|
preventDropOnDocument,
|
||||||
|
useFsAccessApi,
|
||||||
|
getFilesFromEvent,
|
||||||
|
validator,
|
||||||
|
rejectColor,
|
||||||
|
acceptColor,
|
||||||
|
enablePointerEvents,
|
||||||
|
loaderProps,
|
||||||
|
inputProps,
|
||||||
|
// mod,
|
||||||
|
classNames,
|
||||||
|
...others
|
||||||
|
} = {
|
||||||
|
...defaultProps,
|
||||||
|
..._props,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { getRootProps, getInputProps, isDragAccept, isDragReject, open } =
|
||||||
|
useDropzone({
|
||||||
|
onDrop: onDropAny,
|
||||||
|
onDropAccepted: onDrop,
|
||||||
|
onDropRejected: onReject,
|
||||||
|
disabled: disabled || loading,
|
||||||
|
accept: Array.isArray(accept)
|
||||||
|
? accept.reduce((r, key) => ({ ...r, [key]: [] }), {})
|
||||||
|
: accept,
|
||||||
|
multiple,
|
||||||
|
maxSize,
|
||||||
|
maxFiles,
|
||||||
|
autoFocus,
|
||||||
|
noClick: !activateOnClick,
|
||||||
|
noDrag: !activateOnDrag,
|
||||||
|
noDragEventsBubbling: !dragEventsBubbling,
|
||||||
|
noKeyboard: !activateOnKeyboard,
|
||||||
|
onDragEnter,
|
||||||
|
onDragLeave,
|
||||||
|
onDragOver,
|
||||||
|
onFileDialogCancel,
|
||||||
|
onFileDialogOpen,
|
||||||
|
preventDropOnDocument,
|
||||||
|
useFsAccessApi,
|
||||||
|
validator,
|
||||||
|
...(getFilesFromEvent ? { getFilesFromEvent } : null),
|
||||||
|
});
|
||||||
|
|
||||||
|
const isIdle = !isDragAccept && !isDragReject;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropzoneProvider
|
||||||
|
value={{ accept: isDragAccept, reject: isDragReject, idle: isIdle }}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
{...getRootProps({
|
||||||
|
className: clsx(styles.root, classNames?.root),
|
||||||
|
})}
|
||||||
|
// {...getStyles('root', { focusable: true })}
|
||||||
|
{...others}
|
||||||
|
mod={[
|
||||||
|
{
|
||||||
|
accept: isDragAccept,
|
||||||
|
reject: isDragReject,
|
||||||
|
idle: isIdle,
|
||||||
|
loading,
|
||||||
|
'activate-on-click': activateOnClick,
|
||||||
|
},
|
||||||
|
// mod,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<input {...getInputProps(inputProps)} name={name} />
|
||||||
|
<div
|
||||||
|
data-enable-pointer-events={enablePointerEvents || undefined}
|
||||||
|
className={classNames?.content}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
</DropzoneProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Dropzone.displayName = '@mantine/dropzone/Dropzone';
|
||||||
|
Dropzone.Accept = DropzoneAccept;
|
||||||
|
Dropzone.Idle = DropzoneIdle;
|
||||||
|
Dropzone.Reject = DropzoneReject;
|
||||||
12
packages/webapp/src/components/Dropzone/DropzoneProvider.tsx
Normal file
12
packages/webapp/src/components/Dropzone/DropzoneProvider.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { createSafeContext } from './create-safe-context';
|
||||||
|
|
||||||
|
export interface DropzoneContextValue {
|
||||||
|
idle: boolean;
|
||||||
|
accept: boolean;
|
||||||
|
reject: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const [DropzoneProvider, useDropzoneContext] =
|
||||||
|
createSafeContext<DropzoneContextValue>(
|
||||||
|
'Dropzone component was not found in tree',
|
||||||
|
);
|
||||||
36
packages/webapp/src/components/Dropzone/DropzoneStatus.tsx
Normal file
36
packages/webapp/src/components/Dropzone/DropzoneStatus.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import React, { cloneElement } from 'react';
|
||||||
|
import { upperFirst } from 'lodash';
|
||||||
|
import { DropzoneContextValue, useDropzoneContext } from './DropzoneProvider';
|
||||||
|
import { isElement } from '@/utils/is-element';
|
||||||
|
|
||||||
|
export interface DropzoneStatusProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DropzoneStatusComponent = React.FC<DropzoneStatusProps>;
|
||||||
|
|
||||||
|
function createDropzoneStatus(status: keyof DropzoneContextValue) {
|
||||||
|
const Component: DropzoneStatusComponent = (props) => {
|
||||||
|
const { children, ...others } = props;
|
||||||
|
|
||||||
|
const ctx = useDropzoneContext();
|
||||||
|
const _children = isElement(children) ? children : <span>{children}</span>;
|
||||||
|
|
||||||
|
if (ctx[status]) {
|
||||||
|
return cloneElement(_children as JSX.Element, others);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
Component.displayName = `@bigcapital/core/dropzone/${upperFirst(status)}`;
|
||||||
|
|
||||||
|
return Component;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DropzoneAccept = createDropzoneStatus('accept');
|
||||||
|
export const DropzoneReject = createDropzoneStatus('reject');
|
||||||
|
export const DropzoneIdle = createDropzoneStatus('idle');
|
||||||
|
|
||||||
|
export type DropzoneAcceptProps = DropzoneStatusProps;
|
||||||
|
export type DropzoneRejectProps = DropzoneStatusProps;
|
||||||
|
export type DropzoneIdleProps = DropzoneStatusProps;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import React, { createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
export function createSafeContext<ContextValue>(errorMessage: string) {
|
||||||
|
const Context = createContext<ContextValue | null>(null);
|
||||||
|
|
||||||
|
const useSafeContext = () => {
|
||||||
|
const ctx = useContext(Context);
|
||||||
|
|
||||||
|
if (ctx === null) {
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Provider = ({
|
||||||
|
children,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
value: ContextValue;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => <Context.Provider value={value}>{children}</Context.Provider>;
|
||||||
|
|
||||||
|
return [Provider, useSafeContext] as const;
|
||||||
|
}
|
||||||
1
packages/webapp/src/components/Dropzone/index.ts
Normal file
1
packages/webapp/src/components/Dropzone/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './Dropzone';
|
||||||
39
packages/webapp/src/components/Dropzone/mine-types.ts
Normal file
39
packages/webapp/src/components/Dropzone/mine-types.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
export const MIME_TYPES = {
|
||||||
|
// Images
|
||||||
|
png: 'image/png',
|
||||||
|
gif: 'image/gif',
|
||||||
|
jpeg: 'image/jpeg',
|
||||||
|
svg: 'image/svg+xml',
|
||||||
|
webp: 'image/webp',
|
||||||
|
avif: 'image/avif',
|
||||||
|
heic: 'image/heic',
|
||||||
|
|
||||||
|
// Documents
|
||||||
|
mp4: 'video/mp4',
|
||||||
|
zip: 'application/zip',
|
||||||
|
csv: 'text/csv',
|
||||||
|
pdf: 'application/pdf',
|
||||||
|
doc: 'application/msword',
|
||||||
|
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
xls: 'application/vnd.ms-excel',
|
||||||
|
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
ppt: 'application/vnd.ms-powerpoint',
|
||||||
|
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
|
exe: 'application/vnd.microsoft.portable-executable',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const IMAGE_MIME_TYPE = [
|
||||||
|
MIME_TYPES.png,
|
||||||
|
MIME_TYPES.gif,
|
||||||
|
MIME_TYPES.jpeg,
|
||||||
|
MIME_TYPES.svg,
|
||||||
|
MIME_TYPES.webp,
|
||||||
|
MIME_TYPES.avif,
|
||||||
|
MIME_TYPES.heic,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const PDF_MIME_TYPE = [MIME_TYPES.pdf];
|
||||||
|
export const MS_WORD_MIME_TYPE = [MIME_TYPES.doc, MIME_TYPES.docx];
|
||||||
|
export const MS_EXCEL_MIME_TYPE = [MIME_TYPES.xls, MIME_TYPES.xlsx];
|
||||||
|
export const MS_POWERPOINT_MIME_TYPE = [MIME_TYPES.ppt, MIME_TYPES.pptx];
|
||||||
|
export const EXE_MIME_TYPE = [MIME_TYPES.exe];
|
||||||
111
packages/webapp/src/components/Stepper/Stepper.tsx
Normal file
111
packages/webapp/src/components/Stepper/Stepper.tsx
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { cloneElement } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { toArray } from 'lodash';
|
||||||
|
import { Box } from '../Layout';
|
||||||
|
import { StepperCompleted } from './StepperCompleted';
|
||||||
|
import { StepperStep } from './StepperStep';
|
||||||
|
import { StepperStepState } from './types';
|
||||||
|
|
||||||
|
export interface StepperProps {
|
||||||
|
/** <Stepper.Step /> components */
|
||||||
|
children: React.ReactNode;
|
||||||
|
|
||||||
|
/** Index of the active step */
|
||||||
|
active: number;
|
||||||
|
|
||||||
|
/** Called when step is clicked */
|
||||||
|
onStepClick?: (stepIndex: number) => void;
|
||||||
|
|
||||||
|
/** Determines whether next steps can be selected, `true` by default **/
|
||||||
|
allowNextStepsSelect?: boolean;
|
||||||
|
|
||||||
|
classNames?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Stepper({
|
||||||
|
active,
|
||||||
|
onStepClick,
|
||||||
|
children,
|
||||||
|
classNames,
|
||||||
|
}: StepperProps) {
|
||||||
|
const convertedChildren = toArray(children) as React.ReactElement[];
|
||||||
|
const _children = convertedChildren.filter(
|
||||||
|
(child) => child.type !== StepperCompleted,
|
||||||
|
);
|
||||||
|
const completedStep = convertedChildren.find(
|
||||||
|
(item) => item.type === StepperCompleted,
|
||||||
|
);
|
||||||
|
const items = _children.reduce<React.ReactElement[]>((acc, item, index) => {
|
||||||
|
const state =
|
||||||
|
active === index
|
||||||
|
? StepperStepState.Progress
|
||||||
|
: active > index
|
||||||
|
? StepperStepState.Completed
|
||||||
|
: StepperStepState.Inactive;
|
||||||
|
|
||||||
|
const shouldAllowSelect = () => {
|
||||||
|
if (typeof onStepClick !== 'function') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (typeof item.props.allowStepSelect === 'boolean') {
|
||||||
|
return item.props.allowStepSelect;
|
||||||
|
}
|
||||||
|
return state === 'stepCompleted' || allowNextStepsSelect;
|
||||||
|
};
|
||||||
|
const isStepSelectionEnabled = shouldAllowSelect();
|
||||||
|
|
||||||
|
acc.push(
|
||||||
|
cloneElement(item, {
|
||||||
|
key: index,
|
||||||
|
step: index + 1,
|
||||||
|
state,
|
||||||
|
onClick: () => isStepSelectionEnabled && onStepClick?.(index),
|
||||||
|
allowStepClick: isStepSelectionEnabled,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (index !== _children.length - 1) {
|
||||||
|
acc.push(
|
||||||
|
<StepSeparator
|
||||||
|
data-active={index < active || undefined}
|
||||||
|
key={`separator-${index}`}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const stepContent = _children[active]?.props?.children;
|
||||||
|
const completedContent = completedStep?.props?.children;
|
||||||
|
const content =
|
||||||
|
active > _children.length - 1 ? completedContent : stepContent;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<StepsItems>{items}</StepsItems>
|
||||||
|
<StepsContent className={classNames?.content}>{content} </StepsContent>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Stepper.Step = StepperStep;
|
||||||
|
Stepper.Completed = StepperCompleted;
|
||||||
|
Stepper.displayName = '@bigcapital/core/stepper';
|
||||||
|
|
||||||
|
const StepsItems = styled(Box)`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
`;
|
||||||
|
const StepsContent = styled(Box)`
|
||||||
|
margin-top: 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
`;
|
||||||
|
const StepSeparator = styled.div`
|
||||||
|
flex: 1;
|
||||||
|
display: block;
|
||||||
|
border-color: #c5cbd3;
|
||||||
|
border-top-style: solid;
|
||||||
|
border-top-width: 1px;
|
||||||
|
`;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export interface StepperCompletedProps {
|
||||||
|
/** Label content */
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StepperCompleted: React.FC<StepperCompletedProps> = () => null;
|
||||||
|
StepperCompleted.displayName = '@bigcapital/core/StepperCompleted';
|
||||||
102
packages/webapp/src/components/Stepper/StepperStep.tsx
Normal file
102
packages/webapp/src/components/Stepper/StepperStep.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { StepperStepState } from './types';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { Icon } from '../Icon';
|
||||||
|
|
||||||
|
interface StepperStepProps {
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
step?: number;
|
||||||
|
active?: boolean;
|
||||||
|
state?: StepperStepState;
|
||||||
|
allowStepClick?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StepperStep({
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
step,
|
||||||
|
active,
|
||||||
|
state,
|
||||||
|
children,
|
||||||
|
}: StepperStepProps) {
|
||||||
|
return (
|
||||||
|
<StepButton>
|
||||||
|
<StepIconWrap>
|
||||||
|
<StepIcon
|
||||||
|
isCompleted={state === StepperStepState.Completed}
|
||||||
|
isActive={state === StepperStepState.Progress}
|
||||||
|
>
|
||||||
|
{state === StepperStepState.Completed && (
|
||||||
|
<Icon icon={'done'} iconSize={24} />
|
||||||
|
)}
|
||||||
|
<StepIconText>{step}</StepIconText>
|
||||||
|
</StepIcon>
|
||||||
|
</StepIconWrap>
|
||||||
|
|
||||||
|
<StepTextWrap>
|
||||||
|
<StepTitle
|
||||||
|
isCompleted={state === StepperStepState.Completed}
|
||||||
|
isActive={state === StepperStepState.Progress}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</StepTitle>
|
||||||
|
{description && (
|
||||||
|
<StepDescription
|
||||||
|
isCompleted={state === StepperStepState.Completed}
|
||||||
|
isActive={state === StepperStepState.Progress}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</StepDescription>
|
||||||
|
)}
|
||||||
|
</StepTextWrap>
|
||||||
|
</StepButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const StepButton = styled.button`
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
border: 0;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
text-align: left;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StepIcon = styled.span`
|
||||||
|
display: block;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
display: block;
|
||||||
|
line-height: 24px;
|
||||||
|
border-radius: 24px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: ${(props) =>
|
||||||
|
props.isCompleted || props.isActive ? 'rgb(0, 82, 204)' : '#9e9e9e'};
|
||||||
|
color: #fff;
|
||||||
|
margin: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StepTitle = styled.div`
|
||||||
|
color: ${(props) =>
|
||||||
|
props.isCompleted || props.isActive ? 'rgb(0, 82, 204)' : '#738091'};
|
||||||
|
`;
|
||||||
|
const StepDescription = styled.div`
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: ${(props) =>
|
||||||
|
props.isCompleted || props.isActive ? 'rgb(0, 82, 204)' : '#738091'};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StepIconWrap = styled.div`
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StepTextWrap = styled.div`
|
||||||
|
text-align: left;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StepIconText = styled.div``;
|
||||||
1
packages/webapp/src/components/Stepper/index.ts
Normal file
1
packages/webapp/src/components/Stepper/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './Stepper';
|
||||||
7
packages/webapp/src/components/Stepper/types.ts
Normal file
7
packages/webapp/src/components/Stepper/types.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export enum StepperStepState {
|
||||||
|
Progress = 'stepProgress',
|
||||||
|
Completed = 'stepCompleted',
|
||||||
|
Inactive = 'stepInactive',
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
DashboardActionViewsList,
|
DashboardActionViewsList,
|
||||||
DashboardFilterButton,
|
DashboardFilterButton,
|
||||||
DashboardRowsHeightButton,
|
DashboardRowsHeightButton,
|
||||||
DashboardActionsBar
|
DashboardActionsBar,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
|
|
||||||
import { AccountAction, AbilitySubject } from '@/constants/abilityOption';
|
import { AccountAction, AbilitySubject } from '@/constants/abilityOption';
|
||||||
@@ -37,6 +37,7 @@ import withSettings from '@/containers/Settings/withSettings';
|
|||||||
import withSettingsActions from '@/containers/Settings/withSettingsActions';
|
import withSettingsActions from '@/containers/Settings/withSettingsActions';
|
||||||
|
|
||||||
import { compose } from '@/utils';
|
import { compose } from '@/utils';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accounts actions bar.
|
* Accounts actions bar.
|
||||||
@@ -67,6 +68,8 @@ function AccountsActionsBar({
|
|||||||
}) {
|
}) {
|
||||||
const { resourceViews, fields } = useAccountsChartContext();
|
const { resourceViews, fields } = useAccountsChartContext();
|
||||||
|
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
const onClickNewAccount = () => {
|
const onClickNewAccount = () => {
|
||||||
openDialog(DialogsName.AccountForm, {});
|
openDialog(DialogsName.AccountForm, {});
|
||||||
};
|
};
|
||||||
@@ -111,6 +114,11 @@ function AccountsActionsBar({
|
|||||||
const handleTableRowSizeChange = (size) => {
|
const handleTableRowSizeChange = (size) => {
|
||||||
addSetting('accounts', 'tableSize', size);
|
addSetting('accounts', 'tableSize', size);
|
||||||
};
|
};
|
||||||
|
// handle the import button click.
|
||||||
|
const handleImportBtnClick = () => {
|
||||||
|
history.push('/accounts/import');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardActionsBar>
|
<DashboardActionsBar>
|
||||||
<NavbarGroup>
|
<NavbarGroup>
|
||||||
@@ -183,6 +191,7 @@ function AccountsActionsBar({
|
|||||||
className={Classes.MINIMAL}
|
className={Classes.MINIMAL}
|
||||||
icon={<Icon icon="file-import-16" iconSize={16} />}
|
icon={<Icon icon="file-import-16" iconSize={16} />}
|
||||||
text={<T id={'import'} />}
|
text={<T id={'import'} />}
|
||||||
|
onClick={handleImportBtnClick}
|
||||||
/>
|
/>
|
||||||
<NavbarDivider />
|
<NavbarDivider />
|
||||||
<DashboardRowsHeightButton
|
<DashboardRowsHeightButton
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
|
||||||
|
.root {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.title{
|
||||||
|
color: #5F6B7C;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconWrap{
|
||||||
|
color: #8F99A8;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
color: #8F99A8;
|
||||||
|
}
|
||||||
|
.dropzoneContent{
|
||||||
|
text-align: center;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons-wrap {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzoneHint{
|
||||||
|
display: flex;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #8F99A8;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
49
packages/webapp/src/containers/Import/ImportDropzone.tsx
Normal file
49
packages/webapp/src/containers/Import/ImportDropzone.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// @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 styles from './ImportDropzone.module.css';
|
||||||
|
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Group className={styles.dropzoneHint}>
|
||||||
|
<Box>Supperted Formats: CSV, XLSX</Box>
|
||||||
|
<Box>Maximum size: 25MB</Box>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 1.8rem;
|
||||||
|
|
||||||
|
thead{
|
||||||
|
th{
|
||||||
|
border-top: 1px solid #d9d9da;
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
color: #738091;
|
||||||
|
}
|
||||||
|
th.label{
|
||||||
|
width: 32%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody{
|
||||||
|
tr td {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
packages/webapp/src/containers/Import/ImportFileMapping.tsx
Normal file
74
packages/webapp/src/containers/Import/ImportFileMapping.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import clsx from 'classnames';
|
||||||
|
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';
|
||||||
|
|
||||||
|
export function ImportFileMapping() {
|
||||||
|
return (
|
||||||
|
<ImportFileMappingForm>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<ImportFileMappingFloatingActions />
|
||||||
|
</ImportFileMappingForm>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ImportFileMappingFields() {
|
||||||
|
const { entityColumns, sheetColumns } = useImportFileContext();
|
||||||
|
|
||||||
|
const items = useMemo(
|
||||||
|
() => sheetColumns.map((column) => ({ value: column, text: column })),
|
||||||
|
[sheetColumns],
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = entityColumns.map((column, index) => (
|
||||||
|
<tr key={index}>
|
||||||
|
<td className={'label'}>{column.name}</td>
|
||||||
|
<td className={'field'}>
|
||||||
|
<FSelect
|
||||||
|
name={column.key}
|
||||||
|
items={items}
|
||||||
|
fill
|
||||||
|
popoverProps={{ minimal: false }}
|
||||||
|
minimal={false}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
));
|
||||||
|
return <>{columns}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ImportFileMappingFloatingActions() {
|
||||||
|
const { isSubmitting } = useFormikContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
|
||||||
|
<Group>
|
||||||
|
<Button type="submit" intent={Intent.PRIMARY} loading={isSubmitting}>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
<Button>Cancel</Button>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { useImportFileMapping } from '@/hooks/query/import';
|
||||||
|
import { Form, Formik, FormikHelpers } from 'formik';
|
||||||
|
import { useImportFileContext } from './ImportFileProvider';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
|
const validationSchema = null;
|
||||||
|
|
||||||
|
interface ImportFileMappingFormProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImportFileMappingFormValues = Record<string, string | null>;
|
||||||
|
|
||||||
|
export function ImportFileMappingForm({
|
||||||
|
children,
|
||||||
|
}: ImportFileMappingFormProps) {
|
||||||
|
const { mutateAsync: submitImportFileMapping } = useImportFileMapping();
|
||||||
|
const { importId, setStep } = useImportFileContext();
|
||||||
|
|
||||||
|
const initialValues = useImportFileMappingInitialValues();
|
||||||
|
|
||||||
|
const handleSubmit = (
|
||||||
|
values: ImportFileMappingFormValues,
|
||||||
|
{ setSubmitting }: FormikHelpers<ImportFileMappingFormValues>,
|
||||||
|
) => {
|
||||||
|
setSubmitting(true);
|
||||||
|
const _values = transformValueToReq(values);
|
||||||
|
|
||||||
|
submitImportFileMapping([importId, _values])
|
||||||
|
.then(() => {
|
||||||
|
setSubmitting(false);
|
||||||
|
setStep(2);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setSubmitting(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={initialValues}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
// validationSchema={validationSchema}
|
||||||
|
>
|
||||||
|
<Form>{children}</Form>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformValueToReq = (value: ImportFileMappingFormValues) => {
|
||||||
|
const mapping = Object.keys(value)
|
||||||
|
.filter((key) => !isEmpty(value[key]))
|
||||||
|
.map((key) => ({ from: value[key], to: key }));
|
||||||
|
return { mapping };
|
||||||
|
};
|
||||||
|
|
||||||
|
const useImportFileMappingInitialValues = () => {
|
||||||
|
const { entityColumns } = useImportFileContext();
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
entityColumns.reduce((acc, { key, name }) => {
|
||||||
|
acc[key] = '';
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
[entityColumns],
|
||||||
|
);
|
||||||
|
};
|
||||||
121
packages/webapp/src/containers/Import/ImportFilePreview.tsx
Normal file
121
packages/webapp/src/containers/Import/ImportFilePreview.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { Button, Callout, Intent, Text } from '@blueprintjs/core';
|
||||||
|
import clsx from 'classnames';
|
||||||
|
import {
|
||||||
|
ImportFilePreviewBootProvider,
|
||||||
|
useImportFilePreviewBootContext,
|
||||||
|
} from './ImportFilePreviewBoot';
|
||||||
|
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';
|
||||||
|
|
||||||
|
export function ImportFilePreview() {
|
||||||
|
const { importId } = useImportFileContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ImportFilePreviewBootProvider importId={importId}>
|
||||||
|
<ImportFilePreviewContent />
|
||||||
|
</ImportFilePreviewBootProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ImportFilePreviewContent() {
|
||||||
|
const { importPreview } = useImportFilePreviewBootContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Callout>
|
||||||
|
{importPreview.createdCount} of {importPreview.totalCount} Items in your
|
||||||
|
file are ready to be imported.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Text>
|
||||||
|
Items that are ready to be imported - {importPreview.createdCount}
|
||||||
|
</Text>
|
||||||
|
<ul>
|
||||||
|
<li>Items to be created: ({importPreview.createdCount})</li>
|
||||||
|
<li>Items to be skipped: ({importPreview.skippedCount})</li>
|
||||||
|
<li>Items have errors: ({importPreview.errorsCount})</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<table className={clsx('bp4-html-table')}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className={'number'}>#</th>
|
||||||
|
<th className={'name'}>Name</th>
|
||||||
|
<th>Error</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{importPreview?.errors.map((error, key) => (
|
||||||
|
<tr key={key}>
|
||||||
|
<td>{error.rowNumber}</td>
|
||||||
|
<td>{error.rowNumber}</td>
|
||||||
|
<td>
|
||||||
|
{error.errorMessage.map((message) => (
|
||||||
|
<div>{message}</div>
|
||||||
|
))}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<Text>
|
||||||
|
Unmapped Sheet Columns - ({importPreview?.unmappedColumnsCount})
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{importPreview.unmappedColumns?.map((column, key) => (
|
||||||
|
<li key={key}>{column}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<ImportFilePreviewFloatingActions />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ImportFilePreviewFloatingActions() {
|
||||||
|
const { importId } = useImportFileContext();
|
||||||
|
const { importPreview } = useImportFilePreviewBootContext();
|
||||||
|
const { mutateAsync: importFile, isLoading: isImportFileLoading } =
|
||||||
|
useImportFileProcess();
|
||||||
|
|
||||||
|
const history = useHistory();
|
||||||
|
const isValidToImport = importPreview?.createdCount > 0;
|
||||||
|
|
||||||
|
const handleSubmitBtn = () => {
|
||||||
|
importFile(importId)
|
||||||
|
.then(() => {
|
||||||
|
AppToaster.show({
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
message: `The ${
|
||||||
|
importPreview.createdCount
|
||||||
|
} of ${10} has imported successfully.`,
|
||||||
|
});
|
||||||
|
history.push('/accounts');
|
||||||
|
})
|
||||||
|
.catch((error) => {});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className={clsx(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
|
||||||
|
<Group>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
loading={isImportFileLoading}
|
||||||
|
onClick={handleSubmitBtn}
|
||||||
|
disabled={!isValidToImport}
|
||||||
|
>
|
||||||
|
Import
|
||||||
|
</Button>
|
||||||
|
<Button>Cancel</Button>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { useImportFilePreview } from '@/hooks/query/import';
|
||||||
|
import { transformToCamelCase } from '@/utils';
|
||||||
|
import React, { createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
interface ImportFilePreviewBootContextValue {}
|
||||||
|
|
||||||
|
const ImportFilePreviewBootContext =
|
||||||
|
createContext<ImportFilePreviewBootContextValue>(
|
||||||
|
{} as ImportFilePreviewBootContextValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useImportFilePreviewBootContext = () => {
|
||||||
|
const context = useContext<ImportFilePreviewBootContextValue>(
|
||||||
|
ImportFilePreviewBootContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useImportFilePreviewBootContext must be used within an ImportFilePreviewBootProvider',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ImportFilePreviewBootProps {
|
||||||
|
importId: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ImportFilePreviewBootProvider = ({
|
||||||
|
importId,
|
||||||
|
children,
|
||||||
|
}: ImportFilePreviewBootProps) => {
|
||||||
|
const {
|
||||||
|
data: importPreview,
|
||||||
|
isLoading: isImportPreviewLoading,
|
||||||
|
isFetching: isImportPreviewFetching,
|
||||||
|
} = useImportFilePreview(importId, {
|
||||||
|
enabled: Boolean(importId),
|
||||||
|
});
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
importPreview,
|
||||||
|
isImportPreviewLoading,
|
||||||
|
isImportPreviewFetching,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<ImportFilePreviewBootContext.Provider value={value}>
|
||||||
|
{isImportPreviewLoading ? 'loading' : <>{children}</>}
|
||||||
|
</ImportFilePreviewBootContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
87
packages/webapp/src/containers/Import/ImportFileProvider.tsx
Normal file
87
packages/webapp/src/containers/Import/ImportFileProvider.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React, {
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
type EntityColumn = { key: string; name: string };
|
||||||
|
type SheetColumn = string;
|
||||||
|
type SheetMap = { from: string; to: string };
|
||||||
|
|
||||||
|
interface ImportFileContextValue {
|
||||||
|
sheetColumns: SheetColumn[];
|
||||||
|
setSheetColumns: Dispatch<SetStateAction<SheetColumn[]>>;
|
||||||
|
|
||||||
|
entityColumns: EntityColumn[];
|
||||||
|
setEntityColumns: Dispatch<SetStateAction<EntityColumn[]>>;
|
||||||
|
|
||||||
|
sheetMapping: SheetMap[];
|
||||||
|
setSheetMapping: Dispatch<SetStateAction<SheetMap[]>>;
|
||||||
|
|
||||||
|
step: number;
|
||||||
|
setStep: Dispatch<SetStateAction<number>>;
|
||||||
|
|
||||||
|
importId: string;
|
||||||
|
setImportId: Dispatch<SetStateAction<string>>;
|
||||||
|
|
||||||
|
resource: string;
|
||||||
|
}
|
||||||
|
interface ImportFileProviderProps {
|
||||||
|
resource: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImportFileContext = createContext<ImportFileContextValue>(
|
||||||
|
{} as ImportFileContextValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useImportFileContext = () => {
|
||||||
|
const context = useContext<ImportFileContextValue>(ImportFileContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useImportFileContext must be used within an ImportFileProvider',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ImportFileProvider = ({
|
||||||
|
resource,
|
||||||
|
children,
|
||||||
|
}: ImportFileProviderProps) => {
|
||||||
|
const [sheetColumns, setSheetColumns] = useState<SheetColumn[]>([]);
|
||||||
|
const [entityColumns, setEntityColumns] = useState<SheetColumn[]>([]);
|
||||||
|
const [sheetMapping, setSheetMapping] = useState<SheetMap[]>([]);
|
||||||
|
const [importId, setImportId] = useState<string>('');
|
||||||
|
|
||||||
|
const [step, setStep] = useState<number>(0);
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
sheetColumns,
|
||||||
|
setSheetColumns,
|
||||||
|
|
||||||
|
entityColumns,
|
||||||
|
setEntityColumns,
|
||||||
|
|
||||||
|
sheetMapping,
|
||||||
|
setSheetMapping,
|
||||||
|
|
||||||
|
step,
|
||||||
|
setStep,
|
||||||
|
|
||||||
|
importId,
|
||||||
|
setImportId,
|
||||||
|
|
||||||
|
resource,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ImportFileContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</ImportFileContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { AppToaster } from '@/components';
|
||||||
|
import { useImportFileUpload } from '@/hooks/query/import';
|
||||||
|
import { Intent } from '@blueprintjs/core';
|
||||||
|
import { Formik, Form, FormikHelpers } from 'formik';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import { useImportFileContext } from './ImportFileProvider';
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
file: null,
|
||||||
|
} as ImportFileUploadValues;
|
||||||
|
|
||||||
|
interface ImportFileUploadFormProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationSchema = Yup.object().shape({
|
||||||
|
file: Yup.mixed().required('File is required'),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface ImportFileUploadValues {
|
||||||
|
file: File | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImportFileUploadForm({ children }: ImportFileUploadFormProps) {
|
||||||
|
const { mutateAsync: uploadImportFile } = useImportFileUpload();
|
||||||
|
const { setStep, setSheetColumns, setEntityColumns, setImportId } = useImportFileContext();
|
||||||
|
|
||||||
|
const handleSubmit = (
|
||||||
|
values: ImportFileUploadValues,
|
||||||
|
{ setSubmitting }: FormikHelpers<ImportFileUploadValues>,
|
||||||
|
) => {
|
||||||
|
if (!values.file) return;
|
||||||
|
|
||||||
|
setSubmitting(true);
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', values.file);
|
||||||
|
formData.append('resource', 'Account');
|
||||||
|
|
||||||
|
uploadImportFile(formData)
|
||||||
|
.then(({ data }) => {
|
||||||
|
setImportId(data.import.import_id);
|
||||||
|
setSheetColumns(data.sheet_columns);
|
||||||
|
setEntityColumns(data.resource_columns);
|
||||||
|
setStep(1);
|
||||||
|
setSubmitting(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setSubmitting(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={initialValues}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
>
|
||||||
|
<Form>{children}</Form>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.root {
|
||||||
|
margin-top: 2.2rem
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import clsx from 'classnames';
|
||||||
|
import { Group, 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';
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
packages/webapp/src/containers/Import/ImportPage.module.scss
Normal file
11
packages/webapp/src/containers/Import/ImportPage.module.scss
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.root{
|
||||||
|
padding: 32px 40px;
|
||||||
|
min-width: 700px;
|
||||||
|
max-width: 800px;
|
||||||
|
width: 75%;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
.rootWrap {
|
||||||
|
max-width: 1800px;
|
||||||
|
}
|
||||||
19
packages/webapp/src/containers/Import/ImportPage.tsx
Normal file
19
packages/webapp/src/containers/Import/ImportPage.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { ImportStepper } from './ImportStepper';
|
||||||
|
import { Box, DashboardInsider } from '@/components';
|
||||||
|
import styles from './ImportPage.module.scss';
|
||||||
|
import { ImportFileProvider } from './ImportFileProvider';
|
||||||
|
|
||||||
|
export default function ImportPage() {
|
||||||
|
return (
|
||||||
|
<DashboardInsider>
|
||||||
|
<Box className={styles.rootWrap}>
|
||||||
|
<Box className={styles.root}>
|
||||||
|
<ImportFileProvider resource="account">
|
||||||
|
<ImportStepper />
|
||||||
|
</ImportFileProvider>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</DashboardInsider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
|
||||||
|
.root{
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #D3D8DE;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.description{
|
||||||
|
margin: 0;
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #8F99A8;
|
||||||
|
}
|
||||||
|
.title{
|
||||||
|
color: #5F6B7C;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonWrap{
|
||||||
|
flex: 25% 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { Box, Group } from '@/components';
|
||||||
|
import { Button } from '@blueprintjs/core';
|
||||||
|
import styles from './ImportSampleDownload.module.scss';
|
||||||
|
|
||||||
|
export function ImportSampleDownload() {
|
||||||
|
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>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className={styles.buttonWrap}>
|
||||||
|
<Button minimal outlined>
|
||||||
|
Download File
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.content {
|
||||||
|
margin-top: 2.4rem;
|
||||||
|
}
|
||||||
28
packages/webapp/src/containers/Import/ImportStepper.tsx
Normal file
28
packages/webapp/src/containers/Import/ImportStepper.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
export function ImportStepper() {
|
||||||
|
const { step } = useImportFileContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stepper active={step} classNames={{ content: styles.content }}>
|
||||||
|
<Stepper.Step label={'File Upload'}>
|
||||||
|
<ImportFileUploadStep />
|
||||||
|
</Stepper.Step>
|
||||||
|
|
||||||
|
<Stepper.Step label={'Mapping'}>
|
||||||
|
<ImportFileMapping />
|
||||||
|
</Stepper.Step>
|
||||||
|
|
||||||
|
<Stepper.Step label={'Results'}>
|
||||||
|
<ImportFilePreview />
|
||||||
|
</Stepper.Step>
|
||||||
|
</Stepper>
|
||||||
|
);
|
||||||
|
}
|
||||||
63
packages/webapp/src/hooks/query/import.ts
Normal file
63
packages/webapp/src/hooks/query/import.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { useMutation, useQuery, useQueryClient } from 'react-query';
|
||||||
|
import useApiRequest from '../useRequest';
|
||||||
|
import { transformToCamelCase } from '@/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export function useImportFileUpload(props = {}) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useMutation((values) => apiRequest.post(`import/file`, values), {
|
||||||
|
onSuccess: (res, id) => {
|
||||||
|
// Invalidate queries.
|
||||||
|
},
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useImportFileMapping(props = {}) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useMutation(
|
||||||
|
([importId, values]) =>
|
||||||
|
apiRequest.post(`import/${importId}/mapping`, values),
|
||||||
|
{
|
||||||
|
onSuccess: (res, id) => {
|
||||||
|
// Invalidate queries.
|
||||||
|
},
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function useImportFilePreview(importId: string, props = {}) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useQuery(['importPreview', importId], () =>
|
||||||
|
apiRequest
|
||||||
|
.get(`import/${importId}/preview`)
|
||||||
|
.then((res) => transformToCamelCase(res.data)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export function useImportFileProcess(props = {}) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useMutation(
|
||||||
|
(importId) => apiRequest.post(`import/${importId}/import`),
|
||||||
|
{
|
||||||
|
onSuccess: (res, id) => {
|
||||||
|
// Invalidate queries.
|
||||||
|
},
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,6 +10,12 @@ const SUBSCRIPTION_TYPE = {
|
|||||||
|
|
||||||
export const getDashboardRoutes = () => [
|
export const getDashboardRoutes = () => [
|
||||||
// Accounts.
|
// Accounts.
|
||||||
|
{
|
||||||
|
path: '/accounts/import',
|
||||||
|
component: lazy(() => import('@/containers/Import/ImportPage')),
|
||||||
|
breadcrumb: 'Import Accounts',
|
||||||
|
pageTitle: 'Import Accounts',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: `/accounts`,
|
path: `/accounts`,
|
||||||
component: lazy(() => import('@/containers/Accounts/AccountsChart')),
|
component: lazy(() => import('@/containers/Accounts/AccountsChart')),
|
||||||
@@ -19,7 +25,6 @@ export const getDashboardRoutes = () => [
|
|||||||
defaultSearchResource: RESOURCES_TYPES.ACCOUNT,
|
defaultSearchResource: RESOURCES_TYPES.ACCOUNT,
|
||||||
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Accounting.
|
// Accounting.
|
||||||
{
|
{
|
||||||
path: `/make-journal-entry`,
|
path: `/make-journal-entry`,
|
||||||
@@ -1062,8 +1067,7 @@ export const getDashboardRoutes = () => [
|
|||||||
{
|
{
|
||||||
path: '/tax-rates',
|
path: '/tax-rates',
|
||||||
component: lazy(
|
component: lazy(
|
||||||
() =>
|
() => import('@/containers/TaxRates/pages/TaxRatesLanding'),
|
||||||
import('@/containers/TaxRates/pages/TaxRatesLanding'),
|
|
||||||
),
|
),
|
||||||
pageTitle: 'Tax Rates',
|
pageTitle: 'Tax Rates',
|
||||||
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
||||||
|
|||||||
@@ -577,4 +577,16 @@ export default {
|
|||||||
],
|
],
|
||||||
viewBox: '0 0 20 20',
|
viewBox: '0 0 20 20',
|
||||||
},
|
},
|
||||||
|
done: {
|
||||||
|
path: [
|
||||||
|
'M395-285 226-455l50-50 119 118 289-288 50 51-339 339Z',
|
||||||
|
],
|
||||||
|
viewBox: '0 -960 960 960'
|
||||||
|
},
|
||||||
|
download: {
|
||||||
|
path: [
|
||||||
|
'M480-336 288-528l51-51 105 105v-342h72v342l105-105 51 51-192 192ZM263.717-192Q234-192 213-213.15T192-264v-72h72v72h432v-72h72v72q0 29.7-21.162 50.85Q725.676-192 695.96-192H263.717Z'
|
||||||
|
],
|
||||||
|
viewBox: '0 -960 960 960'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
.cloud-spinner{
|
.cloud-spinner{
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&.is-loading:before{
|
&.is-loading:before{
|
||||||
content: "";
|
content: "";
|
||||||
|
|||||||
17
packages/webapp/src/utils/is-element.ts
Normal file
17
packages/webapp/src/utils/is-element.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export function isElement(value: any): value is React.ReactElement {
|
||||||
|
if (Array.isArray(value) || value === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
if (value.type === React.Fragment) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
13
pnpm-lock.yaml
generated
13
pnpm-lock.yaml
generated
@@ -660,6 +660,9 @@ importers:
|
|||||||
react-dropzone:
|
react-dropzone:
|
||||||
specifier: ^11.0.1
|
specifier: ^11.0.1
|
||||||
version: 11.7.1(react@18.2.0)
|
version: 11.7.1(react@18.2.0)
|
||||||
|
react-dropzone-esm:
|
||||||
|
specifier: ^15.0.1
|
||||||
|
version: 15.0.1(react@18.2.0)
|
||||||
react-error-boundary:
|
react-error-boundary:
|
||||||
specifier: ^3.0.2
|
specifier: ^3.0.2
|
||||||
version: 3.1.4(react@18.2.0)
|
version: 3.1.4(react@18.2.0)
|
||||||
@@ -21180,6 +21183,16 @@ packages:
|
|||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-dropzone-esm@15.0.1(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-RdeGpqwHnoV/IlDFpQji7t7pTtlC2O1i/Br0LWkRZ9hYtLyce814S71h5NolnCZXsIN5wrZId6+8eQj2EBnEzg==}
|
||||||
|
engines: {node: '>= 10.13'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>= 16.8 || 18.0.0'
|
||||||
|
dependencies:
|
||||||
|
prop-types: 15.8.1
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-dropzone@11.7.1(react@18.2.0):
|
/react-dropzone@11.7.1(react@18.2.0):
|
||||||
resolution: {integrity: sha512-zxCMwhfPy1olUEbw3FLNPLhAm/HnaYH5aELIEglRbqabizKAdHs0h+WuyOpmA+v1JXn0++fpQDdNfUagWt5hJQ==}
|
resolution: {integrity: sha512-zxCMwhfPy1olUEbw3FLNPLhAm/HnaYH5aELIEglRbqabizKAdHs0h+WuyOpmA+v1JXn0++fpQDdNfUagWt5hJQ==}
|
||||||
engines: {node: '>= 10.13'}
|
engines: {node: '>= 10.13'}
|
||||||
|
|||||||
Reference in New Issue
Block a user