mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 05:40:31 +00:00
feat: element customize component
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
.root {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.mainFields{
|
||||
width: 400px;
|
||||
height: 100vh;
|
||||
}
|
||||
.fieldGroup {
|
||||
|
||||
:global .bp4-form-content{
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.footerActions{
|
||||
padding: 10px 16px;
|
||||
border-top: 1px solid #d9d9d9;
|
||||
flex-flow: row-reverse;
|
||||
}
|
||||
|
||||
.showCompanyLogoField:global(.bp4-large){
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import { Box, Group } from '@/components';
|
||||
import { ElementCustomizeProvider } from './ElementCustomizeProvider';
|
||||
import {
|
||||
ElementCustomizeForm,
|
||||
ElementCustomizeFormProps,
|
||||
} from './ElementCustomizerForm';
|
||||
import { ElementCustomizeTabsControllerProvider } from './ElementCustomizeTabsController';
|
||||
import { ElementCustomizeFields } from './ElementCustomizeFields';
|
||||
import { ElementCustomizePreview } from './ElementCustomizePreview';
|
||||
import { extractChildren } from '@/utils/extract-children';
|
||||
|
||||
export interface ElementCustomizeProps<T> extends ElementCustomizeFormProps<T> {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ElementCustomize<T>({
|
||||
initialValues,
|
||||
validationSchema,
|
||||
onSubmit,
|
||||
children,
|
||||
}: ElementCustomizeProps<T>) {
|
||||
const PaperTemplate = React.useMemo(
|
||||
() => extractChildren(children, ElementCustomize.PaperTemplate),
|
||||
[children],
|
||||
);
|
||||
const CustomizeTabs = React.useMemo(
|
||||
() => extractChildren(children, ElementCustomize.FieldsTab),
|
||||
[children],
|
||||
);
|
||||
|
||||
const value = { PaperTemplate, CustomizeTabs };
|
||||
|
||||
return (
|
||||
<ElementCustomizeForm
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<ElementCustomizeTabsControllerProvider>
|
||||
<ElementCustomizeProvider value={value}>
|
||||
<Group spacing={0} align="stretch">
|
||||
<ElementCustomizeFields />
|
||||
<ElementCustomizePreview />
|
||||
</Group>
|
||||
</ElementCustomizeProvider>
|
||||
</ElementCustomizeTabsControllerProvider>
|
||||
</ElementCustomizeForm>
|
||||
);
|
||||
}
|
||||
|
||||
export interface ElementCustomizePaperTemplateProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
ElementCustomize.PaperTemplate = ({
|
||||
children,
|
||||
}: ElementCustomizePaperTemplateProps) => {
|
||||
return <Box>{children}</Box>;
|
||||
};
|
||||
|
||||
export interface ElementCustomizeContentProps {
|
||||
id: string;
|
||||
label: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
ElementCustomize.FieldsTab = ({
|
||||
id,
|
||||
label,
|
||||
children,
|
||||
}: ElementCustomizeContentProps) => {
|
||||
return <Box>{children}</Box>;
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import * as R from 'ramda';
|
||||
import { Button, Intent } from '@blueprintjs/core';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { Group, Stack } from '@/components';
|
||||
import { ElementCustomizeHeader } from './ElementCustomizeHeader';
|
||||
import { ElementCustomizeTabs } from './ElementCustomizeTabs';
|
||||
import { useElementCustomizeTabsController } from './ElementCustomizeTabsController';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
import { useElementCustomizeContext } from './ElementCustomizeProvider';
|
||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||
import styles from './ElementCustomize.module.scss';
|
||||
|
||||
export function ElementCustomizeFields() {
|
||||
return (
|
||||
<Group spacing={0} align={'stretch'} className={styles.root}>
|
||||
<ElementCustomizeTabs />
|
||||
<ElementCustomizeFieldsMain />
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export function ElementCustomizeFieldsMain() {
|
||||
const { currentTabId } = useElementCustomizeTabsController();
|
||||
const { CustomizeTabs } = useElementCustomizeContext();
|
||||
|
||||
const CustomizeTabPanel = React.useMemo(
|
||||
() =>
|
||||
React.Children.map(CustomizeTabs, (tab) => {
|
||||
return tab.props.id === currentTabId ? tab : null;
|
||||
}).filter(Boolean),
|
||||
[CustomizeTabs, currentTabId],
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack spacing={0} className={styles.mainFields}>
|
||||
<ElementCustomizeHeader label={'Customize'} />
|
||||
|
||||
<Stack spacing={0} style={{ flex: '1 1 auto', overflow: 'auto' }}>
|
||||
{CustomizeTabPanel}
|
||||
<ElementCustomizeFooterActions />
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function ElementCustomizeFooterActionsRoot({ closeDrawer }) {
|
||||
const { name } = useDrawerContext();
|
||||
const { submitForm } = useFormikContext();
|
||||
|
||||
const handleSubmitBtnClick = () => {
|
||||
submitForm();
|
||||
};
|
||||
const handleCancelBtnClick = () => {
|
||||
closeDrawer(name);
|
||||
};
|
||||
|
||||
return (
|
||||
<Group spacing={10} className={styles.footerActions}>
|
||||
<Button
|
||||
onClick={handleSubmitBtnClick}
|
||||
intent={Intent.PRIMARY}
|
||||
style={{ minWidth: 75 }}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button onClick={handleCancelBtnClick}>Cancel</Button>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
const ElementCustomizeFooterActions = R.compose(withDrawerActions)(
|
||||
ElementCustomizeFooterActionsRoot,
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
.root {
|
||||
align-items: center;
|
||||
border-radius: 0;
|
||||
box-shadow: 0 1px 0 rgba(17, 20, 24, .15);
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
min-height: 55px;
|
||||
padding: 5px 5px 5px 20px;
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.title{
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Button, Classes } from '@blueprintjs/core';
|
||||
import { Group, Icon } from '@/components';
|
||||
import styles from './ElementCustomizeHeader.module.scss';
|
||||
|
||||
interface ElementCustomizeHeaderProps {
|
||||
label?: string;
|
||||
children?: React.ReactNode;
|
||||
closeButton?: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function ElementCustomizeHeader({
|
||||
label,
|
||||
closeButton,
|
||||
onClose,
|
||||
children,
|
||||
}: ElementCustomizeHeaderProps) {
|
||||
const handleClose = () => {
|
||||
onClose && onClose();
|
||||
};
|
||||
return (
|
||||
<Group className={styles.root}>
|
||||
{label && <h1 className={styles.title}>{label}</h1>}
|
||||
{closeButton && (
|
||||
<Button
|
||||
aria-label="Close"
|
||||
className={Classes.DIALOG_CLOSE_BUTTON}
|
||||
icon={<Icon icon={'smallCross'} color={'#000'} />}
|
||||
minimal={true}
|
||||
onClick={handleClose}
|
||||
style={{ marginLeft: 'auto' }}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { Stack } from '@/components';
|
||||
import { ElementCustomizeHeader } from './ElementCustomizeHeader';
|
||||
import { ElementCustomizePreviewContent } from './ElementCustomizePreviewContent';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||
|
||||
function ElementCustomizePreviewRoot({ closeDrawer }) {
|
||||
const { name } = useDrawerContext();
|
||||
|
||||
const handleCloseBtnClick = () => {
|
||||
closeDrawer(name);
|
||||
};
|
||||
return (
|
||||
<Stack
|
||||
spacing={0}
|
||||
style={{ borderLeft: '1px solid #D9D9D9', height: '100vh', flex: '1 1' }}
|
||||
>
|
||||
<ElementCustomizeHeader
|
||||
label={'Preview'}
|
||||
closeButton
|
||||
onClose={handleCloseBtnClick}
|
||||
/>
|
||||
<ElementCustomizePreviewContent />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export const ElementCustomizePreview = R.compose(withDrawerActions)(
|
||||
ElementCustomizePreviewRoot,
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Box } from '@/components';
|
||||
import { useElementCustomizeContext } from './ElementCustomizeProvider';
|
||||
|
||||
export function ElementCustomizePreviewContent() {
|
||||
const { PaperTemplate } = useElementCustomizeContext();
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
padding: '28px 24px 40px',
|
||||
backgroundColor: '#F5F5F5',
|
||||
overflow: 'auto',
|
||||
flex: '1',
|
||||
}}
|
||||
>
|
||||
{PaperTemplate}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import React, { createContext, useContext } from 'react';
|
||||
|
||||
interface ElementCustomizeValue {
|
||||
PaperTemplate?: React.ReactNode;
|
||||
CustomizeTabs: React.ReactNode;
|
||||
}
|
||||
|
||||
const ElementCustomizeContext = createContext<ElementCustomizeValue>(
|
||||
{} as ElementCustomizeValue,
|
||||
);
|
||||
|
||||
export const ElementCustomizeProvider: React.FC<{
|
||||
value: ElementCustomizeValue;
|
||||
children: React.ReactNode;
|
||||
}> = ({ value, children }) => {
|
||||
return (
|
||||
<ElementCustomizeContext.Provider value={{ ...value }}>
|
||||
{children}
|
||||
</ElementCustomizeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useElementCustomizeContext = (): ElementCustomizeValue => {
|
||||
const context = useContext<ElementCustomizeValue>(ElementCustomizeContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useElementCustomize must be used within an ElementCustomizeProvider',
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
|
||||
.root {
|
||||
flex: 1;
|
||||
min-width: 165px;
|
||||
max-width: 165px;
|
||||
}
|
||||
|
||||
.content{
|
||||
padding: 5px;
|
||||
flex: 1;
|
||||
border-right: 1px solid #E1E1E1;
|
||||
}
|
||||
|
||||
.tabsList{
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
|
||||
:global .bp4-tab-list{
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { Box, Stack } from '@/components';
|
||||
import { Tab, Tabs } from '@blueprintjs/core';
|
||||
import { ElementCustomizeHeader } from './ElementCustomizeHeader';
|
||||
import {
|
||||
ElementCustomizeTabsEnum,
|
||||
useElementCustomizeTabsController,
|
||||
} from './ElementCustomizeTabsController';
|
||||
import { useElementCustomizeContext } from './ElementCustomizeProvider';
|
||||
import styles from './ElementCustomizeTabs.module.scss';
|
||||
|
||||
export function ElementCustomizeTabs() {
|
||||
const { setCurrentTabId } = useElementCustomizeTabsController();
|
||||
|
||||
const { CustomizeTabs } = useElementCustomizeContext();
|
||||
|
||||
const tabItems = React.Children.map(CustomizeTabs, (node) => ({
|
||||
...(React.isValidElement(node) ? node.props : {}),
|
||||
}));
|
||||
const handleChange = (value: ElementCustomizeTabsEnum) => {
|
||||
setCurrentTabId(value);
|
||||
};
|
||||
return (
|
||||
<Stack spacing={0} className={styles.root}>
|
||||
<ElementCustomizeHeader label={''} />
|
||||
|
||||
<Box className={styles.content}>
|
||||
<Tabs
|
||||
vertical
|
||||
fill
|
||||
large
|
||||
onChange={handleChange}
|
||||
className={styles.tabsList}
|
||||
>
|
||||
{tabItems?.map(({ id, label }: { id: string; label: string }) => (
|
||||
<Tab id={id} key={id} title={label} />
|
||||
))}
|
||||
</Tabs>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
|
||||
export enum ElementCustomizeTabsEnum {
|
||||
General = 'general',
|
||||
Items = 'items',
|
||||
Totals = 'totals'
|
||||
}
|
||||
|
||||
const DEFAULT_TAB_ID = ElementCustomizeTabsEnum.General;
|
||||
|
||||
interface ElementCustomizeTabsControllerValue {
|
||||
currentTabId: ElementCustomizeTabsEnum;
|
||||
setCurrentTabId: React.Dispatch<
|
||||
React.SetStateAction<ElementCustomizeTabsEnum>
|
||||
>;
|
||||
}
|
||||
|
||||
const ElementCustomizeTabsController = createContext(
|
||||
{} as ElementCustomizeTabsControllerValue,
|
||||
);
|
||||
|
||||
export const useElementCustomizeTabsController = () => {
|
||||
return useContext(ElementCustomizeTabsController);
|
||||
};
|
||||
|
||||
interface ElementCustomizeTabsControllerProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ElementCustomizeTabsControllerProvider = ({
|
||||
children,
|
||||
}: ElementCustomizeTabsControllerProps) => {
|
||||
const [currentTabId, setCurrentTabId] =
|
||||
useState<ElementCustomizeTabsEnum>(DEFAULT_TAB_ID);
|
||||
|
||||
const value = {
|
||||
currentTabId,
|
||||
setCurrentTabId,
|
||||
};
|
||||
|
||||
return (
|
||||
<ElementCustomizeTabsController.Provider value={value}>
|
||||
{children}
|
||||
</ElementCustomizeTabsController.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Formik, Form, FormikHelpers } from 'formik';
|
||||
|
||||
export interface ElementCustomizeFormProps<T> {
|
||||
initialValues?: T;
|
||||
validationSchema?: any;
|
||||
onSubmit?: (values: T, formikHelpers: FormikHelpers<T>) => void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ElementCustomizeForm<T>({
|
||||
initialValues,
|
||||
validationSchema,
|
||||
onSubmit,
|
||||
children,
|
||||
}: ElementCustomizeFormProps<T>) {
|
||||
return (
|
||||
<Formik<T>
|
||||
initialValues={{ ...initialValues }}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={(value, helpers) => onSubmit && onSubmit(value, helpers)}
|
||||
>
|
||||
<Form>{children}</Form>
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user