diff --git a/packages/server/src/api/controllers/Organization.ts b/packages/server/src/api/controllers/Organization.ts index 2124b76fe..76157739e 100644 --- a/packages/server/src/api/controllers/Organization.ts +++ b/packages/server/src/api/controllers/Organization.ts @@ -118,8 +118,8 @@ export default class OrganizationController extends BaseController { check('address.phone').optional().isString().trim(), // # Branding - check('primary_color').optional().isHexColor().trim(), - check('logo_key').optional().isString().trim(), + check('primary_color').optional({ nullable: true }).isHexColor().trim(), + check('logo_key').optional({ nullable: true }).isString().trim(), check('tax_number').optional({ nullable: true }).isString().trim(), ]; diff --git a/packages/server/src/system/models/TenantMetadata.ts b/packages/server/src/system/models/TenantMetadata.ts index ccf183c2f..2618a53c6 100644 --- a/packages/server/src/system/models/TenantMetadata.ts +++ b/packages/server/src/system/models/TenantMetadata.ts @@ -41,4 +41,18 @@ export default class TenantMetadata extends BaseModel { static get tableName() { return 'tenants_metadata'; } + + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return ['logoUri']; + } + + /** + * + */ + public get logoUri() { + return this.logoKey ? `https://bigcapital.sfo3.digitaloceanspaces.com/${this.logoKey}` : null; + } } diff --git a/packages/webapp/src/constants/preferencesMenu.tsx b/packages/webapp/src/constants/preferencesMenu.tsx index b6c474836..468cfd89d 100644 --- a/packages/webapp/src/constants/preferencesMenu.tsx +++ b/packages/webapp/src/constants/preferencesMenu.tsx @@ -8,6 +8,11 @@ export default [ disabled: false, href: '/preferences/general', }, + { + text: 'Branding', + disabled: false, + href: '/preferences/branding', + }, { text: 'Billing', href: '/preferences/billing', diff --git a/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingBoot.tsx b/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingBoot.tsx index 432206752..7456efbca 100644 --- a/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingBoot.tsx +++ b/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingBoot.tsx @@ -1,6 +1,10 @@ +import { useCurrentOrganization } from '@/hooks/query'; import React, { createContext, useContext, ReactNode } from 'react'; -interface PreferencesBrandingContextType {} +interface PreferencesBrandingContextType { + isOrganizationLoading: boolean; + organization: any; +} const PreferencesBrandingContext = createContext( @@ -14,7 +18,14 @@ interface PreferencesBrandingProviderProps { export const PreferencesBrandingBoot: React.FC< PreferencesBrandingProviderProps > = ({ children }) => { - const contextValue: PreferencesBrandingContextType = {}; + // Fetches current organization information. + const { isLoading: isOrganizationLoading, data: organization } = + useCurrentOrganization({}); + + const contextValue: PreferencesBrandingContextType = { + isOrganizationLoading, + organization, + }; return ( diff --git a/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingForm.tsx b/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingForm.tsx index 2e4073613..5bbdc829c 100644 --- a/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingForm.tsx +++ b/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingForm.tsx @@ -1,11 +1,19 @@ import React, { CSSProperties } from 'react'; import { Formik, Form, FormikHelpers } from 'formik'; import * as Yup from 'yup'; +import { omit } from 'lodash'; import { PreferencesBrandingFormValues } from './_types'; import { useUploadAttachments } from '@/hooks/query/attachments'; import { AppToaster } from '@/components'; import { Intent } from '@blueprintjs/core'; -import { excludePrivateProps } from '@/utils'; +import { + excludePrivateProps, + transformToCamelCase, + transformToForm, + transfromToSnakeCase, +} from '@/utils'; +import { useUpdateOrganization } from '@/hooks/query'; +import { usePreferencesBrandingBoot } from './PreferencesBrandingBoot'; const initialValues = { logoKey: '', @@ -28,7 +36,19 @@ export const PreferencesBrandingForm = ({ }: PreferencesBrandingFormProps) => { // Uploads the attachments. const { mutateAsync: uploadAttachments } = useUploadAttachments({}); + // Mutate organization information. + const { mutateAsync: updateOrganization } = useUpdateOrganization(); + const { organization } = usePreferencesBrandingBoot(); + + const formInitialValues = { + ...transformToForm( + transformToCamelCase(organization?.metadata), + initialValues, + ), + } as PreferencesBrandingFormValues; + + // Handle the form submitting. const handleSubmit = async ( values: PreferencesBrandingFormValues, { setSubmitting }: FormikHelpers, @@ -53,20 +73,32 @@ export const PreferencesBrandingForm = ({ setSubmitting(false); // Adds the attachment key to the values after finishing upload. - _values['_logoFile'] = uploadedAttachmentRes?.key; + _values['logoKey'] = 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); } + // Exclude all the private props that starts with _. + const excludedPrivateValues = excludePrivateProps(_values); + + const __values = transfromToSnakeCase( + omit(excludedPrivateValues, ['logoUri']), + ); + // Update organization branding. + // @ts-expect-error + await updateOrganization({ ...__values }); + + AppToaster.show({ + message: 'Organization branding has been updated.', + intent: Intent.SUCCESS, + }); }; return ( diff --git a/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingFormContent.tsx b/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingFormContent.tsx index 0762b8482..3fd8d2c1a 100644 --- a/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingFormContent.tsx +++ b/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingFormContent.tsx @@ -53,6 +53,7 @@ export function BrandingCompanyLogoUpload() { setFieldValue('_logoFile', file); setFieldValue('logoUri', imageUrl); + setFieldValue('logoKey', ''); }} classNames={{ root: styles.fileUploadRoot, diff --git a/packages/webapp/src/containers/Preferences/General/GeneralForm.tsx b/packages/webapp/src/containers/Preferences/General/GeneralForm.tsx index 4560a0dde..8d983d547 100644 --- a/packages/webapp/src/containers/Preferences/General/GeneralForm.tsx +++ b/packages/webapp/src/containers/Preferences/General/GeneralForm.tsx @@ -105,7 +105,6 @@ export default function PreferencesGeneralForm({ isSubmitting }) { @@ -120,7 +119,6 @@ export default function PreferencesGeneralForm({ isSubmitting }) { placeholder={'Address 2'} fastField /> - - apiRequest.put('organization', information), + (information: any) => apiRequest.put('organization', information), { onSuccess: () => { queryClient.invalidateQueries(t.ORGANIZATION_CURRENT);