diff --git a/packages/webapp/src/containers/ElementCustomize/components/CompanyLogoUpload.tsx b/packages/webapp/src/containers/ElementCustomize/components/CompanyLogoUpload.tsx index 2901cdea6..a5b3ec195 100644 --- a/packages/webapp/src/containers/ElementCustomize/components/CompanyLogoUpload.tsx +++ b/packages/webapp/src/containers/ElementCustomize/components/CompanyLogoUpload.tsx @@ -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} diff --git a/packages/webapp/src/containers/Preferences/Branding/PreferencesBranding.module.scss b/packages/webapp/src/containers/Preferences/Branding/PreferencesBranding.module.scss new file mode 100644 index 000000000..a228d1d9a --- /dev/null +++ b/packages/webapp/src/containers/Preferences/Branding/PreferencesBranding.module.scss @@ -0,0 +1,6 @@ + + +.fileUploadRoot{ + width: 350px; + height: 140px; +} \ No newline at end of file diff --git a/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingBoot.tsx b/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingBoot.tsx new file mode 100644 index 000000000..432206752 --- /dev/null +++ b/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingBoot.tsx @@ -0,0 +1,35 @@ +import React, { createContext, useContext, ReactNode } from 'react'; + +interface PreferencesBrandingContextType {} + +const PreferencesBrandingContext = + createContext( + {} as PreferencesBrandingContextType, + ); + +interface PreferencesBrandingProviderProps { + children: ReactNode; +} + +export const PreferencesBrandingBoot: React.FC< + PreferencesBrandingProviderProps +> = ({ children }) => { + const contextValue: PreferencesBrandingContextType = {}; + + return ( + + {children} + + ); +}; + +export const usePreferencesBrandingBoot = () => { + const context = useContext(PreferencesBrandingContext); + + if (context === undefined) { + throw new Error( + 'usePreferencesBranding must be used within a PreferencesBrandingProvider', + ); + } + return context; +}; diff --git a/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingForm.tsx b/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingForm.tsx new file mode 100644 index 000000000..2e4073613 --- /dev/null +++ b/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingForm.tsx @@ -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, + ) => { + 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 ( + +
{children}
+
+ ); +}; + +const formStyle: CSSProperties = { + display: 'flex', + flexDirection: 'column', + flex: 1, +}; diff --git a/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingFormContent.tsx b/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingFormContent.tsx new file mode 100644 index 000000000..c69d5ad7b --- /dev/null +++ b/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingFormContent.tsx @@ -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 ( + + + + + + + + + + + + + ); +} + +export function PreferencesBrandingFormFooter() { + const { isSubmitting } = useFormikContext(); + + return ( + + + + ); +} + +export function BrandingCompanyLogoUpload() { + const { setFieldValue, values } = + useFormikContext(); + + return ( + { + const imageUrl = file ? URL.createObjectURL(file) : ''; + + setFieldValue('_logoFile', file); + setFieldValue('logoUri', imageUrl); + }} + classNames={{ + root: styles.fileUploadRoot, + }} + /> + ); +} + +function BrandingCompanyLogoDesc() { + return ( + + + This logo will be displayed in transaction PDFs and email notifications. + + + Preferred Image Dimensions: 240 × 240 pixels @ 72 DPI Maximum File Size: + 1MB + + + ); +} diff --git a/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingPage.tsx b/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingPage.tsx new file mode 100644 index 000000000..295f4a66e --- /dev/null +++ b/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingPage.tsx @@ -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 ( + + + + + + + + + ); +} + +export default R.compose(withDashboardActions)(PreferencesBrandingPageRoot); diff --git a/packages/webapp/src/containers/Preferences/Branding/_types.ts b/packages/webapp/src/containers/Preferences/Branding/_types.ts new file mode 100644 index 000000000..603d8254a --- /dev/null +++ b/packages/webapp/src/containers/Preferences/Branding/_types.ts @@ -0,0 +1,6 @@ +export interface PreferencesBrandingFormValues { + logoKey: string; + logoUri: string; + primaryColor: string; + _logoFile?: any; +} diff --git a/packages/webapp/src/routes/preferences.tsx b/packages/webapp/src/routes/preferences.tsx index 5f3cc1088..f78e8b314 100644 --- a/packages/webapp/src/routes/preferences.tsx +++ b/packages/webapp/src/routes/preferences.tsx @@ -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')),