diff --git a/package-lock.json b/package-lock.json index 9dae798b71..7a0e7f1433 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "pusher-js": "^8.4.0-rc2", "randexp": "^0.5.3", "react": "^18.2.0", + "react-avatar": "^5.0.3", "react-colorful": "^5.6.1", "react-date-range": "^1.4.0", "react-datepicker": "^4.11.0", @@ -4205,6 +4206,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -4391,6 +4401,18 @@ "toggle-selection": "^1.0.6" } }, + "node_modules/core-js-pure": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.40.0.tgz", + "integrity": "sha512-AtDzVIgRrmRKQai62yuSIN5vNiQjcJakJb4fbhVw3ehxx7Lohphvw9SGNWKhLFqSxC4ilD0g/L1huAYFQU3Q6A==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -4522,6 +4544,15 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, "node_modules/css-box-model": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", @@ -6676,6 +6707,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT" + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -6876,6 +6913,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-retina": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-retina/-/is-retina-1.0.3.tgz", + "integrity": "sha512-/tCmbIETZwCd8uHWO+GvbRa7jxwHFHdfetHfiwoP0aN9UDf3prUJMtKn7iBFYipYhqY1bSTjur8hC/Dakt8eyw==", + "license": "MIT" + }, "node_modules/is-set": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", @@ -8997,6 +9040,17 @@ "css-mediaquery": "^0.1.2" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "license": "BSD-3-Clause", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/mdast-util-from-markdown": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz", @@ -11109,6 +11163,22 @@ "node": ">=0.10.0" } }, + "node_modules/react-avatar": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/react-avatar/-/react-avatar-5.0.3.tgz", + "integrity": "sha512-DNc+qkWH9QehSEZqHBhqpXWsPY+rU9W7kD68QFHfu8Atfsvx/3ML0DzAePgTUd96nCXQQ3KZMcC3LKYT8FiBIg==", + "license": "MIT", + "dependencies": { + "is-retina": "^1.0.3", + "md5": "^2.0.0" + }, + "peerDependencies": { + "@babel/runtime": ">=7", + "core-js-pure": ">=3", + "prop-types": "^15.0.0 || ^16.0.0", + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-base16-styling": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.9.1.tgz", diff --git a/package.json b/package.json index 458480c4f2..9d4136d905 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "pusher-js": "^8.4.0-rc2", "randexp": "^0.5.3", "react": "^18.2.0", + "react-avatar": "^5.0.3", "react-colorful": "^5.6.1", "react-date-range": "^1.4.0", "react-datepicker": "^4.11.0", diff --git a/src/common/hooks/useHandleCollapseExpandSidebar.ts b/src/common/hooks/useHandleCollapseExpandSidebar.ts new file mode 100644 index 0000000000..4bdadba196 --- /dev/null +++ b/src/common/hooks/useHandleCollapseExpandSidebar.ts @@ -0,0 +1,37 @@ +/** + * Invoice Ninja (https://invoiceninja.com). + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */ + +import { useUpdateCompanyUser } from '$app/pages/settings/user/common/hooks/useUpdateCompanyUser'; +import { cloneDeep, set } from 'lodash'; +import { useHandleCurrentUserChangeProperty } from './useHandleCurrentUserChange'; +import { useInjectUserChanges } from './useInjectUserChanges'; + +export function useHandleCollapseExpandSidebar() { + const userChanges = useInjectUserChanges(); + + const updateCompanyUser = useUpdateCompanyUser(); + const handleUserChange = useHandleCurrentUserChangeProperty(); + + return (value: boolean) => { + handleUserChange('company_user.react_settings.show_mini_sidebar', value); + + if (userChanges) { + const updatedUserChanges = cloneDeep(userChanges); + + set( + updatedUserChanges, + 'company_user.react_settings.show_mini_sidebar', + value + ); + + updateCompanyUser(updatedUserChanges); + } + }; +} diff --git a/src/common/hooks/useInjectUserChanges.ts b/src/common/hooks/useInjectUserChanges.ts index 2f83cb710d..1bb3ab841e 100644 --- a/src/common/hooks/useInjectUserChanges.ts +++ b/src/common/hooks/useInjectUserChanges.ts @@ -31,7 +31,7 @@ export function useInjectUserChanges(options?: Options) { const changes = useUserChanges(); useEffect(() => { - if (changes && options?.overwrite === false) { + if (Object.keys(changes || {}).length && !options?.overwrite) { // We don't want to overwrite existing changes, // so let's just not inject anything if we already have a value, // and relative argument. diff --git a/src/components/CompanySwitcher.tsx b/src/components/CompanySwitcher.tsx index 4451d0b0fe..c6709fedd2 100644 --- a/src/components/CompanySwitcher.tsx +++ b/src/components/CompanySwitcher.tsx @@ -10,27 +10,37 @@ import { Menu, Transition } from '@headlessui/react'; import { AuthenticationTypes } from '$app/common/dtos/authentication'; -import { useCurrentUser } from '$app/common/hooks/useCurrentUser'; import { authenticate } from '$app/common/stores/slices/user'; import { RootState } from '$app/common/stores/store'; import { Fragment, useEffect, useState } from 'react'; -import { Check, ChevronDown } from 'react-feather'; import { useTranslation } from 'react-i18next'; import { useQueryClient } from 'react-query'; import { useDispatch, useSelector } from 'react-redux'; -import { DropdownElement } from './dropdown/DropdownElement'; import { useLogo } from '$app/common/hooks/useLogo'; import { useCompanyName } from '$app/common/hooks/useLogo'; import { CompanyCreate } from '$app/pages/settings/company/create/CompanyCreate'; import { useCurrentCompany } from '$app/common/hooks/useCurrentCompany'; import { isDemo, isHosted, isSelfHosted } from '$app/common/helpers'; import { freePlan } from '$app/common/guards/guards/free-plan'; -import { Icon } from './icons/Icon'; -import { MdLogout, MdManageAccounts } from 'react-icons/md'; -import { BiPlusCircle } from 'react-icons/bi'; -import { useColorScheme } from '$app/common/colors'; import { useAdmin } from '$app/common/hooks/permissions/useHasPermission'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; +import { ExpandCollapseChevron } from './icons/ExpandCollapseChevron'; +import { styled } from 'styled-components'; +import { usePreventNavigation } from '$app/common/hooks/usePreventNavigation'; +import { Check } from './icons/Check'; +import Avatar from 'react-avatar'; +import { Plus } from './icons/Plus'; +import { Person } from './icons/Person'; +import { Exit } from './icons/Exit'; +import { useCurrentUser } from '$app/common/hooks/useCurrentUser'; +import { useInjectUserChanges } from '$app/common/hooks/useInjectUserChanges'; +import { useColorScheme } from '$app/common/colors'; + +const SwitcherDiv = styled.div` + &:hover { + background-color: ${(props) => props.theme.hoverColor}; + } +`; export function CompanySwitcher() { const [t] = useTranslation(); @@ -44,7 +54,6 @@ export function CompanySwitcher() { const canUserAddCompany = isSelfHosted() || (isHosted() && !freePlan()); const logo = useLogo(); - const user = useCurrentUser(); const location = useLocation(); const colors = useColorScheme(); const companyName = useCompanyName(); @@ -52,6 +61,15 @@ export function CompanySwitcher() { const { isAdmin, isOwner } = useAdmin(); const currentCompany = useCurrentCompany(); + const currentUser = useCurrentUser(); + const userChanges = useInjectUserChanges(); + + const isMiniSidebar = Boolean( + userChanges?.company_user?.react_settings?.show_mini_sidebar + ); + + const preventNavigation = usePreventNavigation(); + const [shouldShowAddCompany, setShouldShowAddCompany] = useState(false); const [isCompanyCreateModalOpened, setIsCompanyCreateModalOpened] = @@ -97,6 +115,22 @@ export function CompanySwitcher() { } }, [currentCompany]); + if (isMiniSidebar) { + return ( + <> + Company logo + + ); + } + return ( <> - -
- Company logo -
- - {companyName} - - {(user?.first_name || user?.last_name) && ( - - {user.first_name} {user.last_name} - - )} -
+ +
+ Company logo + + + {companyName} + + +
-
-
+
- -

{t('signed_in_as')}

-

{user?.email}

-
+
+

{t('signed_in_as')}

+ +

+ {currentUser?.email} +

+
-
+
{state?.api?.length >= 1 && state?.api?.map((record: any, index: number) => ( - switchCompany(index)} - > -
- - {record.company.settings.name || - t('untitled_company')} - - - {state.currentIndex === index && } -
-
+
+ {index === 0 && ( +

+ {t('company')} +

+ )} + + + preventNavigation({ + fn: () => switchCompany(index), + actionKey: 'switchCompany', + }) + } + > +
+ {record.company.settings.company_logo ? ( + Company logo + ) : ( + + )} + +
+ {record.company.settings.name || + t('untitled_company')} +
+
+ + {state.currentIndex === index && ( + + )} +
+
))}
+
{shouldShowAddCompany && canUserAddCompany && (isAdmin || isOwner) && ( - setIsCompanyCreateModalOpened(true)} - icon={} - > - {t('add_company')} - +
+ setIsCompanyCreateModalOpened(true)} + > + + + {t('add_company')} + +
)} {(isAdmin || isOwner) && ( - } - > - {t('account_management')} - +
+ + preventNavigation({ + url: '/settings/account_management', + }) + } + > + + + {t('account_management')} + +
)} - } - > - {t('logout')} - +
+ + preventNavigation({ + url: '/logout', + }) + } + > + + + {t('logout')} + +
diff --git a/src/components/HelpSidebarIcons.tsx b/src/components/HelpSidebarIcons.tsx index 9114b534c8..9ebc0ded7a 100644 --- a/src/components/HelpSidebarIcons.tsx +++ b/src/components/HelpSidebarIcons.tsx @@ -14,15 +14,13 @@ import { request } from '$app/common/helpers/request'; import { useCurrentAccount } from '$app/common/hooks/useCurrentAccount'; import { updateCompanyUsers } from '$app/common/stores/slices/company-users'; import { useFormik } from 'formik'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { HelpCircle, Info, Mail, MessageSquare, AlertCircle, - ChevronLeft, - ChevronRight, } from 'react-feather'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; @@ -32,9 +30,6 @@ import { Modal } from './Modal'; import { toast } from '$app/common/helpers/toast/toast'; import { useColorScheme } from '$app/common/colors'; import { useInjectUserChanges } from '$app/common/hooks/useInjectUserChanges'; -import { useHandleCurrentUserChangeProperty } from '$app/common/hooks/useHandleCurrentUserChange'; -import { useUpdateCompanyUser } from '$app/pages/settings/user/common/hooks/useUpdateCompanyUser'; -import { useCurrentUser } from '$app/common/hooks/useCurrentUser'; import classNames from 'classnames'; import { AboutModal } from './AboutModal'; import { Icon } from './icons/Icon'; @@ -43,6 +38,9 @@ import { useQuery } from 'react-query'; import axios from 'axios'; import { MdWarning } from 'react-icons/md'; import { UpdateAppModal } from './UpdateAppModal'; +import { OpenNavbarArrow } from './icons/OpenNavbarArrow'; +import { useHandleCollapseExpandSidebar } from '$app/common/hooks/useHandleCollapseExpandSidebar'; +import { CloseNavbarArrow } from './icons/CloseNavbarArrow'; interface Props { docsLink?: string; @@ -55,13 +53,11 @@ export function HelpSidebarIcons(props: Props) { const colors = useColorScheme(); const user = useInjectUserChanges(); const account = useCurrentAccount(); - const currentUser = useCurrentUser(); const { mobileNavbar } = props; const dispatch = useDispatch(); - const updateCompanyUser = useUpdateCompanyUser(); - const handleUserChange = useHandleCurrentUserChangeProperty(); + const handleCollapseExpandSidebar = useHandleCollapseExpandSidebar(); const { data: latestVersion } = useQuery({ queryKey: ['/pdf.invoicing.co/api/version'], @@ -129,20 +125,6 @@ export function HelpSidebarIcons(props: Props) { }); }; - useEffect(() => { - const showMiniSidebar = - user?.company_user?.react_settings?.show_mini_sidebar; - - if ( - user && - typeof showMiniSidebar !== 'undefined' && - currentUser?.company_user?.react_settings?.show_mini_sidebar !== - showMiniSidebar - ) { - updateCompanyUser(user); - } - }, [user?.company_user?.react_settings.show_mini_sidebar]); - return ( <> )} - +
); diff --git a/src/components/icons/Check.tsx b/src/components/icons/Check.tsx new file mode 100644 index 0000000000..f16c301b31 --- /dev/null +++ b/src/components/icons/Check.tsx @@ -0,0 +1,36 @@ +/** + * Invoice Ninja (https://invoiceninja.com). + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */ + +type Props = { + color?: string; +}; + +export function Check({ color = '#18181B' }: Props) { + return ( + + + + ); +} diff --git a/src/components/icons/CloseNavbarArrow.tsx b/src/components/icons/CloseNavbarArrow.tsx new file mode 100644 index 0000000000..7aa73e66a0 --- /dev/null +++ b/src/components/icons/CloseNavbarArrow.tsx @@ -0,0 +1,67 @@ +/** + * Invoice Ninja (https://invoiceninja.com). + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */ + +interface Props { + color?: string; + size?: string; +} + +export function CloseNavbarArrow({ + color = '#A1A1AA', + size = '1.3rem', +}: Props) { + return ( + + + + + + ); +} diff --git a/src/components/icons/Exit.tsx b/src/components/icons/Exit.tsx new file mode 100644 index 0000000000..1a40a47244 --- /dev/null +++ b/src/components/icons/Exit.tsx @@ -0,0 +1,35 @@ +/** + * Invoice Ninja (https://invoiceninja.com). + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */ + +export function Exit() { + return ( + + + + + ); +} diff --git a/src/components/icons/ExpandCollapseChevron.tsx b/src/components/icons/ExpandCollapseChevron.tsx new file mode 100644 index 0000000000..0a34b01814 --- /dev/null +++ b/src/components/icons/ExpandCollapseChevron.tsx @@ -0,0 +1,45 @@ +/** + * Invoice Ninja (https://invoiceninja.com). + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */ + +interface Props { + color?: string; +} + +export function ExpandCollapseChevron({ color = '#FFFFFF' }: Props) { + return ( + + + + + ); +} diff --git a/src/components/icons/OpenNavbarArrow.tsx b/src/components/icons/OpenNavbarArrow.tsx new file mode 100644 index 0000000000..e8b65dd447 --- /dev/null +++ b/src/components/icons/OpenNavbarArrow.tsx @@ -0,0 +1,63 @@ +/** + * Invoice Ninja (https://invoiceninja.com). + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */ + +interface Props { + color?: string; + size?: string; +} + +export function OpenNavbarArrow({ color = '#74747C', size = '1.3rem' }: Props) { + return ( + + + + + + ); +} diff --git a/src/components/icons/Person.tsx b/src/components/icons/Person.tsx new file mode 100644 index 0000000000..1067eb617f --- /dev/null +++ b/src/components/icons/Person.tsx @@ -0,0 +1,43 @@ +/** + * Invoice Ninja (https://invoiceninja.com). + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */ + +export function Person() { + return ( + + + + + ); +} diff --git a/src/components/icons/Plus.tsx b/src/components/icons/Plus.tsx new file mode 100644 index 0000000000..9dcc3ec89c --- /dev/null +++ b/src/components/icons/Plus.tsx @@ -0,0 +1,47 @@ +/** + * Invoice Ninja (https://invoiceninja.com). + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */ + +export function Plus() { + return ( + + + + + ); +} diff --git a/src/components/layouts/Default.tsx b/src/components/layouts/Default.tsx index 1bd4d9a567..be77600970 100644 --- a/src/components/layouts/Default.tsx +++ b/src/components/layouts/Default.tsx @@ -62,6 +62,7 @@ import { useSocketEvent } from '$app/common/queries/sockets'; import { Invoice } from '$app/common/interfaces/invoice'; import toast from 'react-hot-toast'; import { EInvoiceCredits } from '../banners/EInvoiceCredits'; +import classNames from 'classnames'; export interface SaveOption { label: string; @@ -422,12 +423,13 @@ export function Default(props: Props) {
+
- {isMiniSidebar ? ( - Company logo - ) : ( - - )} +
-
+