Skip to content

Commit

Permalink
Merge pull request #2330 from Civolilah/cleanup/1842-company-switcher
Browse files Browse the repository at this point in the history
[Cleanup] Adjusting Company Switcher Per New Design
  • Loading branch information
beganovich authored Feb 17, 2025
2 parents 78b2537 + fbd2306 commit 6ecde2e
Show file tree
Hide file tree
Showing 17 changed files with 598 additions and 142 deletions.
2 changes: 2 additions & 0 deletions src/common/constants/company-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
*/

import DefaultLogo from '../../resources/images/[email protected]';
import SmallLogo from 'public/logo180.png';

export default {
logo: DefaultLogo,
smallLogo: SmallLogo,
};
37 changes: 37 additions & 0 deletions src/common/hooks/useHandleCollapseExpandSidebar.ts
Original file line number Diff line number Diff line change
@@ -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);
}
};
}
2 changes: 1 addition & 1 deletion src/common/hooks/useInjectUserChanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 11 additions & 5 deletions src/common/hooks/useLogo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,21 @@ import { useCompanyChanges } from './useCompanyChanges';
import { useCurrentCompany } from './useCurrentCompany';
import { useTranslation } from 'react-i18next';

export function useLogo() {
interface Params {
fallbackSmallLogo?: boolean;
}

export function useLogo(props?: Params) {
const { fallbackSmallLogo } = props || {};

const companyChanges = useCompanyChanges();
const currentCompany = useCurrentCompany();

return (
companyChanges?.settings?.company_logo ||
return companyChanges?.settings?.company_logo ||
currentCompany?.settings?.company_logo ||
companySettings.logo
);
fallbackSmallLogo
? companySettings.smallLogo
: companySettings.logo;
}

export function useCompanyName() {
Expand Down
225 changes: 162 additions & 63 deletions src/components/CompanySwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { 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';
import companySettings from '$app/common/constants/company-settings';

const SwitcherDiv = styled.div`
&:hover {
background-color: ${(props) => props.theme.hoverColor};
}
`;

export function CompanySwitcher() {
const [t] = useTranslation();
Expand All @@ -43,15 +53,23 @@ export function CompanySwitcher() {
const { id } = useParams();
const canUserAddCompany = isSelfHosted() || (isHosted() && !freePlan());

const logo = useLogo();
const user = useCurrentUser();
const logo = useLogo({ fallbackSmallLogo: true });
const location = useLocation();
const colors = useColorScheme();
const companyName = useCompanyName();
const queryClient = useQueryClient();
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<boolean>(false);
const [isCompanyCreateModalOpened, setIsCompanyCreateModalOpened] =
Expand Down Expand Up @@ -97,6 +115,22 @@ export function CompanySwitcher() {
}
}, [currentCompany]);

if (isMiniSidebar) {
return (
<>
<img
className="rounded-full border overflow-hidden aspect-square object-cover"
src={logo}
alt="Company logo"
style={{
borderColor: '#e5e7eb',
width: '1.66rem',
}}
/>
</>
);
}

return (
<>
<CompanyCreate
Expand All @@ -109,21 +143,24 @@ export function CompanySwitcher() {
className="relative inline-block text-left w-full"
data-cy="companyDropdown"
>
<Menu.Button className="flex items-center justify-between w-full rounded font-medium pl-2">
<div className="flex items-center justify-center space-x-3">
<img className="w-8" src={logo} alt="Company logo" />
<div className="flex flex-col items-between">
<span className="text-sm text-start w-28 truncate">
{companyName}
</span>
{(user?.first_name || user?.last_name) && (
<span className="text-xs text-start w-28 truncate">
{user.first_name} {user.last_name}
</span>
)}
</div>
<Menu.Button className="flex items-center justify-start space-x-3 w-full">
<div className="flex items-center space-x-3 p-1.5 rounded-md hover:bg-gray-700">
<img
className="rounded-full border overflow-hidden aspect-square object-cover"
src={logo}
alt="Company logo"
style={{
borderColor: '#e5e7eb',
width: '1.65rem',
}}
/>

<span className="text-sm text-start w-36 truncate text-gray-200">
{companyName}
</span>

<ExpandCollapseChevron color="#e5e7eb" />
</div>
<ChevronDown size={18} className="text-gray-300" />
</Menu.Button>

<Transition
Expand All @@ -136,71 +173,133 @@ export function CompanySwitcher() {
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
style={{ backgroundColor: colors.$1, borderColor: colors.$4 }}
className="border origin-top-right absolute left-0 mt-2 w-56 rounded shadow-lg"
className="origin-top-right absolute left-0 mt-2 rounded shadow-lg border"
style={{
backgroundColor: colors.$1,
width: '14.5rem',
borderColor: colors.$4,
}}
>
<div className="py-1">
<div className="border-b" style={{ borderColor: colors.$4 }}>
<Menu.Item>
<DropdownElement>
<p className="text-sm">{t('signed_in_as')}</p>
<p className="text-sm font-medium truncate">{user?.email}</p>
</DropdownElement>
<div className="px-3 pb-1.5 pt-2">
<p className="text-xs text-gray-500">{t('signed_in_as')}</p>

<p className="font-medium truncate text-sm">
{currentUser?.email}
</p>
</div>
</Menu.Item>
</div>

<div className="py-1">
<div
className="flex flex-col pb-1 pt-2 border-b"
style={{ borderColor: colors.$4 }}
>
{state?.api?.length >= 1 &&
state?.api?.map((record: any, index: number) => (
<Menu.Item key={index}>
<DropdownElement
actionKey="switchCompany"
onClick={() => switchCompany(index)}
>
<div className="flex items-center space-x-3">
<span>
{record.company.settings.name ||
t('untitled_company')}
</span>

{state.currentIndex === index && <Check size={18} />}
</div>
</DropdownElement>
<div className="px-1 space-y-0.5">
{index === 0 && (
<p className="pl-2 text-xs text-gray-500">
{t('company')}
</p>
)}

<SwitcherDiv
className="flex items-center px-2 justify-between py-1.5 rounded-md cursor-pointer"
theme={{ hoverColor: colors.$5 }}
onClick={() =>
preventNavigation({
fn: () => switchCompany(index),
actionKey: 'switchCompany',
})
}
>
<div className="flex items-center space-x-2 flex-1">
<img
className="rounded-full border overflow-hidden aspect-square object-cover"
src={
record.company.settings.company_logo ||
companySettings.smallLogo
}
alt="Company logo"
style={{
borderColor: colors.$5,
width: '1.5rem',
}}
/>

<div className="w-36 truncate text-sm">
{record.company.settings.name ||
t('untitled_company')}
</div>
</div>

{state.currentIndex === index && (
<Check color={colors.$3} />
)}
</SwitcherDiv>
</div>
</Menu.Item>
))}
</div>

<div className="py-1">
{shouldShowAddCompany &&
canUserAddCompany &&
(isAdmin || isOwner) && (
<Menu.Item>
<DropdownElement
className="flex items-center"
onClick={() => setIsCompanyCreateModalOpened(true)}
icon={<Icon element={BiPlusCircle} size={22} />}
>
<span>{t('add_company')}</span>
</DropdownElement>
<div className="px-1">
<SwitcherDiv
className="flex items-center pl-3 space-x-3 py-2 rounded-md cursor-pointer"
theme={{ hoverColor: colors.$5 }}
onClick={() => setIsCompanyCreateModalOpened(true)}
>
<Plus />

<span className="text-sm">{t('add_company')}</span>
</SwitcherDiv>
</div>
</Menu.Item>
)}

{(isAdmin || isOwner) && (
<Menu.Item>
<DropdownElement
to="/settings/account_management"
icon={<Icon element={MdManageAccounts} size={22} />}
>
{t('account_management')}
</DropdownElement>
<div className="px-1">
<SwitcherDiv
className="flex items-center space-x-3 pl-3 py-2 rounded-md cursor-pointer"
theme={{ hoverColor: colors.$5 }}
onClick={() =>
preventNavigation({
url: '/settings/account_management',
})
}
>
<Person />

<span className="text-sm">{t('account_management')}</span>
</SwitcherDiv>
</div>
</Menu.Item>
)}

<Menu.Item>
<DropdownElement
to="/logout"
icon={<Icon element={MdLogout} size={22} />}
>
{t('logout')}
</DropdownElement>
<div className="pl-1.5 pr-1">
<SwitcherDiv
className="flex items-center space-x-3 pl-3 py-2 rounded-md cursor-pointer"
theme={{ hoverColor: colors.$5 }}
onClick={() =>
preventNavigation({
url: '/logout',
})
}
>
<Exit />

<span className="text-sm">{t('logout')}</span>
</SwitcherDiv>
</div>
</Menu.Item>
</div>
</Menu.Items>
Expand Down
Loading

0 comments on commit 6ecde2e

Please sign in to comment.