Files
actual/packages/desktop-client/src/components/LoggedInUser.tsx
JuulzGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>github-actions[bot] <github-actions[bot]@users.noreply.github.com>
240d72d060 Update Titlebar colors and bare button theme colors (#8267)
* Update bare button colors to use page color variables

* Refactor button text color variables in dark theme

Updated button text colors to use page text variables.

* Refactor button text color variables in light.css

Updated button text colors to use page text variables.

* Fix indentation for buttonBareText variable

* Update button text colors in midnight theme

* Refactor Titlebar component styles and props

Updated button text color and removed unnecessary color styles.

* Fix button text color styling in LoggedInUser component

* Correct color style in LoggedInUser Button

Fix color style assignment in Button component.

* Remove color prop from LoggedInUser component

Removed the 'color' prop from LoggedInUser component.

* [autofix.ci] apply automated fixes

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #8267

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-19 20:44:33 +00:00

272 lines
7.4 KiB
TypeScript

import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { CSSProperties } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { useLocation } from 'react-router';
import { Button } from '@actual-app/components/button';
import { Menu } from '@actual-app/components/menu';
import { Popover } from '@actual-app/components/popover';
import { styles } from '@actual-app/components/styles';
import { Text } from '@actual-app/components/text';
import { theme } from '@actual-app/components/theme';
import { View } from '@actual-app/components/view';
import { listen } from '@actual-app/core/platform/client/connection';
import type { RemoteFile, SyncedLocalFile } from '@actual-app/core/types/file';
import type { TransObjectLiteral } from '@actual-app/core/types/util';
import { useAuth } from '#auth/AuthProvider';
import { Permissions } from '#auth/types';
import { closeBudget } from '#budgetfiles/budgetfilesSlice';
import { useMetadataPref } from '#hooks/useMetadataPref';
import { useNavigate } from '#hooks/useNavigate';
import { useDispatch, useSelector } from '#redux';
import { getUserData, signOut } from '#users/usersSlice';
import { PrivacyFilter } from './PrivacyFilter';
import { useMultiuserEnabled, useServerURL } from './ServerContext';
type LoggedInUserProps = {
hideIfNoServer?: boolean;
style?: CSSProperties;
};
export function LoggedInUser({ hideIfNoServer, style }: LoggedInUserProps) {
const { t } = useTranslation();
const dispatch = useDispatch();
const navigate = useNavigate();
const userData = useSelector(state => state.user.data);
const [loading, setLoading] = useState(true);
const [menuOpen, setMenuOpen] = useState(false);
const serverUrl = useServerURL();
const triggerRef = useRef<HTMLButtonElement>(null);
const [budgetId] = useMetadataPref('id');
const [cloudFileId] = useMetadataPref('cloudFileId');
const location = useLocation();
const { hasPermission } = useAuth();
const multiuserEnabled = useMultiuserEnabled();
const allFiles = useSelector(state => state.budgetfiles.allFiles || []);
const remoteFiles = allFiles.filter(
f => f.state === 'remote' || f.state === 'synced' || f.state === 'detached',
) as (SyncedLocalFile | RemoteFile)[];
const currentFile = remoteFiles.find(f => f.cloudFileId === cloudFileId);
const hasSyncedPrefs = useSelector(state => state.prefs.synced);
const initializeUserData = useCallback(async () => {
try {
await dispatch(getUserData());
} catch (error) {
console.error('Failed to initialize user data:', error);
} finally {
setLoading(false);
}
}, [dispatch]);
useEffect(() => {
void initializeUserData();
}, [initializeUserData]);
useEffect(() => {
return listen('sync-event', ({ type }) => {
if (type === 'start') {
setLoading(true);
return;
}
const shouldReinitialize =
userData &&
((type === 'success' && userData.offline) ||
(type === 'error' && !userData.offline));
if (shouldReinitialize) {
void initializeUserData();
} else {
setLoading(false);
}
});
}, [initializeUserData, userData]);
async function onCloseBudget() {
await dispatch(closeBudget());
}
async function onChangePassword() {
await onCloseBudget();
void navigate('/change-password');
}
const handleMenuSelect = async (type: string) => {
setMenuOpen(false);
switch (type) {
case 'change-password':
void onChangePassword();
break;
case 'sign-in':
await onCloseBudget();
void navigate('/login');
break;
case 'user-access':
void navigate('/user-access');
break;
case 'user-directory':
void navigate('/user-directory');
break;
case 'index':
void navigate('/');
break;
case 'sign-out':
void dispatch(signOut());
break;
case 'config-server':
await onCloseBudget();
void navigate('/config-server');
break;
default:
break;
}
};
function serverMessage() {
if (!serverUrl) {
return t('No server');
}
if (userData?.offline) {
return t('Server offline');
}
return t('Server online');
}
if (hideIfNoServer && !serverUrl) return null;
if (loading && serverUrl) {
return (
<Text
style={{
color: theme.pageTextLight,
fontStyle: 'italic',
...styles.delayedFadeIn,
...style,
}}
>
<Trans>Connecting...</Trans>
</Text>
);
}
type MenuItem = {
name: string;
text: string;
};
const getMenuItems = (): (MenuItem | typeof Menu.line)[] => {
const isAdmin = hasPermission(Permissions.ADMINISTRATOR);
const baseMenu: (MenuItem | typeof Menu.line)[] = [];
if (
serverUrl &&
!userData?.offline &&
userData?.loginMethod === 'password'
) {
baseMenu.push({ name: 'change-password', text: t('Change password') });
}
if (serverUrl) {
baseMenu.push({ name: 'sign-out', text: t('Sign out') });
}
baseMenu.push({
name: 'config-server',
text: serverUrl ? t('Change server URL') : t('Start using a server'),
});
const adminMenu: (MenuItem | typeof Menu.line)[] = [];
if (multiuserEnabled && isAdmin) {
if (!budgetId && location.pathname !== '/') {
adminMenu.push({ name: 'index', text: t('View file list') });
} else if (
serverUrl &&
!userData?.offline &&
location.pathname !== '/user-directory'
) {
adminMenu.push({ name: 'user-directory', text: t('User Directory') });
}
}
if (
multiuserEnabled &&
((currentFile && userData && currentFile.owner === userData.userId) ||
isAdmin) &&
serverUrl &&
!userData?.offline &&
cloudFileId &&
location.pathname !== '/user-access'
) {
adminMenu.push({
name: 'user-access',
text: t('User Access Management'),
});
}
if (adminMenu.length > 0) {
adminMenu.push(Menu.line);
}
return [...adminMenu, ...baseMenu];
};
return (
<View style={{ flexDirection: 'row', alignItems: 'center', ...style }}>
<Button ref={triggerRef} variant="bare" onPress={() => setMenuOpen(true)}>
{serverMessage()}
</Button>
{!loading &&
multiuserEnabled &&
userData &&
userData?.displayName &&
!hasSyncedPrefs && (
<small>
(
<Trans>
logged in as:{' '}
<span>
{{ userName: userData?.displayName } as TransObjectLiteral}
</span>
</Trans>
)
</small>
)}
{!loading &&
multiuserEnabled &&
userData &&
userData?.displayName &&
hasSyncedPrefs && (
<small>
(
<Trans>
logged in as:{' '}
<span>
<PrivacyFilter>
{{ userName: userData?.displayName } as TransObjectLiteral}
</PrivacyFilter>
</span>
</Trans>
)
</small>
)}
<Popover
offset={8}
triggerRef={triggerRef}
isOpen={menuOpen}
onOpenChange={() => setMenuOpen(false)}
>
<Menu
onMenuSelect={handleMenuSelect}
items={getMenuItems().filter(Boolean)}
/>
</Popover>
</View>
);
}