-
Joel Jeremy Marquez authored
[React Aria Button] All remaining components except filters, mobile transaction edit, and autocomplete (#3163) * vrt * Fix account menu test * Fix payee icons and category notes * vrt * vrt * Fix notes button * VRT * VRT * React Aria Button - All except filters and autocomplete * Release notes * Fix typecheck errors * Fix typecheck error * Fix button links * Update * Fix typecheck error * Fix link button variant * Fix typecheck error * Fix typecheck error * Fix typecheck error * Fix border * Fix mobile accounts page * VRT * Fix playwright config * Revert MobileForms and TransactionEdit * VRT * Remove borderRadius * Fix add account button * VRT * Revert VRT * Fix tests * Fix Cover and Transfer modals * Fix lint error
Joel Jeremy Marquez authored[React Aria Button] All remaining components except filters, mobile transaction edit, and autocomplete (#3163) * vrt * Fix account menu test * Fix payee icons and category notes * vrt * vrt * Fix notes button * VRT * VRT * React Aria Button - All except filters and autocomplete * Release notes * Fix typecheck errors * Fix typecheck error * Fix button links * Update * Fix typecheck error * Fix link button variant * Fix typecheck error * Fix typecheck error * Fix typecheck error * Fix border * Fix mobile accounts page * VRT * Fix playwright config * Revert MobileForms and TransactionEdit * VRT * Remove borderRadius * Fix add account button * VRT * Revert VRT * Fix tests * Fix Cover and Transfer modals * Fix lint error
Titlebar.tsx 9.10 KiB
import React, { useState, useEffect } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { Routes, Route, useLocation } from 'react-router-dom';
import * as Platform from 'loot-core/src/client/platform';
import * as queries from 'loot-core/src/client/queries';
import { listen } from 'loot-core/src/platform/client/fetch';
import { isDevelopmentEnvironment } from 'loot-core/src/shared/environment';
import { useActions } from '../hooks/useActions';
import { useGlobalPref } from '../hooks/useGlobalPref';
import { useMetadataPref } from '../hooks/useMetadataPref';
import { useNavigate } from '../hooks/useNavigate';
import { useSyncedPref } from '../hooks/useSyncedPref';
import { SvgArrowLeft } from '../icons/v1';
import {
SvgAlertTriangle,
SvgNavigationMenu,
SvgViewHide,
SvgViewShow,
} from '../icons/v2';
import { useResponsive } from '../ResponsiveProvider';
import { theme, type CSSProperties, styles } from '../style';
import { AccountSyncCheck } from './accounts/AccountSyncCheck';
import { AnimatedRefresh } from './AnimatedRefresh';
import { MonthCountSelector } from './budget/MonthCountSelector';
import { Button } from './common/Button2';
import { Link } from './common/Link';
import { Text } from './common/Text';
import { View } from './common/View';
import { LoggedInUser } from './LoggedInUser';
import { useServerURL } from './ServerContext';
import { useSidebar } from './sidebar/SidebarProvider';
import { useSheetValue } from './spreadsheet/useSheetValue';
import { ThemeSelector } from './ThemeSelector';
function UncategorizedButton() {
const count: number | null = useSheetValue(queries.uncategorizedCount());
if (count === null || count <= 0) {
return null;
}
return (
<Link
variant="button"
buttonVariant="bare"
to="/accounts/uncategorized"
style={{
color: theme.errorText,
}}
>
{count} uncategorized {count === 1 ? 'transaction' : 'transactions'}
</Link>
);
}
type PrivacyButtonProps = {
style?: CSSProperties;
};
function PrivacyButton({ style }: PrivacyButtonProps) {
const [isPrivacyEnabledPref, setPrivacyEnabledPref] =
useSyncedPref('isPrivacyEnabled');
const isPrivacyEnabled = String(isPrivacyEnabledPref) === 'true';
const privacyIconStyle = { width: 15, height: 15 };
useHotkeys(
'shift+ctrl+p, shift+cmd+p, shift+meta+p',
() => {
setPrivacyEnabledPref(String(!isPrivacyEnabled));
},
{
preventDefault: true,
scopes: ['app'],
},
[setPrivacyEnabledPref, isPrivacyEnabled],
);
return (
<Button
variant="bare"
aria-label={`${isPrivacyEnabled ? 'Disable' : 'Enable'} privacy mode`}
onPress={() => setPrivacyEnabledPref(String(!isPrivacyEnabled))}
style={style}
>
{isPrivacyEnabled ? (
<SvgViewHide style={privacyIconStyle} />
) : (
<SvgViewShow style={privacyIconStyle} />
)}
</Button>
);
}
type SyncButtonProps = {
style?: CSSProperties;
isMobile?: boolean;
};
function SyncButton({ style, isMobile = false }: SyncButtonProps) {
const [cloudFileId] = useMetadataPref('cloudFileId');
const { sync } = useActions();
const [syncing, setSyncing] = useState(false);
const [syncState, setSyncState] = useState<
null | 'offline' | 'local' | 'disabled' | 'error'
>(null);
useEffect(() => {
const unlisten = listen('sync-event', ({ type, subtype, syncDisabled }) => {
if (type === 'start') {
setSyncing(true);
setSyncState(null);
} else {
// Give the layout some time to apply the starting animation
// so we always finish it correctly even if it's almost
// instant
setTimeout(() => {
setSyncing(false);
}, 200);
}
if (type === 'error') {
// Use the offline state if either there is a network error or
// if this file isn't a "cloud file". You can't sync a local
// file.
if (subtype === 'network') {
setSyncState('offline');
} else if (!cloudFileId) {
setSyncState('local');
} else {
setSyncState('error');
}
} else if (type === 'success') {
setSyncState(syncDisabled ? 'disabled' : null);
}
});
return unlisten;
}, []);
const mobileColor =
syncState === 'error'
? theme.errorText
: syncState === 'disabled' ||
syncState === 'offline' ||
syncState === 'local'
? theme.mobileHeaderTextSubdued
: theme.mobileHeaderText;
const desktopColor =
syncState === 'error'
? theme.errorTextDark
: syncState === 'disabled' ||
syncState === 'offline' ||
syncState === 'local'
? theme.tableTextLight
: 'inherit';
const activeStyle = isMobile
? {
color: mobileColor,
}
: {};
const hoveredStyle = isMobile
? {
color: mobileColor,
background: theme.mobileHeaderTextHover,
}
: {};
const mobileIconStyle = {
color: mobileColor,
justifyContent: 'center',
margin: 10,
paddingLeft: 5,
paddingRight: 3,
};
const mobileTextStyle = {
...styles.text,
fontWeight: 500,
marginLeft: 2,
marginRight: 5,
};
useHotkeys(
'ctrl+s, cmd+s, meta+s',
sync,
{
enableOnFormTags: true,
preventDefault: true,
scopes: ['app'],
},
[sync],
);
return (
<Button
variant="bare"
aria-label="Sync"
style={({ isHovered, isPressed }) => ({
...(isMobile
? {
...style,
WebkitAppRegion: 'none',
...mobileIconStyle,
}
: {
...style,
WebkitAppRegion: 'none',
color: desktopColor,
}),
...(isHovered ? hoveredStyle : {}),
...(isPressed ? activeStyle : {}),
})}
onPress={sync}
>
{isMobile ? (
syncState === 'error' ? (
<SvgAlertTriangle width={14} height={14} />
) : (
<AnimatedRefresh width={18} height={18} animating={syncing} />
)
) : syncState === 'error' ? (
<SvgAlertTriangle width={13} />
) : (
<AnimatedRefresh animating={syncing} />
)}
<Text style={isMobile ? { ...mobileTextStyle } : { marginLeft: 3 }}>
{syncState === 'disabled'
? 'Disabled'
: syncState === 'offline'
? 'Offline'
: 'Sync'}
</Text>
</Button>
);
}
function BudgetTitlebar() {
const [maxMonths, setMaxMonthsPref] = useGlobalPref('maxMonths');
return (
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<MonthCountSelector
maxMonths={maxMonths || 1}
onChange={value => setMaxMonthsPref(value)}
/>
</View>
);
}
type TitlebarProps = {
style?: CSSProperties;
};
export function Titlebar({ style }: TitlebarProps) {
const navigate = useNavigate();
const location = useLocation();
const sidebar = useSidebar();
const { isNarrowWidth } = useResponsive();
const serverURL = useServerURL();
const [floatingSidebar] = useGlobalPref('floatingSidebar');
return isNarrowWidth ? null : (
<View
style={{
flexDirection: 'row',
alignItems: 'center',
padding: '0 15px',
height: 36,
pointerEvents: 'none',
'& *': {
pointerEvents: 'auto',
},
...(!Platform.isBrowser &&
Platform.OS === 'mac' &&
floatingSidebar && { paddingLeft: 80 }),
...style,
}}
>
{(floatingSidebar || sidebar.alwaysFloats) && (
<Button
aria-label="Sidebar menu"
variant="bare"
style={{ marginRight: 8 }}
onHoverStart={e => {
if (e.pointerType === 'mouse') {
sidebar.setHidden(false);
}
}}
onPress={e => {
if (e.pointerType !== 'mouse') {
sidebar.setHidden(!sidebar.hidden);
}
}}
>
<SvgNavigationMenu
className="menu"
style={{ width: 15, height: 15, color: theme.pageText, left: 0 }}
/>
</Button>
)}
<Routes>
<Route
path="/accounts"
element={
location.state?.goBack ? (
<Button variant="bare" onPress={() => navigate(-1)}>
<SvgArrowLeft
width={10}
height={10}
style={{ marginRight: 5, color: 'currentColor' }}
/>{' '}
Back
</Button>
) : null
}
/>
<Route path="/accounts/:id" element={<AccountSyncCheck />} />
<Route path="/budget" element={<BudgetTitlebar />} />
<Route path="*" element={null} />
</Routes>
<View style={{ flex: 1 }} />
<UncategorizedButton />
{isDevelopmentEnvironment() && !Platform.isPlaywright && (
<ThemeSelector style={{ marginLeft: 10 }} />
)}
<PrivacyButton style={{ marginLeft: 10 }} />
{serverURL ? <SyncButton style={{ marginLeft: 10 }} /> : null}
<LoggedInUser style={{ marginLeft: 10 }} />
</View>
);
}