-
Matiss Janis Aboltins authored
*
enabling no-unused-vars eslint rule * fix more lint issuesMatiss Janis Aboltins authored*
enabling no-unused-vars eslint rule * fix more lint issues
MobileTransaction.js 12.67 KiB
import React, { useEffect } from 'react';
import { useFocusRing } from '@react-aria/focus';
import { useListBox, useListBoxSection, useOption } from '@react-aria/listbox';
import { mergeProps } from '@react-aria/utils';
import { Item, Section } from '@react-stately/collections';
import { useListState } from '@react-stately/list';
import memoizeOne from 'memoize-one';
import * as monthUtils from 'loot-core/src/shared/months';
import { getScheduledAmount } from 'loot-core/src/shared/schedules';
import { titleFirst } from 'loot-core/src/shared/util';
import { integerToCurrency, groupById } from 'loot-core/src/shared/util';
import { Text, TextOneLine, View } from 'loot-design/src/components/common';
import { styles, colors } from 'loot-design/src/style';
import ArrowsSynchronize from 'loot-design/src/svg/v2/ArrowsSynchronize';
import CheckCircle1 from 'loot-design/src/svg/v2/CheckCircle1';
const zIndices = { SECTION_HEADING: 10 };
let getPayeesById = memoizeOne(payees => groupById(payees));
let getAccountsById = memoizeOne(accounts => groupById(accounts));
export function isPreviewId(id) {
return id.indexOf('preview/') !== -1;
}
function getDescriptionPretty(transaction, payee, transferAcct) {
let { amount } = transaction;
if (transferAcct) {
return `Transfer ${amount > 0 ? 'from' : 'to'} ${transferAcct.name}`;
} else if (payee) {
return payee.name;
}
return '';
}
function lookupName(items, id) {
return items.find(item => item.id === id).name;
}
export function DateHeader({ date }) {
return (
<ListItem
style={{
height: 25,
backgroundColor: colors.n10,
borderColor: colors.n9,
justifyContent: 'center'
}}
>
<Text style={[styles.text, { fontSize: 13, color: colors.n4 }]}>
{monthUtils.format(date, 'MMMM dd, yyyy')}
</Text>
</ListItem>
);
}
function Status({ status }) {
let color;
switch (status) {
case 'missed':
color = colors.r3;
break;
case 'due':
color = colors.y3;
break;
case 'upcoming':
color = colors.n4;
break;
default:
}
return (
<Text
style={{
fontSize: 11,
color,
fontStyle: 'italic'
}}
>
{titleFirst(status)}
</Text>
);
}
export class Transaction extends React.PureComponent {
render() {
const {
transaction,
accounts,
categories,
payees,
showCategory,
added,
// onSelect,
style
} = this.props;
let {
id,
payee: payeeId,
amount,
category,
cleared,
is_parent,
notes,
schedule
} = transaction;
if (isPreviewId(id)) {
amount = getScheduledAmount(amount);
}
let categoryName = category ? lookupName(categories, category) : null;
let payee = payees && payeeId && getPayeesById(payees)[payeeId];
let transferAcct =
payee &&
payee.transfer_acct &&
getAccountsById(accounts)[payee.transfer_acct];
let prettyDescription = getDescriptionPretty(
transaction,
payee,
transferAcct
);
let prettyCategory = transferAcct
? 'Transfer'
: is_parent
? 'Split'
: categoryName;
let isPreview = isPreviewId(id);
let textStyle = isPreview && {
fontStyle: 'italic',
color: colors.n5
};
return (
// <Button
// onClick={() => onSelect(transaction)}
// style={{
// backgroundColor: 'white',
// border: 'none',
// width: '100%',
// '&:active': { opacity: 0.1 }
// }}
// >
<ListItem
style={[
{ flex: 1, height: 60, padding: '5px 10px' }, // remove padding when Button is back
isPreview && { backgroundColor: colors.n11 },
style
]}
>
<View style={[{ flex: 1 }]}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
{schedule && (
<ArrowsSynchronize
style={{
width: 12,
height: 12,
marginRight: 5,
color: textStyle.color || colors.n1
}}
/>
)}
<TextOneLine
style={[
styles.text,
textStyle,
{ fontSize: 14, fontWeight: added ? '600' : '400' },
prettyDescription === '' && {
color: colors.n6,
fontStyle: 'italic'
}
]}
>
{prettyDescription || 'Empty'}
</TextOneLine>
</View>
{isPreview ? (
<Status status={notes} />
) : (
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginTop: 3
}}
>
<CheckCircle1
style={{
width: 11,
height: 11,
color: cleared ? colors.g6 : colors.n8,
marginRight: 5
}}
/>
{showCategory && (
<TextOneLine
style={{
fontSize: 11,
marginTop: 1,
fontWeight: '400',
color: prettyCategory ? colors.n3 : colors.p7,
fontStyle: prettyCategory ? null : 'italic',
textAlign: 'left'
}}
>
{prettyCategory || 'Uncategorized'}
</TextOneLine>
)}
</View>
)}
</View>
<Text
style={[
styles.text,
textStyle,
{ marginLeft: 25, marginRight: 5, fontSize: 14 }
]}
>
{integerToCurrency(amount)}
</Text>
</ListItem>
// </Button>
);
}
}
export class TransactionList extends React.Component {
makeData = memoizeOne(transactions => {
// Group by date. We can assume transactions is ordered
const sections = [];
transactions.forEach(transaction => {
if (
sections.length === 0 ||
transaction.date !== sections[sections.length - 1].date
) {
// Mark the last transaction in the section so it can render
// with a different border
let lastSection = sections[sections.length - 1];
if (lastSection && lastSection.data.length > 0) {
let lastData = lastSection.data;
lastData[lastData.length - 1].isLast = true;
}
sections.push({
id: transaction.date,
date: transaction.date,
data: []
});
}
if (!transaction.is_child) {
sections[sections.length - 1].data.push(transaction);
}
});
return sections;
});
render() {
const {
transactions,
scrollProps = {},
onLoadMore
// refreshControl
} = this.props;
const sections = this.makeData(transactions);
return (
<>
{scrollProps.ListHeaderComponent}
<ListBox
{...scrollProps}
aria-label="transaction list"
label=""
loadMore={onLoadMore}
selectionMode="none"
style={{ flex: '1 auto', height: '100%', overflowY: 'auto' }}
>
{sections.length === 0 ? (
<Section>
<Item>
<div
style={{
display: 'flex',
justifyContent: 'center',
width: '100%'
}}
>
<Text style={{ fontSize: 15 }}>No transactions</Text>
</div>
</Item>
</Section>
) : null}
{sections.map(section => {
return (
<Section
title={monthUtils.format(section.date, 'MMMM dd, yyyy')}
key={section.id}
>
{section.data.map((transaction, index, transactions) => {
return (
<Item
key={transaction.id}
style={{
fontSize:
index === transactions.length - 1 ? 98 : 'inherit'
}}
textValue={transaction.id}
>
<Transaction
transaction={transaction}
categories={this.props.categories}
accounts={this.props.accounts}
payees={this.props.payees}
showCategory={this.props.showCategory}
added={this.props.isNew(transaction.id)}
onSelect={() => {}} // onSelect(transaction)}
/>
</Item>
);
})}
</Section>
);
})}
</ListBox>
</>
);
}
}
function ListBox(props) {
let state = useListState(props);
let listBoxRef = React.useRef();
let { listBoxProps, labelProps } = useListBox(props, state, listBoxRef);
useEffect(() => {
function loadMoreTransactions() {
if (
Math.abs(
listBoxRef.current.scrollHeight -
listBoxRef.current.clientHeight -
listBoxRef.current.scrollTop
) < listBoxRef.current.clientHeight // load more when we're one screen height from the end
) {
props.loadMore();
}
}
listBoxRef.current.addEventListener('scroll', loadMoreTransactions);
return () => {
listBoxRef.current &&
listBoxRef.current.removeEventListener('scroll', loadMoreTransactions);
};
}, [state.collection]);
return (
<>
<div {...labelProps}>{props.label}</div>
<ul
{...listBoxProps}
ref={listBoxRef}
style={{
padding: 0,
listStyle: 'none',
margin: 0,
overflowY: 'auto',
width: '100%'
}}
>
{[...state.collection].map(item => (
<ListBoxSection key={item.key} section={item} state={state} />
))}
</ul>
</>
);
}
function ListBoxSection({ section, state }) {
let { itemProps, headingProps, groupProps } = useListBoxSection({
heading: section.rendered,
'aria-label': section['aria-label']
});
// The heading is rendered inside an <li> element, which contains
// a <ul> with the child items.
return (
<>
<li {...itemProps} style={{ width: '100%' }}>
{section.rendered && (
<div
{...headingProps}
style={{
...styles.smallText,
backgroundColor: colors.n10,
borderBottom: `1px solid ${colors.n9}`,
borderTop: `1px solid ${colors.n9}`,
color: colors.n4,
display: 'flex',
justifyContent: 'center',
paddingBottom: 4,
paddingTop: 4,
position: 'sticky',
top: '0',
width: '100%',
zIndex: zIndices.SECTION_HEADING
}}
>
{section.rendered}
</div>
)}
<ul
{...groupProps}
style={{
padding: 0,
listStyle: 'none'
}}
>
{[...section.childNodes].map((node, index, nodes) => (
<Option
key={node.key}
item={node}
state={state}
isLast={index === nodes.length - 1}
/>
))}
</ul>
</li>
</>
);
}
function Option({ isLast, item, state }) {
// Get props for the option element
let ref = React.useRef();
let { optionProps, isSelected } = useOption({ key: item.key }, state, ref);
// Determine whether we should show a keyboard
// focus ring for accessibility
let { isFocusVisible, focusProps } = useFocusRing();
return (
<li
{...mergeProps(optionProps, focusProps)}
ref={ref}
style={{
background: isSelected ? 'blueviolet' : 'transparent',
color: isSelected ? 'white' : null,
outline: isFocusVisible ? '2px solid orange' : 'none',
...(!isLast && { borderBottom: `1px solid ${colors.border}` })
}}
>
{item.rendered}
</li>
);
}
export const ROW_HEIGHT = 50;
export const ListItem = React.forwardRef(
({ children, style, ...props }, ref) => {
return (
<View
style={[
{
height: ROW_HEIGHT,
flexDirection: 'row',
alignItems: 'center',
paddingLeft: 10,
paddingRight: 10
},
style
]}
ref={ref}
{...props}
>
{children}
</View>
);
}
);