mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-11 12:43:09 -05:00
✨ (reports) add loading indicators (#1491)
* ✨ (reports) add loading indicators
* Release notes
This commit is contained in:
committed by
GitHub
parent
2ef0fc9415
commit
a0ecd65e70
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { Fragment, useState } from 'react';
|
||||
|
||||
import Eye from '../../icons/v2/Eye';
|
||||
import EyeSlashed from '../../icons/v2/EyeSlashed';
|
||||
@@ -67,7 +67,7 @@ export default function CategorySelector({
|
||||
),
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Fragment key={categoryGroup.id}>
|
||||
<li
|
||||
style={{
|
||||
display:
|
||||
@@ -75,7 +75,6 @@ export default function CategorySelector({
|
||||
marginBottom: 4,
|
||||
flexDirection: 'row',
|
||||
}}
|
||||
key={categoryGroup.id}
|
||||
>
|
||||
<Checkbox
|
||||
id={`form_${categoryGroup.id}`}
|
||||
@@ -162,7 +161,7 @@ export default function CategorySelector({
|
||||
})}
|
||||
</ul>
|
||||
</li>
|
||||
</>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { integerToCurrency } from 'loot-core/src/shared/util';
|
||||
|
||||
import useCategories from '../../hooks/useCategories';
|
||||
import useFeatureFlag from '../../hooks/useFeatureFlag';
|
||||
import AnimatedLoading from '../../icons/AnimatedLoading';
|
||||
import { colors, styles } from '../../style';
|
||||
import AnchorLink from '../common/AnchorLink';
|
||||
import Block from '../common/Block';
|
||||
@@ -63,6 +64,20 @@ function Card({ flex, to, style, children }) {
|
||||
return content;
|
||||
}
|
||||
|
||||
function LoadingIndicator() {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<AnimatedLoading style={{ width: 25, height: 25 }} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function NetWorthCard({ accounts }) {
|
||||
const end = monthUtils.currentMonth();
|
||||
const start = monthUtils.subMonths(end, 5);
|
||||
@@ -76,10 +91,6 @@ function NetWorthCard({ accounts }) {
|
||||
);
|
||||
const data = useReport('net_worth', params);
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card flex={2} to="/reports/net-worth">
|
||||
<View
|
||||
@@ -97,30 +108,39 @@ function NetWorthCard({ accounts }) {
|
||||
</Block>
|
||||
<DateRange start={start} end={end} />
|
||||
</View>
|
||||
<View style={{ textAlign: 'right' }}>
|
||||
<Block
|
||||
style={[styles.mediumText, { fontWeight: 500, marginBottom: 5 }]}
|
||||
>
|
||||
{data && (
|
||||
<View style={{ textAlign: 'right' }}>
|
||||
<Block
|
||||
style={[
|
||||
styles.mediumText,
|
||||
{ fontWeight: 500, marginBottom: 5 },
|
||||
]}
|
||||
>
|
||||
<PrivacyFilter activationFilters={[!isCardHovered]}>
|
||||
{integerToCurrency(data.netWorth)}
|
||||
</PrivacyFilter>
|
||||
</Block>
|
||||
<PrivacyFilter activationFilters={[!isCardHovered]}>
|
||||
{integerToCurrency(data.netWorth)}
|
||||
<Change
|
||||
amount={data.totalChange}
|
||||
style={{ color: colors.n6, fontWeight: 300 }}
|
||||
/>
|
||||
</PrivacyFilter>
|
||||
</Block>
|
||||
<PrivacyFilter activationFilters={[!isCardHovered]}>
|
||||
<Change
|
||||
amount={data.totalChange}
|
||||
style={{ color: colors.n6, fontWeight: 300 }}
|
||||
/>
|
||||
</PrivacyFilter>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<NetWorthGraph
|
||||
start={start}
|
||||
end={end}
|
||||
graphData={data.graphData}
|
||||
compact={true}
|
||||
style={{ height: 'auto', flex: 1 }}
|
||||
/>
|
||||
{data ? (
|
||||
<NetWorthGraph
|
||||
start={start}
|
||||
end={end}
|
||||
graphData={data.graphData}
|
||||
compact={true}
|
||||
style={{ height: 'auto', flex: 1 }}
|
||||
/>
|
||||
) : (
|
||||
<LoadingIndicator />
|
||||
)}
|
||||
</View>
|
||||
</Card>
|
||||
);
|
||||
@@ -136,13 +156,9 @@ function CashFlowCard() {
|
||||
const onCardHover = useCallback(() => setIsCardHovered(true));
|
||||
const onCardHoverEnd = useCallback(() => setIsCardHovered(false));
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { graphData } = data;
|
||||
const expense = -(graphData.expense || 0);
|
||||
const income = graphData.income || 0;
|
||||
const { graphData } = data || {};
|
||||
const expense = -(graphData?.expense || 0);
|
||||
const income = graphData?.income || 0;
|
||||
|
||||
return (
|
||||
<Card flex={1} to="/reports/cash-flow">
|
||||
@@ -161,102 +177,108 @@ function CashFlowCard() {
|
||||
</Block>
|
||||
<DateRange start={start} end={end} />
|
||||
</View>
|
||||
<View style={{ textAlign: 'right' }}>
|
||||
<PrivacyFilter activationFilters={[!isCardHovered]}>
|
||||
<Change
|
||||
amount={income - expense}
|
||||
style={{ color: colors.n6, fontWeight: 300 }}
|
||||
/>
|
||||
</PrivacyFilter>
|
||||
</View>
|
||||
{data && (
|
||||
<View style={{ textAlign: 'right' }}>
|
||||
<PrivacyFilter activationFilters={[!isCardHovered]}>
|
||||
<Change
|
||||
amount={income - expense}
|
||||
style={{ color: colors.n6, fontWeight: 300 }}
|
||||
/>
|
||||
</PrivacyFilter>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Container style={{ height: 'auto', flex: 1 }}>
|
||||
{(width, height, portalHost) => (
|
||||
<VictoryGroup
|
||||
colorScale={[theme.colors.blue, theme.colors.red]}
|
||||
width={100}
|
||||
height={height}
|
||||
theme={theme}
|
||||
domain={{
|
||||
x: [0, 100],
|
||||
y: [0, Math.max(income, expense, 100)],
|
||||
}}
|
||||
containerComponent={
|
||||
<VictoryVoronoiContainer voronoiDimension="x" />
|
||||
}
|
||||
labelComponent={
|
||||
<Tooltip
|
||||
portalHost={portalHost}
|
||||
offsetX={(width - 100) / 2}
|
||||
offsetY={y => (y + 40 > height ? height - 40 : y)}
|
||||
light={true}
|
||||
forceActive={true}
|
||||
style={{
|
||||
padding: 0,
|
||||
}}
|
||||
{data ? (
|
||||
<Container style={{ height: 'auto', flex: 1 }}>
|
||||
{(width, height, portalHost) => (
|
||||
<VictoryGroup
|
||||
colorScale={[theme.colors.blue, theme.colors.red]}
|
||||
width={100}
|
||||
height={height}
|
||||
theme={theme}
|
||||
domain={{
|
||||
x: [0, 100],
|
||||
y: [0, Math.max(income, expense, 100)],
|
||||
}}
|
||||
containerComponent={
|
||||
<VictoryVoronoiContainer voronoiDimension="x" />
|
||||
}
|
||||
labelComponent={
|
||||
<Tooltip
|
||||
portalHost={portalHost}
|
||||
offsetX={(width - 100) / 2}
|
||||
offsetY={y => (y + 40 > height ? height - 40 : y)}
|
||||
light={true}
|
||||
forceActive={true}
|
||||
style={{
|
||||
padding: 0,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
padding={{
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
}}
|
||||
>
|
||||
<VictoryBar
|
||||
barWidth={13}
|
||||
data={[
|
||||
{
|
||||
x: 30,
|
||||
y: Math.max(income, 5),
|
||||
premadeLabel: (
|
||||
<View style={{ textAlign: 'right' }}>
|
||||
Income
|
||||
<View>
|
||||
<PrivacyFilter activationFilters={[!isCardHovered]}>
|
||||
{integerToCurrency(income)}
|
||||
</PrivacyFilter>
|
||||
</View>
|
||||
</View>
|
||||
),
|
||||
labelPosition: 'left',
|
||||
},
|
||||
]}
|
||||
labels={d => d.premadeLabel}
|
||||
/>
|
||||
}
|
||||
padding={{
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
}}
|
||||
>
|
||||
<VictoryBar
|
||||
barWidth={13}
|
||||
data={[
|
||||
{
|
||||
x: 30,
|
||||
y: Math.max(income, 5),
|
||||
premadeLabel: (
|
||||
<View style={{ textAlign: 'right' }}>
|
||||
Income
|
||||
<VictoryBar
|
||||
barWidth={13}
|
||||
data={[
|
||||
{
|
||||
x: 60,
|
||||
y: Math.max(expense, 5),
|
||||
premadeLabel: (
|
||||
<View>
|
||||
<PrivacyFilter activationFilters={[!isCardHovered]}>
|
||||
{integerToCurrency(income)}
|
||||
</PrivacyFilter>
|
||||
Expenses
|
||||
<View>
|
||||
<PrivacyFilter activationFilters={[!isCardHovered]}>
|
||||
{integerToCurrency(expense)}
|
||||
</PrivacyFilter>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
),
|
||||
labelPosition: 'left',
|
||||
},
|
||||
]}
|
||||
labels={d => d.premadeLabel}
|
||||
/>
|
||||
<VictoryBar
|
||||
barWidth={13}
|
||||
data={[
|
||||
{
|
||||
x: 60,
|
||||
y: Math.max(expense, 5),
|
||||
premadeLabel: (
|
||||
<View>
|
||||
Expenses
|
||||
<View>
|
||||
<PrivacyFilter activationFilters={[!isCardHovered]}>
|
||||
{integerToCurrency(expense)}
|
||||
</PrivacyFilter>
|
||||
</View>
|
||||
</View>
|
||||
),
|
||||
labelPosition: 'right',
|
||||
fill: theme.colors.red,
|
||||
},
|
||||
]}
|
||||
labels={d => d.premadeLabel}
|
||||
/>
|
||||
</VictoryGroup>
|
||||
)}
|
||||
</Container>
|
||||
),
|
||||
labelPosition: 'right',
|
||||
fill: theme.colors.red,
|
||||
},
|
||||
]}
|
||||
labels={d => d.premadeLabel}
|
||||
/>
|
||||
</VictoryGroup>
|
||||
)}
|
||||
</Container>
|
||||
) : (
|
||||
<LoadingIndicator />
|
||||
)}
|
||||
</View>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function CategorySpendingCard() {
|
||||
const categories = useCategories();
|
||||
const { list: categories = [] } = useCategories();
|
||||
|
||||
const end = monthUtils.currentDay();
|
||||
const start = monthUtils.subMonths(end, 3);
|
||||
@@ -266,9 +288,7 @@ function CategorySpendingCard() {
|
||||
start,
|
||||
end,
|
||||
3,
|
||||
(categories.list || []).filter(
|
||||
category => !category.is_income && !category.hidden,
|
||||
),
|
||||
categories.filter(category => !category.is_income && !category.hidden),
|
||||
);
|
||||
}, [start, end, categories]);
|
||||
|
||||
@@ -289,13 +309,16 @@ function CategorySpendingCard() {
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
{!perCategorySpending ? null : (
|
||||
|
||||
{perCategorySpending ? (
|
||||
<CategorySpendingGraph
|
||||
start={start}
|
||||
end={end}
|
||||
graphData={perCategorySpending}
|
||||
compact={true}
|
||||
/>
|
||||
) : (
|
||||
<LoadingIndicator />
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -52,7 +52,6 @@ export default function createSpreadsheet(
|
||||
setData: (graphData: CategorySpendingGraphData) => void,
|
||||
) => {
|
||||
if (start === null || end === null || categories.length === 0) {
|
||||
setData({ categories: [], tickValues: [], data: {} });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -72,7 +71,6 @@ export default function createSpreadsheet(
|
||||
.select('date'),
|
||||
);
|
||||
if (firstTransaction.data.length === 0) {
|
||||
setData({ categories: [], tickValues: [], data: {} });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export function Area({ start, end, scale, range }: AreaProps) {
|
||||
const startX = scale.x(d.parseISO(start + '-01'));
|
||||
const endX = scale.x(d.parseISO(end + '-01'));
|
||||
|
||||
if (startX < 0 || endX < 0) {
|
||||
if (startX < 0 || endX < 0 || startX === undefined || endX === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
6
upcoming-release-notes/1491.md
Normal file
6
upcoming-release-notes/1491.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Add loading indicators to reports page
|
||||
Reference in New Issue
Block a user