mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 22:00:31 +00:00
feat: Company branding preferences
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
|
import clsx from 'classnames';
|
||||||
import { Button, Intent } from '@blueprintjs/core';
|
import { Button, Intent } from '@blueprintjs/core';
|
||||||
import { Icon, Stack } from '@/components';
|
import { Icon, Stack } from '@/components';
|
||||||
import { Dropzone, DropzoneProps } from '@/components/Dropzone';
|
import { Dropzone, DropzoneProps } from '@/components/Dropzone';
|
||||||
@@ -69,7 +70,7 @@ export function CompanyLogoUpload({
|
|||||||
onReject={(files) => console.log('rejected files', files)}
|
onReject={(files) => console.log('rejected files', files)}
|
||||||
maxSize={5 * 1024 ** 2}
|
maxSize={5 * 1024 ** 2}
|
||||||
accept={[MIME_TYPES.png, MIME_TYPES.jpeg]}
|
accept={[MIME_TYPES.png, MIME_TYPES.jpeg]}
|
||||||
classNames={{ root: styles?.root, content: styles.dropzoneContent }}
|
classNames={{ root: clsx(styles?.root, classNames?.root), content: styles.dropzoneContent }}
|
||||||
activateOnClick={false}
|
activateOnClick={false}
|
||||||
openRef={openRef}
|
openRef={openRef}
|
||||||
{...dropzoneProps}
|
{...dropzoneProps}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
.fileUploadRoot{
|
||||||
|
width: 350px;
|
||||||
|
height: 140px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import React, { createContext, useContext, ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface PreferencesBrandingContextType {}
|
||||||
|
|
||||||
|
const PreferencesBrandingContext =
|
||||||
|
createContext<PreferencesBrandingContextType>(
|
||||||
|
{} as PreferencesBrandingContextType,
|
||||||
|
);
|
||||||
|
|
||||||
|
interface PreferencesBrandingProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PreferencesBrandingBoot: React.FC<
|
||||||
|
PreferencesBrandingProviderProps
|
||||||
|
> = ({ children }) => {
|
||||||
|
const contextValue: PreferencesBrandingContextType = {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PreferencesBrandingContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</PreferencesBrandingContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePreferencesBrandingBoot = () => {
|
||||||
|
const context = useContext(PreferencesBrandingContext);
|
||||||
|
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
'usePreferencesBranding must be used within a PreferencesBrandingProvider',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import React, { CSSProperties } from 'react';
|
||||||
|
import { Formik, Form, FormikHelpers } from 'formik';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import { PreferencesBrandingFormValues } from './_types';
|
||||||
|
import { useUploadAttachments } from '@/hooks/query/attachments';
|
||||||
|
import { AppToaster } from '@/components';
|
||||||
|
import { Intent } from '@blueprintjs/core';
|
||||||
|
import { excludePrivateProps } from '@/utils';
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
logoKey: '',
|
||||||
|
logoUri: '',
|
||||||
|
primaryColor: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const validationSchema = Yup.object({
|
||||||
|
logoKey: Yup.string().optional(),
|
||||||
|
logoUri: Yup.string().optional(),
|
||||||
|
primaryColor: Yup.string().required('Primary color is required'),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface PreferencesBrandingFormProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PreferencesBrandingForm = ({
|
||||||
|
children,
|
||||||
|
}: PreferencesBrandingFormProps) => {
|
||||||
|
// Uploads the attachments.
|
||||||
|
const { mutateAsync: uploadAttachments } = useUploadAttachments({});
|
||||||
|
|
||||||
|
const handleSubmit = async (
|
||||||
|
values: PreferencesBrandingFormValues,
|
||||||
|
{ setSubmitting }: FormikHelpers<PreferencesBrandingFormValues>,
|
||||||
|
) => {
|
||||||
|
const _values = { ...values };
|
||||||
|
|
||||||
|
const handleError = (message: string) => {
|
||||||
|
AppToaster.show({ intent: Intent.DANGER, message });
|
||||||
|
setSubmitting(false);
|
||||||
|
};
|
||||||
|
// Start upload the company logo file if it is presented.
|
||||||
|
if (values._logoFile) {
|
||||||
|
const formData = new FormData();
|
||||||
|
const key = Date.now().toString();
|
||||||
|
|
||||||
|
formData.append('file', values._logoFile);
|
||||||
|
formData.append('internalKey', key);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// @ts-expect-error
|
||||||
|
const uploadedAttachmentRes = await uploadAttachments(formData);
|
||||||
|
setSubmitting(false);
|
||||||
|
|
||||||
|
// Adds the attachment key to the values after finishing upload.
|
||||||
|
_values['_logoFile'] = uploadedAttachmentRes?.key;
|
||||||
|
} catch {
|
||||||
|
handleError('An error occurred while uploading company logo.');
|
||||||
|
setSubmitting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Exclude all the private props that starts with _.
|
||||||
|
const excludedPrivateValues = excludePrivateProps(_values);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={initialValues}
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
<Form style={formStyle}>{children}</Form>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formStyle: CSSProperties = {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
flex: 1,
|
||||||
|
};
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { Button, Classes, Intent, Text } from '@blueprintjs/core';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import { FFormGroup, Group, Stack } from '@/components';
|
||||||
|
import { FColorInput } from '@/components/Forms/FColorInput';
|
||||||
|
import { CompanyLogoUpload } from '@/containers/ElementCustomize/components/CompanyLogoUpload';
|
||||||
|
import { PreferencesBrandingFormValues } from './_types';
|
||||||
|
import styles from './PreferencesBranding.module.scss';
|
||||||
|
|
||||||
|
export function PreferencesBrandingFormContent() {
|
||||||
|
return (
|
||||||
|
<Stack style={{ flex: '1' }}>
|
||||||
|
<FFormGroup name={'companyLogo'} label={'Company Logo'}>
|
||||||
|
<Group spacing={15} align={'left'}>
|
||||||
|
<BrandingCompanyLogoUpload />
|
||||||
|
<BrandingCompanyLogoDesc />
|
||||||
|
</Group>
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
<FFormGroup
|
||||||
|
name={'primaryColor'}
|
||||||
|
label={'Primary Color'}
|
||||||
|
helperText={
|
||||||
|
'Note: These preferences will be applied across PDF and mail templates, including the customer payment page.'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FColorInput name={'primaryColor'} />
|
||||||
|
</FFormGroup>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PreferencesBrandingFormFooter() {
|
||||||
|
const { isSubmitting } = useFormikContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group style={{ padding: '12px 0', borderTop: '1px solid #e1e1e1' }}>
|
||||||
|
<Button intent={Intent.PRIMARY} type={'submit'} loading={isSubmitting}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BrandingCompanyLogoUpload() {
|
||||||
|
const { setFieldValue, values } =
|
||||||
|
useFormikContext<PreferencesBrandingFormValues>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CompanyLogoUpload
|
||||||
|
initialPreview={values?.logoUri}
|
||||||
|
onChange={(file) => {
|
||||||
|
const imageUrl = file ? URL.createObjectURL(file) : '';
|
||||||
|
|
||||||
|
setFieldValue('_logoFile', file);
|
||||||
|
setFieldValue('logoUri', imageUrl);
|
||||||
|
}}
|
||||||
|
classNames={{
|
||||||
|
root: styles.fileUploadRoot,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BrandingCompanyLogoDesc() {
|
||||||
|
return (
|
||||||
|
<Stack spacing={10} style={{ fontSize: 12, paddingTop: 12, flex: 1 }}>
|
||||||
|
<Text className={Classes.TEXT_MUTED}>
|
||||||
|
This logo will be displayed in transaction PDFs and email notifications.
|
||||||
|
</Text>
|
||||||
|
<Text className={Classes.TEXT_MUTED}>
|
||||||
|
Preferred Image Dimensions: 240 × 240 pixels @ 72 DPI Maximum File Size:
|
||||||
|
1MB
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import * as R from 'ramda';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { Stack } from '@/components';
|
||||||
|
import { PreferencesBrandingBoot } from './PreferencesBrandingBoot';
|
||||||
|
import { PreferencesBrandingForm } from './PreferencesBrandingForm';
|
||||||
|
import {
|
||||||
|
PreferencesBrandingFormContent,
|
||||||
|
PreferencesBrandingFormFooter,
|
||||||
|
} from './PreferencesBrandingFormContent';
|
||||||
|
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
|
||||||
|
|
||||||
|
function PreferencesBrandingPageRoot({ changePreferencesPageTitle }) {
|
||||||
|
useEffect(() => {
|
||||||
|
changePreferencesPageTitle('Branding');
|
||||||
|
}, [changePreferencesPageTitle]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
style={{ padding: '20px 40px 0', maxWidth: 900, width: '100%', flex: 1 }}
|
||||||
|
>
|
||||||
|
<PreferencesBrandingBoot>
|
||||||
|
<PreferencesBrandingForm>
|
||||||
|
<PreferencesBrandingFormContent />
|
||||||
|
<PreferencesBrandingFormFooter />
|
||||||
|
</PreferencesBrandingForm>
|
||||||
|
</PreferencesBrandingBoot>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default R.compose(withDashboardActions)(PreferencesBrandingPageRoot);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export interface PreferencesBrandingFormValues {
|
||||||
|
logoKey: string;
|
||||||
|
logoUri: string;
|
||||||
|
primaryColor: string;
|
||||||
|
_logoFile?: any;
|
||||||
|
}
|
||||||
@@ -9,6 +9,11 @@ export const getPreferenceRoutes = () => [
|
|||||||
component: lazy(() => import('@/containers/Preferences/General/General')),
|
component: lazy(() => import('@/containers/Preferences/General/General')),
|
||||||
exact: true,
|
exact: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: `${BASE_URL}/branding`,
|
||||||
|
component: lazy(() => import('../containers/Preferences/Branding/PreferencesBrandingPage')),
|
||||||
|
exact: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: `${BASE_URL}/users`,
|
path: `${BASE_URL}/users`,
|
||||||
component: lazy(() => import('../containers/Preferences/Users/Users')),
|
component: lazy(() => import('../containers/Preferences/Users/Users')),
|
||||||
|
|||||||
Reference in New Issue
Block a user