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

@@ -1,15 +0,0 @@
.field{
height: 28px;
line-height: 28px;
border-radius: 5px;
}
.colorPicker{
background-color: rgb(103, 114, 229);
border-radius: 3px;
height: 16px;
width: 16px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
cursor: pointer;
}

View File

@@ -1,80 +0,0 @@
import { useState } from 'react';
import clsx from 'classnames';
import {
IInputGroupProps,
InputGroup,
IPopoverProps,
Popover,
PopoverInteractionKind,
Position,
} from '@blueprintjs/core';
import { HexColorPicker } from 'react-colorful';
import { useUncontrolled } from '@/hooks/useUncontrolled';
import { Box, BoxProps } from '@/components';
import styles from './ColorField.module.scss';
export interface ColorFieldProps {
value?: string;
initialValue?: string;
onChange?: (value: string) => void;
popoverProps?: Partial<IPopoverProps>;
inputProps?: Partial<IInputGroupProps>;
pickerProps?: Partial<BoxProps>;
pickerWrapProps?: Partial<BoxProps>;
}
export function ColorField({
value,
initialValue,
onChange,
popoverProps,
inputProps,
pickerWrapProps,
pickerProps,
}: ColorFieldProps) {
const [_value, handleChange] = useUncontrolled({
value,
initialValue,
onChange,
finalValue: '',
});
const [isOpen, setIsOpen] = useState<boolean>(false);
const handleClose = () => {
setIsOpen(false);
};
return (
<Popover
content={<HexColorPicker color={_value} onChange={handleChange} />}
position={Position.BOTTOM}
interactionKind={PopoverInteractionKind.CLICK}
modifiers={{
offset: { offset: '0, 4' },
}}
onClose={handleClose}
isOpen={isOpen}
minimal
{...popoverProps}
>
<InputGroup
value={_value}
leftElement={
<Box
{...pickerWrapProps}
style={{ padding: 8, ...pickerWrapProps?.style }}
>
<Box
onClick={() => setIsOpen((oldValue) => !oldValue)}
style={{ backgroundColor: _value }}
className={clsx(styles.colorPicker, pickerProps?.className)}
{...pickerProps}
/>
</Box>
}
{...inputProps}
className={clsx(styles.field, inputProps?.className)}
/>
</Popover>
);
}

View File

@@ -1,32 +0,0 @@
import { SVGProps } from 'react';
interface CreditCardIconProps extends SVGProps<SVGSVGElement> {
}
export function CreditCardIcon(props: CreditCardIconProps) {
return (
<svg
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 16 16"
enable-background="new 0 0 16 16"
{...props}
>
<g id="credit_card_2_">
<g>
<path
d="M14.99,2.95h-14c-0.55,0-1,0.45-1,1v1h16v-1C15.99,3.4,15.54,2.95,14.99,2.95z M-0.01,12.95c0,0.55,0.45,1,1,1h14
c0.55,0,1-0.45,1-1v-6h-16C-0.01,6.95-0.01,12.95-0.01,12.95z M5.49,10.95h5c0.28,0,0.5,0.22,0.5,0.5s-0.22,0.5-0.5,0.5h-5
c-0.28,0-0.5-0.22-0.5-0.5S5.22,10.95,5.49,10.95z M2.49,10.95h1c0.28,0,0.5,0.22,0.5,0.5s-0.22,0.5-0.5,0.5h-1
c-0.28,0-0.5-0.22-0.5-0.5S2.22,10.95,2.49,10.95z"
/>
</g>
</g>
</svg>
);
}

View File

@@ -1,64 +0,0 @@
import React from 'react';
import { getIn, FieldConfig, FieldProps } from 'formik';
import { Intent } from '@blueprintjs/core';
import { Field } from '@blueprintjs-formik/core';
import { ColorField, ColorFieldProps } from './ColorField';
interface ColorFieldInputGroupProps
extends Omit<FieldConfig, 'children' | 'component' | 'as' | 'value'>,
ColorFieldProps {}
export interface ColorFieldToInputProps
extends Omit<FieldProps, 'onChange'>,
ColorFieldProps {}
/**
* Transforms field props to input group props for ColorField.
* @param {ColorFieldToInputProps}
* @returns {ColorFieldProps}
*/
function fieldToColorFieldInputGroup({
field: { onBlur: onFieldBlur, onChange: onFieldChange, value, ...field },
form: { touched, errors, setFieldValue },
onChange,
...props
}: ColorFieldToInputProps): ColorFieldProps {
const fieldError = getIn(errors, field.name);
const showError = getIn(touched, field.name) && !!fieldError;
return {
inputProps: {
intent: showError ? Intent.DANGER : Intent.NONE,
},
value,
onChange:
onChange ??
function (value: string) {
setFieldValue(field.name, value);
},
...field,
...props,
};
}
/**
* Transforms field props to input group props for ColorField.
* @param {ColorFieldToInputProps} props -
* @returns {JSX.Element}
*/
function ColorFieldToInputGroup({
...props
}: ColorFieldToInputProps): JSX.Element {
return <ColorField {...fieldToColorFieldInputGroup(props)} />;
}
/**
* Input group Blueprint component binded with Formik for ColorField.
* @param {ColorFieldInputGroupProps}
* @returns {JSX.Element}
*/
export function FColorInput({
...props
}: ColorFieldInputGroupProps): JSX.Element {
return <Field {...props} component={ColorFieldToInputGroup} />;
}

View File

@@ -1,74 +1,65 @@
import React from 'react';
import { Box, Group } from '@/components';
import { InvoiceCustomizeProvider } from './InvoiceCustomizeProvider';
import {
InvoiceCustomizeForm,
InvoiceCustomizeFormProps,
} from './InvoiceCustomizerForm';
import { InvoiceCustomizeTabsControllerProvider } from './InvoiceCustomizeTabsController';
import { InvoiceCustomizeFields } from './InvoiceCustomizeFields';
import { InvoiceCustomizePreview } from './InvoiceCustomizePreview';
import { extractChildren } from '@/utils/extract-children';
import { Box } from '@/components';
import { Classes } from '@blueprintjs/core';
import { InvoicePaperTemplate } from './InvoicePaperTemplate';
import { ElementCustomize } from '../../../ElementCustomize/ElementCustomize';
import { InvoiceCustomizeGeneralField } from './InvoiceCustomizeGeneralFields';
import { InvoiceCustomizeContentFields } from './InvoiceCutomizeContentFields';
export interface InvoiceCustomizeProps<T> extends InvoiceCustomizeFormProps<T> {
children?: React.ReactNode;
interface InvoiceCustomizeValues {
invoiceNumber?: string;
invoiceNumberLabel?: string;
dateIssue?: string;
dateIssueLabel?: string;
dueDate?: string;
dueDateLabel?: string;
companyName?: string;
bigtitle?: string;
itemRateLabel?: string;
itemQuantityLabel?: string;
itemTotalLabel?: string;
// Totals
showDueAmount?: boolean;
showDiscount?: boolean;
showPaymentMade?: boolean;
showTaxes?: boolean;
showSubtotal?: boolean;
showTotal?: boolean;
showBalanceDue?: boolean;
paymentMadeLabel?: string;
discountLabel?: string;
subtotalLabel?: string;
totalLabel?: string;
balanceDueLabel?: string;
}
export function InvoiceCustomize<T>({
initialValues,
validationSchema,
onSubmit,
children,
}: InvoiceCustomizeProps<T>) {
const PaperTemplate = React.useMemo(
() => extractChildren(children, InvoiceCustomize.PaperTemplate),
[children],
);
const CustomizeTabs = React.useMemo(
() => extractChildren(children, InvoiceCustomize.FieldsTab),
[children],
);
const value = { PaperTemplate, CustomizeTabs };
export default function InvoiceCustomizeContent() {
return (
<InvoiceCustomizeForm
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={onSubmit}
>
<InvoiceCustomizeTabsControllerProvider>
<InvoiceCustomizeProvider value={value}>
<Group spacing={0} align="stretch">
<InvoiceCustomizeFields />
<InvoiceCustomizePreview />
</Group>
</InvoiceCustomizeProvider>
</InvoiceCustomizeTabsControllerProvider>
</InvoiceCustomizeForm>
<Box className={Classes.DRAWER_BODY}>
<ElementCustomize<InvoiceCustomizeValues>>
<ElementCustomize.PaperTemplate>
<InvoicePaperTemplate />
</ElementCustomize.PaperTemplate>
<ElementCustomize.FieldsTab id={'general'} label={'General'}>
<InvoiceCustomizeGeneralField />
</ElementCustomize.FieldsTab>
<ElementCustomize.FieldsTab id={'content'} label={'Content'}>
<InvoiceCustomizeContentFields />
</ElementCustomize.FieldsTab>
<ElementCustomize.FieldsTab id={'totals'} label={'Totals'}>
asdfasdfdsaf #3
</ElementCustomize.FieldsTab>
</ElementCustomize>
</Box>
);
}
export interface InvoiceCustomizePaperTemplateProps {
children?: React.ReactNode;
}
InvoiceCustomize.PaperTemplate = ({
children,
}: InvoiceCustomizePaperTemplateProps) => {
return <Box>{children}</Box>;
};
export interface InvoiceCustomizeContentProps {
id: string;
label: string;
children?: React.ReactNode;
}
InvoiceCustomize.FieldsTab = ({
id,
label,
children,
}: InvoiceCustomizeContentProps) => {
return <Box>{children}</Box>;
};

View File

@@ -1,65 +0,0 @@
import React from 'react';
import { Box } from '@/components';
import { Classes } from '@blueprintjs/core';
import { InvoicePaperTemplate } from './InvoicePaperTemplate';
import { InvoiceCustomize } from './InvoiceCustomize';
import { InvoiceCustomizeGeneralField } from './InvoiceCustomizeGeneralFields';
import { InvoiceCustomizeContentFields } from './InvoiceCutomizeContentFields';
interface InvoiceCustomizeValues {
invoiceNumber?: string;
invoiceNumberLabel?: string;
dateIssue?: string;
dateIssueLabel?: string;
dueDate?: string;
dueDateLabel?: string;
companyName?: string;
bigtitle?: string;
itemRateLabel?: string;
itemQuantityLabel?: string;
itemTotalLabel?: string;
// Totals
showDueAmount?: boolean;
showDiscount?: boolean;
showPaymentMade?: boolean;
showTaxes?: boolean;
showSubtotal?: boolean;
showTotal?: boolean;
showBalanceDue?: boolean;
paymentMadeLabel?: string;
discountLabel?: string;
subtotalLabel?: string;
totalLabel?: string;
balanceDueLabel?: string;
}
export default function InvoiceCustomizeContent() {
return (
<Box className={Classes.DRAWER_BODY}>
<InvoiceCustomize<InvoiceCustomizeValues>>
<InvoiceCustomize.PaperTemplate>
<InvoicePaperTemplate />
</InvoiceCustomize.PaperTemplate>
<InvoiceCustomize.FieldsTab id={'general'} label={'General'}>
<InvoiceCustomizeGeneralField />
</InvoiceCustomize.FieldsTab>
<InvoiceCustomize.FieldsTab id={'content'} label={'Content'}>
<InvoiceCustomizeContentFields />
</InvoiceCustomize.FieldsTab>
<InvoiceCustomize.FieldsTab id={'totals'} label={'Totals'}>
asdfasdfdsaf #3
</InvoiceCustomize.FieldsTab>
</InvoiceCustomize>
</Box>
);
}

View File

@@ -4,8 +4,8 @@ import * as R from 'ramda';
import { Drawer, DrawerSuspense } from '@/components';
import withDrawers from '@/containers/Drawer/withDrawers';
const InvoiceCustomizeContent = React.lazy(
() => import('./InvoiceCustomizeContent'),
const InvoiceCustomize = React.lazy(
() => import('./InvoiceCustomize'),
);
/**
@@ -21,7 +21,7 @@ function InvoiceCustomizeDrawerRoot({
return (
<Drawer isOpen={isOpen} name={name} size={'100%'}>
<DrawerSuspense>
<InvoiceCustomizeContent />
<InvoiceCustomize />
</DrawerSuspense>
</Drawer>
);

View File

@@ -1,75 +0,0 @@
// @ts-nocheck
import React from 'react';
import * as R from 'ramda';
import { Button, Intent } from '@blueprintjs/core';
import { Group, Stack } from '@/components';
import { InvoiceCustomizeHeader } from './InvoiceCustomizeHeader';
import { InvoiceCustomizeTabs } from './InvoiceCustomizeTabs';
import { useInvoiceCustomizeTabsController } from './InvoiceCustomizeTabsController';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
import { useFormikContext } from 'formik';
import { useInvoiceCustomizeContext } from './InvoiceCustomizeProvider';
import styles from './InvoiceCustomizeFields.module.scss';
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
export function InvoiceCustomizeFields() {
return (
<Group spacing={0} align={'stretch'} className={styles.root}>
<InvoiceCustomizeTabs />
<InvoiceCustomizeFieldsMain />
</Group>
);
}
export function InvoiceCustomizeFieldsMain() {
const { currentTabId } = useInvoiceCustomizeTabsController();
const { CustomizeTabs } = useInvoiceCustomizeContext();
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}>
<InvoiceCustomizeHeader label={'Customize'} />
<Stack spacing={0} style={{ flex: '1 1 auto', overflow: 'auto' }}>
{CustomizeTabPanel}
<InvoiceCustomizeFooterActions />
</Stack>
</Stack>
);
}
function InvoiceCustomizeFooterActionsRoot({ 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 InvoiceCustomizeFooterActions = R.compose(withDrawerActions)(
InvoiceCustomizeFooterActionsRoot,
);

View File

@@ -1,9 +1,9 @@
// @ts-nocheck
import { Box, FFormGroup, FSwitch, Group, Stack } from '@/components';
import { FColorInput } from './FColorField';
import styles from './InvoiceCustomizeFields.module.scss';
import { Classes, Text } from '@blueprintjs/core';
import { CreditCardIcon } from './CreditCardIcon';
import { FFormGroup, FSwitch, Group, Stack } from '@/components';
import { FColorInput } from '@/components/Forms/FColorInput';
import { CreditCardIcon } from '@/icons/CreditCardIcon';
import styles from './InvoiceCustomizeFields.module.scss';
export function InvoiceCustomizeGeneralField() {
return (

View File

@@ -1,20 +0,0 @@
.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

@@ -1,36 +0,0 @@
import { Group, Icon } from '@/components';
import { Button, Classes } from '@blueprintjs/core';
import styles from './InvoiceCustomizeHeader.module.scss';
interface InvoiceCustomizeHeaderProps {
label?: string;
children?: React.ReactNode;
closeButton?: boolean;
onClose?: () => void;
}
export function InvoiceCustomizeHeader({
label,
closeButton,
onClose,
children,
}: InvoiceCustomizeHeaderProps) {
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

@@ -1,29 +0,0 @@
// @ts-nocheck
import * as R from 'ramda';
import { Stack } from '@/components';
import { InvoiceCustomizeHeader } from './InvoiceCustomizeHeader';
import { InvoiceCustomizePreviewContent } from './InvoiceCustomizePreviewContent';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
function InvoiceCustomizePreviewRoot({ closeDrawer }) {
const { name } = useDrawerContext();
const handleCloseBtnClick = () => {
closeDrawer(name);
};
return (
<Stack spacing={0} style={{ borderLeft: '1px solid #D9D9D9', height: '100vh', flex: '1 1' }}>
<InvoiceCustomizeHeader
label={'Preview'}
closeButton
onClose={handleCloseBtnClick}
/>
<InvoiceCustomizePreviewContent />
</Stack>
);
}
export const InvoiceCustomizePreview = R.compose(withDrawerActions)(
InvoiceCustomizePreviewRoot,
);

View File

@@ -1,19 +0,0 @@
import { Box } from '@/components';
import { useInvoiceCustomizeContext } from './InvoiceCustomizeProvider';
export function InvoiceCustomizePreviewContent() {
const { PaperTemplate } = useInvoiceCustomizeContext();
return (
<Box
style={{
padding: '28px 24px 40px',
backgroundColor: '#F5F5F5',
overflow: 'auto',
flex: '1',
}}
>
{PaperTemplate}
</Box>
);
}

View File

@@ -1,32 +0,0 @@
import React, { createContext, useContext } from 'react';
interface InvoiceCustomizeValue {
PaperTemplate?: React.ReactNode;
CustomizeTabs: React.ReactNode;
}
const InvoiceCustomizeContext = createContext<InvoiceCustomizeValue>(
{} as InvoiceCustomizeValue,
);
export const InvoiceCustomizeProvider: React.FC<{
value: InvoiceCustomizeValue;
children: React.ReactNode;
}> = ({ value, children }) => {
return (
<InvoiceCustomizeContext.Provider value={{ ...value }}>
{children}
</InvoiceCustomizeContext.Provider>
);
};
export const useInvoiceCustomizeContext = (): InvoiceCustomizeValue => {
const context = useContext<InvoiceCustomizeValue>(InvoiceCustomizeContext);
if (!context) {
throw new Error(
'useInvoiceCustomize must be used within an InvoiceCustomizeProvider',
);
}
return context;
};

View File

@@ -1,21 +0,0 @@
.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

@@ -1,42 +0,0 @@
import { Box, Stack } from '@/components';
import { Tab, Tabs } from '@blueprintjs/core';
import { InvoiceCustomizeHeader } from './InvoiceCustomizeHeader';
import {
InvoiceCustomizeTabsEnum,
useInvoiceCustomizeTabsController,
} from './InvoiceCustomizeTabsController';
import styles from './InvoiceCustomizeTabs.module.scss';
import { useInvoiceCustomizeContext } from './InvoiceCustomizeProvider';
import React from 'react';
export function InvoiceCustomizeTabs() {
const { setCurrentTabId } = useInvoiceCustomizeTabsController();
const { CustomizeTabs } = useInvoiceCustomizeContext();
const tabItems = React.Children.map(CustomizeTabs, (node) => ({
...(React.isValidElement(node) ? node.props : {}),
}));
const handleChange = (value: InvoiceCustomizeTabsEnum) => {
setCurrentTabId(value);
};
return (
<Stack spacing={0} className={styles.root}>
<InvoiceCustomizeHeader 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

@@ -1,46 +0,0 @@
import React, { createContext, useContext, useState } from 'react';
export enum InvoiceCustomizeTabsEnum {
General = 'general',
Items = 'items',
Totals = 'totals'
}
const DEFAULT_TAB_ID = InvoiceCustomizeTabsEnum.General;
interface InvoiceCustomizeTabsControllerValue {
currentTabId: InvoiceCustomizeTabsEnum;
setCurrentTabId: React.Dispatch<
React.SetStateAction<InvoiceCustomizeTabsEnum>
>;
}
const InvoiceCustomizeTabsController = createContext(
{} as InvoiceCustomizeTabsControllerValue,
);
export const useInvoiceCustomizeTabsController = () => {
return useContext(InvoiceCustomizeTabsController);
};
interface InvoiceCustomizeTabsControllerProps {
children: React.ReactNode;
}
export const InvoiceCustomizeTabsControllerProvider = ({
children,
}: InvoiceCustomizeTabsControllerProps) => {
const [currentTabId, setCurrentTabId] =
useState<InvoiceCustomizeTabsEnum>(DEFAULT_TAB_ID);
const value = {
currentTabId,
setCurrentTabId,
};
return (
<InvoiceCustomizeTabsController.Provider value={value}>
{children}
</InvoiceCustomizeTabsController.Provider>
);
};

View File

@@ -1,27 +0,0 @@
// @ts-nocheck
import React from 'react';
import { Formik, Form, FormikHelpers } from 'formik';
export interface InvoiceCustomizeFormProps<T> {
initialValues?: T;
validationSchema?: any;
onSubmit?: (values: T, formikHelpers: FormikHelpers<T>) => void;
children?: React.ReactNode;
}
export function InvoiceCustomizeForm<T>({
initialValues,
validationSchema,
onSubmit,
children,
}: InvoiceCustomizeFormProps<T>) {
return (
<Formik<T>
initialValues={{ ...initialValues }}
validationSchema={validationSchema}
onSubmit={(value, helpers) => onSubmit && onSubmit(value, helpers)}
>
<Form>{children}</Form>
</Formik>
);
}

View File

@@ -1,5 +1,5 @@
import { FInputGroup, FSwitch, Group, Stack } from '@/components';
import { Classes } from '@blueprintjs/core';
import { FInputGroup, FSwitch, Group, Stack } from '@/components';
const items = [
{ key: 'dueAmount', label: 'Due Amount' },

View File

@@ -1,5 +1,5 @@
import clsx from 'classnames';
import styles from './PaperTemplate.module.scss';
import styles from './InvoicePaperTemplate.module.scss';
interface PaperTemplateProps {
invoiceNumber?: string;