mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 14:50:32 +00:00
feat: rich editor component
This commit is contained in:
52
packages/webapp/src/components/Forms/FRichEditor.tsx
Normal file
52
packages/webapp/src/components/Forms/FRichEditor.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FieldConfig, FieldProps } from 'formik';
|
||||||
|
import { Field } from '@blueprintjs-formik/core';
|
||||||
|
import { RichEditor, RichEditorProps } from '../../components/RichEditor';
|
||||||
|
|
||||||
|
export interface FRichEditorProps
|
||||||
|
extends Omit<FieldConfig, 'children' | 'component' | 'as'>,
|
||||||
|
RichEditorProps {
|
||||||
|
name: string;
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FieldToRichEditorProps
|
||||||
|
extends FieldProps,
|
||||||
|
Omit<RichEditorProps, 'form'> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transformes the field props to `RichEditor` props.
|
||||||
|
* @param {FieldToRichEditorProps}
|
||||||
|
* @returns {HTMLSelectProps}
|
||||||
|
*/
|
||||||
|
function fieldToRichEditor({
|
||||||
|
field: { onBlur: onFieldBlur, ...field },
|
||||||
|
form: { touched, errors, ...form },
|
||||||
|
...props
|
||||||
|
}: FieldToRichEditorProps): RichEditorProps {
|
||||||
|
return {
|
||||||
|
...field,
|
||||||
|
...props,
|
||||||
|
onChange: (value: string) => {
|
||||||
|
form.setFieldValue(field.name, value);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transformes field props to `RichEditor` props.
|
||||||
|
* @param {FieldToRichEditorProps}
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
*/
|
||||||
|
function FieldToRichEditor({ ...props }: FieldToRichEditorProps): JSX.Element {
|
||||||
|
return <RichEditor {...fieldToRichEditor(props)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rich editor wrapper to bind with Formik.
|
||||||
|
* @param {FRichEditorProps} props -
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
*/
|
||||||
|
export function FRichEditor({ ...props }: FRichEditorProps): JSX.Element {
|
||||||
|
return <Field {...props} component={FieldToRichEditor} />;
|
||||||
|
}
|
||||||
@@ -4,4 +4,5 @@ export * from './FMoneyInputGroup';
|
|||||||
export * from './BlueprintFormik';
|
export * from './BlueprintFormik';
|
||||||
export * from './InputPrependText';
|
export * from './InputPrependText';
|
||||||
export * from './InputPrependButton';
|
export * from './InputPrependButton';
|
||||||
export * from './MoneyInputGroup';
|
export * from './MoneyInputGroup';
|
||||||
|
export * from './FRichEditor';
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/* Basic editor styles */
|
||||||
|
.tiptap {
|
||||||
|
color: #222;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
>*+* {
|
||||||
|
margin-top: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: rgba(#ffffff, 0.1);
|
||||||
|
color: rgba(#ffffff, 0.6);
|
||||||
|
border: 1px solid rgba(#ffffff, 0.1);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: rgba(#ffffff, 0.1);
|
||||||
|
font-family: "JetBrainsMono", monospace;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
|
code {
|
||||||
|
color: inherit;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 1rem;
|
||||||
|
border-left: 2px solid rgba(#ffffff, 0.4);
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 2px solid rgba(#ffffff, 0.1);
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
packages/webapp/src/components/RichEditor/RichEditor.tsx
Normal file
58
packages/webapp/src/components/RichEditor/RichEditor.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { Color } from '@tiptap/extension-color';
|
||||||
|
import ListItem from '@tiptap/extension-list-item';
|
||||||
|
import TextStyle from '@tiptap/extension-text-style';
|
||||||
|
import { EditorProvider } from '@tiptap/react';
|
||||||
|
import StarterKit from '@tiptap/starter-kit';
|
||||||
|
import { useUncontrolled } from '@/hooks/useUncontrolled';
|
||||||
|
import { Box } from '../Layout/Box';
|
||||||
|
import './RichEditor.style.scss';
|
||||||
|
|
||||||
|
const extensions = [
|
||||||
|
Color.configure({ types: [TextStyle.name, ListItem.name] }),
|
||||||
|
TextStyle.configure({ types: [ListItem.name] }),
|
||||||
|
StarterKit.configure({
|
||||||
|
bulletList: {
|
||||||
|
keepMarks: true,
|
||||||
|
keepAttributes: false,
|
||||||
|
},
|
||||||
|
orderedList: {
|
||||||
|
keepMarks: true,
|
||||||
|
keepAttributes: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface RichEditorProps {
|
||||||
|
value?: string;
|
||||||
|
initialValue?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
export const RichEditor = ({
|
||||||
|
value,
|
||||||
|
initialValue,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
}: RichEditorProps) => {
|
||||||
|
const [content, handleChange] = useUncontrolled({
|
||||||
|
value,
|
||||||
|
initialValue,
|
||||||
|
onChange,
|
||||||
|
finalValue: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleBlur = ({ editor }) => {
|
||||||
|
handleChange(editor.getHTML());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className={className}>
|
||||||
|
<EditorProvider
|
||||||
|
extensions={extensions}
|
||||||
|
content={content}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
packages/webapp/src/components/RichEditor/index.ts
Normal file
1
packages/webapp/src/components/RichEditor/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './RichEditor';
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import './styles.scss';
|
||||||
|
import { Color } from '@tiptap/extension-color';
|
||||||
|
import ListItem from '@tiptap/extension-list-item';
|
||||||
|
import TextStyle from '@tiptap/extension-text-style';
|
||||||
|
import { EditorProvider } from '@tiptap/react';
|
||||||
|
import StarterKit from '@tiptap/starter-kit';
|
||||||
|
import { Box } from '@/components';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { useUncontrolled } from '@/hooks/useUncontrolled';
|
||||||
|
|
||||||
|
const extensions = [
|
||||||
|
Color.configure({ types: [TextStyle.name, ListItem.name] }),
|
||||||
|
TextStyle.configure({ types: [ListItem.name] }),
|
||||||
|
StarterKit.configure({
|
||||||
|
bulletList: {
|
||||||
|
keepMarks: true,
|
||||||
|
keepAttributes: false,
|
||||||
|
},
|
||||||
|
orderedList: {
|
||||||
|
keepMarks: true,
|
||||||
|
keepAttributes: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface RichEditorProps {
|
||||||
|
value?: string;
|
||||||
|
initialValue?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
export const RichEditor = ({
|
||||||
|
value,
|
||||||
|
initialValue,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
}: RichEditorProps) => {
|
||||||
|
const [content, handleChange] = useUncontrolled({
|
||||||
|
value,
|
||||||
|
initialValue,
|
||||||
|
finalValue: '',
|
||||||
|
onChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Root>
|
||||||
|
<EditorProvider
|
||||||
|
extensions={extensions}
|
||||||
|
content={content}
|
||||||
|
onBlur={handleChange}
|
||||||
|
/>
|
||||||
|
</Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Root = styled(Box)`
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #dedfe9;
|
||||||
|
border-top: 0;
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
|
`;
|
||||||
@@ -1,8 +1,15 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { Form, useFormikContext } from 'formik';
|
import { Form, useFormikContext } from 'formik';
|
||||||
import { FFormGroup, FInputGroup, FMultiSelect } from '@/components';
|
import {
|
||||||
|
FFormGroup,
|
||||||
|
FInputGroup,
|
||||||
|
FMultiSelect,
|
||||||
|
FRichEditor,
|
||||||
|
FSwitch,
|
||||||
|
Hint,
|
||||||
|
} from '@/components';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Button, Classes, Intent } from '@blueprintjs/core';
|
import { Button, Classes, Intent, Position } from '@blueprintjs/core';
|
||||||
import { saveInvoke } from '@/utils';
|
import { saveInvoke } from '@/utils';
|
||||||
|
|
||||||
interface SendMailNotificationFormProps {
|
interface SendMailNotificationFormProps {
|
||||||
@@ -24,25 +31,47 @@ export function SendMailNotificationForm({
|
|||||||
<HeaderBox>
|
<HeaderBox>
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
label={'From'}
|
label={'From'}
|
||||||
|
labelInfo={
|
||||||
|
<Hint
|
||||||
|
content={'asdasd asdasd asdsad'}
|
||||||
|
position={Position.BOTTOM_LEFT}
|
||||||
|
/>
|
||||||
|
}
|
||||||
name={'from'}
|
name={'from'}
|
||||||
inline={true}
|
inline={true}
|
||||||
fastField={true}
|
fastField={true}
|
||||||
>
|
>
|
||||||
<FMultiSelect
|
<FMultiSelect
|
||||||
items={[]}
|
items={[
|
||||||
|
{
|
||||||
|
text: 'a.bouhuolia@gmail.com',
|
||||||
|
value: 'a.bouhuolia@gmail.com',
|
||||||
|
},
|
||||||
|
]}
|
||||||
name={'from'}
|
name={'from'}
|
||||||
placeholder=""
|
placeholder=""
|
||||||
popoverProps={{ minimal: true, fill: true }}
|
popoverProps={{ minimal: true, fill: true }}
|
||||||
|
tagInputProps={{
|
||||||
|
tagProps: { round: true, minimal: true, large: true },
|
||||||
|
}}
|
||||||
fill={true}
|
fill={true}
|
||||||
/>
|
/>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
<FFormGroup label={'To'} name={'to'} inline={true} fastField={true}>
|
<FFormGroup label={'To'} name={'to'} inline={true} fastField={true}>
|
||||||
<FMultiSelect
|
<FMultiSelect
|
||||||
items={[]}
|
items={[
|
||||||
|
{
|
||||||
|
text: 'a.bouhuolia@gmail.com',
|
||||||
|
value: 'a.bouhuolia@gmail.com',
|
||||||
|
},
|
||||||
|
]}
|
||||||
name={'to'}
|
name={'to'}
|
||||||
placeholder=""
|
placeholder=""
|
||||||
popoverProps={{ minimal: true, fill: true }}
|
popoverProps={{ minimal: true, fill: true }}
|
||||||
|
tagInputProps={{
|
||||||
|
tagProps: { round: true, minimal: true, large: true },
|
||||||
|
}}
|
||||||
fill={true}
|
fill={true}
|
||||||
/>
|
/>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
@@ -56,6 +85,12 @@ export function SendMailNotificationForm({
|
|||||||
<FInputGroup name={'subject'} fill={true} />
|
<FInputGroup name={'subject'} fill={true} />
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
</HeaderBox>
|
</HeaderBox>
|
||||||
|
|
||||||
|
<MailMessageEditor name={'message'} />
|
||||||
|
|
||||||
|
<AttachFormGroup name={'attach_invoice'} inline>
|
||||||
|
<FSwitch name={'attach_invoice'} label={'Attach Invoice'} />
|
||||||
|
</AttachFormGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={Classes.DIALOG_FOOTER}>
|
<div className={Classes.DIALOG_FOOTER}>
|
||||||
@@ -82,16 +117,33 @@ export function SendMailNotificationForm({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const AttachFormGroup = styled(FFormGroup)`
|
||||||
|
background: #f8f9fb;
|
||||||
|
margin-top: 0.6rem;
|
||||||
|
padding: 4px 14px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #dcdcdd;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MailMessageEditor = styled(FRichEditor)`
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #dedfe9;
|
||||||
|
border-top: 0;
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
|
`;
|
||||||
|
|
||||||
const HeaderBox = styled('div')`
|
const HeaderBox = styled('div')`
|
||||||
border-top-right-radius: 5px;
|
border-top-right-radius: 5px;
|
||||||
border-top-left-radius: 5px;
|
border-top-left-radius: 5px;
|
||||||
border: 1px solid #dddfe9;
|
border: 1px solid #dddfe9;
|
||||||
padding: 15px;
|
border-bottom: 2px solid #eaeaef;
|
||||||
|
padding: 6px 15px;
|
||||||
|
|
||||||
.bp4-form-group {
|
.bp4-form-group {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-top: 12px;
|
padding-top: 8px;
|
||||||
padding-bottom: 12px;
|
padding-bottom: 8px;
|
||||||
|
|
||||||
&:not(:last-of-type) {
|
&:not(:last-of-type) {
|
||||||
border-bottom: 1px solid #dddfe9;
|
border-bottom: 1px solid #dddfe9;
|
||||||
@@ -114,5 +166,19 @@ const HeaderBox = styled('div')`
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bp4-input {
|
.bp4-input {
|
||||||
|
border-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&.bp4-active {
|
||||||
|
box-shadow: 0 0 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp4-input-ghost {
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.bp4-tag-input-values {
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
Reference in New Issue
Block a user