mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 12:20:31 +00:00
feat: Company branding preferences
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
// @ts-nocheck
|
||||
import { useRef, useState } from 'react';
|
||||
import clsx from 'classnames';
|
||||
import { Button, Intent } from '@blueprintjs/core';
|
||||
import { Icon, Stack } from '@/components';
|
||||
import { Dropzone, DropzoneProps } from '@/components/Dropzone';
|
||||
@@ -69,7 +70,7 @@ export function CompanyLogoUpload({
|
||||
onReject={(files) => console.log('rejected files', files)}
|
||||
maxSize={5 * 1024 ** 2}
|
||||
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}
|
||||
openRef={openRef}
|
||||
{...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')),
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/branding`,
|
||||
component: lazy(() => import('../containers/Preferences/Branding/PreferencesBrandingPage')),
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/users`,
|
||||
component: lazy(() => import('../containers/Preferences/Users/Users')),
|
||||
|
||||
Reference in New Issue
Block a user