-
Matiss Janis Aboltins authored
Just moving away from the barrel `common` export to a bit more specific imports. Part 1 of the migration (other imports to follow in other PRs).
Matiss Janis Aboltins authoredJust moving away from the barrel `common` export to a bit more specific imports. Part 1 of the migration (other imports to follow in other PRs).
sidebar.js 14.79 KiB
import React, { useState, useMemo, useCallback, useEffect } from 'react';
import { useLocation } from 'react-router';
import { css } from 'glamor';
import * as Platform from 'loot-core/src/client/platform';
import Add from '../icons/v1/Add';
import CheveronDown from '../icons/v1/CheveronDown';
import CheveronRight from '../icons/v1/CheveronRight';
import Cog from '../icons/v1/Cog';
import Pin from '../icons/v1/Pin';
import Reports from '../icons/v1/Reports';
import StoreFrontIcon from '../icons/v1/StoreFront';
import TuningIcon from '../icons/v1/Tuning';
import Wallet from '../icons/v1/Wallet';
import ArrowButtonLeft1 from '../icons/v2/ArrowButtonLeft1';
import CalendarIcon from '../icons/v2/Calendar';
import { styles, colors } from '../style';
import AlignedText from './common/AlignedText';
import AnchorLink from './common/AnchorLink';
import Block from './common/Block';
import Button from './common/Button';
import View from './common/View';
import { useSidebar } from './FloatableSidebar';
import { useDraggable, useDroppable, DropHighlight } from './sort';
import CellValue from './spreadsheet/CellValue';
export const SIDEBAR_WIDTH = 240;
const fontWeight = 600;
function ItemContent({
style,
to,
onClick,
activeStyle,
forceActive,
children,
}) {
return onClick ? (
<View
role="button"
tabIndex={0}
style={[
style,
{
touchAction: 'auto',
userSelect: 'none',
userDrag: 'none',
cursor: 'pointer',
...(forceActive ? activeStyle : {}),
},
]}
onClick={onClick}
>
{children}
</View>
) : (
<AnchorLink
to={to}
style={style}
activeStyle={activeStyle}
forceActive={forceActive}
>
{children}
</AnchorLink>
);
}
function Item({
children,
Icon,
title,
style,
indent = 0,
to,
onClick,
button,
forceHover = false,
forceActive = false,
}) {
const hoverStyle = {
backgroundColor: colors.n2,
};
const activeStyle = {
borderLeft: '4px solid ' + colors.p8,
paddingLeft: 19 + indent - 4,
color: colors.p8,
};
const linkStyle = [
{
...styles.mediumText,
paddingTop: 9,
paddingBottom: 9,
paddingLeft: 19 + indent,
paddingRight: 10,
textDecoration: 'none',
color: colors.n9,
...(forceHover ? hoverStyle : {}),
},
{ ':hover': hoverStyle },
];
const content = (
<View
style={{
flexDirection: 'row',
alignItems: 'center',
height: 20,
}}
>
<Icon width={15} height={15} />
<Block style={{ marginLeft: 8 }}>{title}</Block>
<View style={{ flex: 1 }} />
{button}
</View>
);
return (
<View style={[{ flexShrink: 0 }, style]}>
<ItemContent
style={linkStyle}
to={to}
onClick={onClick}
activeStyle={activeStyle}
forceActive={forceActive}
>
{content}
</ItemContent>
{children ? <View style={{ marginTop: 5 }}>{children}</View> : null}
</View>
);
}
function SecondaryItem({ Icon, title, style, to, onClick, bold, indent = 0 }) {
const hoverStyle = {
backgroundColor: colors.n2,
};
const activeStyle = {
borderLeft: '4px solid ' + colors.p8,
paddingLeft: 14 - 4 + indent,
color: colors.p8,
fontWeight: bold ? fontWeight : null,
};
const linkStyle = [
accountNameStyle,
{
color: colors.n9,
paddingLeft: 14 + indent,
fontWeight: bold ? fontWeight : null,
},
{ ':hover': hoverStyle },
];
const content = (
<View
style={{
flexDirection: 'row',
alignItems: 'center',
height: 16,
}}
>
{Icon && <Icon width={12} height={12} />}
<Block style={{ marginLeft: Icon ? 8 : 0, color: 'inherit' }}>
{title}
</Block>
</View>
);
return (
<View style={[{ flexShrink: 0 }, style]}>
<ItemContent
style={linkStyle}
to={to}
onClick={onClick}
activeStyle={activeStyle}
>
{content}
</ItemContent>
</View>
);
}
let accountNameStyle = [
{
marginTop: -2,
marginBottom: 2,
paddingTop: 4,
paddingBottom: 4,
paddingRight: 15,
paddingLeft: 10,
textDecoration: 'none',
color: colors.n9,
},
{ ':hover': { backgroundColor: colors.n2 } },
styles.smallText,
];
function Account({
name,
account,
connected,
failed,
updated,
to,
query,
style,
outerStyle,
onDragChange,
onDrop,
}) {
let type = account
? account.closed
? 'account-closed'
: account.offbudget
? 'account-offbudget'
: 'account-onbudget'
: 'title';
let { dragRef } = useDraggable({
type,
onDragChange,
item: { id: account && account.id },
canDrag: account != null,
});
let { dropRef, dropPos } = useDroppable({
types: account ? [type] : [],
id: account && account.id,
onDrop: onDrop,
});
return (
<View innerRef={dropRef} style={[{ flexShrink: 0 }, outerStyle]}>
<View>
<DropHighlight pos={dropPos} />
<View innerRef={dragRef}>
<AnchorLink
to={to}
style={[
accountNameStyle,
style,
{ position: 'relative', borderLeft: '4px solid transparent' },
updated && { fontWeight: 700 },
]}
activeStyle={{
borderColor: colors.p8,
color: colors.p8,
// This is kind of a hack, but we don't ever want the account
// that the user is looking at to be "bolded" which means it
// has unread transactions. The system does mark is read and
// unbolds it, but it still "flashes" bold so this just
// ignores it if it's active
fontWeight: (style && style.fontWeight) || 'normal',
'& .dot': {
backgroundColor: colors.p8,
transform: 'translateX(-4.5px)',
},
}}
>
<View
style={{
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
flexDirection: 'row',
alignItems: 'center',
}}
>
<div
className="dot"
{...css({
marginRight: 3,
width: 5,
height: 5,
borderRadius: 5,
backgroundColor: failed ? colors.r7 : colors.g5,
marginLeft: 2,
transition: 'transform .3s',
opacity: connected ? 1 : 0,
})}
/>
</View>
<AlignedText
left={name}
right={
<CellValue debug={true} binding={query} type="financial" />
}
/>
</AnchorLink>
</View>
</View>
</View>
);
}
function Accounts({
accounts,
failedAccounts,
updatedAccounts,
getAccountPath,
allAccountsPath,
budgetedAccountPath,
offBudgetAccountPath,
getBalanceQuery,
getAllAccountBalance,
getOnBudgetBalance,
getOffBudgetBalance,
showClosedAccounts,
onAddAccount,
onToggleClosedAccounts,
onReorder,
}) {
let [isDragging, setIsDragging] = useState(false);
let offbudgetAccounts = useMemo(
() =>
accounts.filter(
account => account.closed === 0 && account.offbudget === 1,
),
[accounts],
);
let budgetedAccounts = useMemo(
() =>
accounts.filter(
account => account.closed === 0 && account.offbudget === 0,
),
[accounts],
);
let closedAccounts = useMemo(
() => accounts.filter(account => account.closed === 1),
[accounts],
);
function onDragChange(drag) {
setIsDragging(drag.state === 'start');
}
let makeDropPadding = (i, length) => {
if (i === 0) {
return {
paddingTop: isDragging ? 15 : 0,
marginTop: isDragging ? -15 : 0,
};
}
return null;
};
return (
<View>
<Account
name="All accounts"
to={allAccountsPath}
query={getAllAccountBalance()}
style={{ fontWeight, marginTop: 15 }}
/>
{budgetedAccounts.length > 0 && (
<Account
name="For budget"
to={budgetedAccountPath}
query={getOnBudgetBalance()}
style={{ fontWeight, marginTop: 13 }}
/>
)}
{budgetedAccounts.map((account, i) => (
<Account
key={account.id}
name={account.name}
account={account}
connected={!!account.bankId}
failed={failedAccounts && failedAccounts.has(account.id)}
updated={updatedAccounts && updatedAccounts.includes(account.id)}
to={getAccountPath(account)}
query={getBalanceQuery(account)}
onDragChange={onDragChange}
onDrop={onReorder}
outerStyle={makeDropPadding(i, budgetedAccounts.length)}
/>
))}
{offbudgetAccounts.length > 0 && (
<Account
name="Off budget"
to={offBudgetAccountPath}
query={getOffBudgetBalance()}
style={{ fontWeight, marginTop: 13 }}
/>
)}
{offbudgetAccounts.map((account, i) => (
<Account
key={account.id}
name={account.name}
account={account}
connected={!!account.bankId}
failed={failedAccounts && failedAccounts.has(account.id)}
updated={updatedAccounts && updatedAccounts.includes(account.id)}
to={getAccountPath(account)}
query={getBalanceQuery(account)}
onDragChange={onDragChange}
onDrop={onReorder}
outerStyle={makeDropPadding(i, offbudgetAccounts.length)}
/>
))}
{closedAccounts.length > 0 && (
<SecondaryItem
style={{ marginTop: 15 }}
title={'Closed accounts' + (showClosedAccounts ? '' : '...')}
onClick={onToggleClosedAccounts}
bold
/>
)}
{showClosedAccounts &&
closedAccounts.map((account, i) => (
<Account
key={account.id}
name={account.name}
account={account}
to={getAccountPath(account)}
query={getBalanceQuery(account)}
onDragChange={onDragChange}
onDrop={onReorder}
/>
))}
<SecondaryItem
style={{
marginTop: 15,
marginBottom: 9,
}}
onClick={onAddAccount}
Icon={Add}
title="Add account"
/>
</View>
);
}
function ToggleButton({ style, isFloating, onFloat }) {
return (
<View
className="float"
style={[style, { flexShrink: 0, color: colors.n5 }]}
>
<Button type="bare" onClick={onFloat}>
{isFloating ? (
<Pin
style={{
margin: -2,
width: 15,
height: 15,
transform: 'rotate(45deg)',
}}
/>
) : (
<ArrowButtonLeft1 style={{ width: 13, height: 13 }} />
)}
</Button>
</View>
);
}
function Tools() {
let [isOpen, setOpen] = useState(false);
let onToggle = useCallback(() => setOpen(open => !open), []);
let location = useLocation();
const isActive = ['/payees', '/rules', '/settings', '/tools'].some(route =>
location.pathname.startsWith(route),
);
useEffect(() => {
if (isActive) {
setOpen(true);
}
}, [location.pathname]);
return (
<View style={{ flexShrink: 0 }}>
<Item
title="More"
Icon={isOpen ? CheveronDown : CheveronRight}
onClick={onToggle}
style={{ marginBottom: isOpen ? 8 : 0 }}
forceActive={!isOpen && isActive}
/>
{isOpen && (
<>
<SecondaryItem
title="Payees"
Icon={StoreFrontIcon}
to="/payees"
indent={15}
/>
<SecondaryItem
title="Rules"
Icon={TuningIcon}
to="/rules"
indent={15}
/>
<SecondaryItem
title="Settings"
Icon={Cog}
to="/settings"
indent={15}
/>
</>
)}
</View>
);
}
export function Sidebar({
style,
budgetName,
accounts,
failedAccounts,
updatedAccounts,
getBalanceQuery,
getAllAccountBalance,
getOnBudgetBalance,
getOffBudgetBalance,
showClosedAccounts,
isFloating,
onFloat,
onAddAccount,
onToggleClosedAccounts,
onReorder,
}) {
let hasWindowButtons = !Platform.isBrowser && Platform.OS === 'mac';
const sidebar = useSidebar();
return (
<View
style={[
{
width: SIDEBAR_WIDTH,
color: colors.n9,
backgroundColor: colors.n1,
'& .float': {
opacity: isFloating ? 1 : 0,
transition: 'opacity .25s, width .25s',
width: hasWindowButtons || isFloating ? null : 0,
},
'&:hover .float': {
opacity: 1,
width: hasWindowButtons ? null : 'auto',
},
},
style,
]}
>
<View
style={[
{
paddingTop: 35,
height: 30,
flexDirection: 'row',
alignItems: 'center',
margin: '0 8px 23px 20px',
transition: 'padding .4s',
},
hasWindowButtons && {
paddingTop: 20,
justifyContent: 'flex-start',
},
]}
>
{budgetName}
<View style={{ flex: 1, flexDirection: 'row' }} />
{!sidebar.alwaysFloats && (
<ToggleButton isFloating={isFloating} onFloat={onFloat} />
)}
</View>
<View style={{ overflow: 'auto' }}>
<Item title="Budget" Icon={Wallet} to="/budget" />
<Item title="Reports" Icon={Reports} to="/reports" />
<Item title="Schedules" Icon={CalendarIcon} to="/schedules" />
<Tools />
<View
style={{
height: 1,
backgroundColor: colors.n3,
marginTop: 15,
flexShrink: 0,
}}
/>
<Accounts
accounts={accounts}
failedAccounts={failedAccounts}
updatedAccounts={updatedAccounts}
getAccountPath={account => `/accounts/${account.id}`}
allAccountsPath="/accounts"
budgetedAccountPath="/accounts/budgeted"
offBudgetAccountPath="/accounts/offbudget"
getBalanceQuery={getBalanceQuery}
getAllAccountBalance={getAllAccountBalance}
getOnBudgetBalance={getOnBudgetBalance}
getOffBudgetBalance={getOffBudgetBalance}
showClosedAccounts={showClosedAccounts}
onAddAccount={onAddAccount}
onToggleClosedAccounts={onToggleClosedAccounts}
onReorder={onReorder}
/>
</View>
</View>
);
}