From a0ecd65e70b621a1e281b324e8204ebb9fa0c840 Mon Sep 17 00:00:00 2001
From: Matiss Janis Aboltins <matiss@mja.lv>
Date: Sat, 12 Aug 2023 09:10:45 +0100
Subject: [PATCH] :sparkles: (reports) add loading indicators (#1491)

* :sparkles: (reports) add loading indicators

* Release notes
---
 .../components/reports/CategorySelector.tsx   |   7 +-
 .../src/components/reports/Overview.js        | 263 ++++++++++--------
 .../graphs/category-spending-spreadsheet.tsx  |   2 -
 .../src/components/reports/graphs/common.tsx  |   2 +-
 upcoming-release-notes/1491.md                |   6 +
 5 files changed, 153 insertions(+), 127 deletions(-)
 create mode 100644 upcoming-release-notes/1491.md

diff --git a/packages/desktop-client/src/components/reports/CategorySelector.tsx b/packages/desktop-client/src/components/reports/CategorySelector.tsx
index a891d22dd..4b77f92b0 100644
--- a/packages/desktop-client/src/components/reports/CategorySelector.tsx
+++ b/packages/desktop-client/src/components/reports/CategorySelector.tsx
@@ -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>
diff --git a/packages/desktop-client/src/components/reports/Overview.js b/packages/desktop-client/src/components/reports/Overview.js
index 790feeb55..04308f7d5 100644
--- a/packages/desktop-client/src/components/reports/Overview.js
+++ b/packages/desktop-client/src/components/reports/Overview.js
@@ -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,
-                  }}
-                />
-              }
-              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>
+        {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>
-                      </View>
-                    ),
-                    labelPosition: 'left',
-                  },
-                ]}
-                labels={d => d.premadeLabel}
-              />
-              <VictoryBar
-                barWidth={13}
-                data={[
-                  {
-                    x: 60,
-                    y: Math.max(expense, 5),
-                    premadeLabel: (
-                      <View>
-                        Expenses
+                      ),
+                      labelPosition: 'left',
+                    },
+                  ]}
+                  labels={d => d.premadeLabel}
+                />
+                <VictoryBar
+                  barWidth={13}
+                  data={[
+                    {
+                      x: 60,
+                      y: Math.max(expense, 5),
+                      premadeLabel: (
                         <View>
-                          <PrivacyFilter activationFilters={[!isCardHovered]}>
-                            {integerToCurrency(expense)}
-                          </PrivacyFilter>
+                          Expenses
+                          <View>
+                            <PrivacyFilter activationFilters={[!isCardHovered]}>
+                              {integerToCurrency(expense)}
+                            </PrivacyFilter>
+                          </View>
                         </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>
   );
diff --git a/packages/desktop-client/src/components/reports/graphs/category-spending-spreadsheet.tsx b/packages/desktop-client/src/components/reports/graphs/category-spending-spreadsheet.tsx
index 9dde112f7..d4bb392ed 100644
--- a/packages/desktop-client/src/components/reports/graphs/category-spending-spreadsheet.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/category-spending-spreadsheet.tsx
@@ -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;
       }
 
diff --git a/packages/desktop-client/src/components/reports/graphs/common.tsx b/packages/desktop-client/src/components/reports/graphs/common.tsx
index 4c384cf54..c88ccd232 100644
--- a/packages/desktop-client/src/components/reports/graphs/common.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/common.tsx
@@ -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;
   }
 
diff --git a/upcoming-release-notes/1491.md b/upcoming-release-notes/1491.md
new file mode 100644
index 000000000..56ac7a9ad
--- /dev/null
+++ b/upcoming-release-notes/1491.md
@@ -0,0 +1,6 @@
+---
+category: Enhancements
+authors: [MatissJanis]
+---
+
+Add loading indicators to reports page
-- 
GitLab