feat: element customize component

This commit is contained in:
Ahmed Bouhuolia
2024-09-09 21:07:22 +02:00
parent dc18bde6be
commit f644ed6708
25 changed files with 319 additions and 292 deletions

View File

@@ -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;
}

View File

@@ -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>;
};

View File

@@ -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,
);

View File

@@ -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;
}

View File

@@ -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>
);
}

View File

@@ -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,
);

View File

@@ -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>
);
}

View File

@@ -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;
};

View File

@@ -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;
}
}

View File

@@ -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>
);
}

View File

@@ -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>
);
};

View File

@@ -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>
);
}