From 02ec279a64ee3e324d373e803d89c7bf5a2ebc85 Mon Sep 17 00:00:00 2001
From: Matiss Janis Aboltins <matiss@mja.lv>
Date: Tue, 12 Mar 2024 22:04:25 +0000
Subject: [PATCH] :recycle: (typescript) make rollover budget components strict
 TS compliant (#2453)

---
 .../components/autocomplete/Autocomplete.tsx  |  2 +-
 .../autocomplete/CategoryAutocomplete.tsx     |  4 +-
 .../budget/rollover/BalanceTooltip.tsx        | 15 ++++---
 .../budget/rollover/CoverTooltip.tsx          | 11 +++--
 .../budget/rollover/HoldTooltip.tsx           | 11 +++--
 .../budget/rollover/RolloverComponents.tsx    | 15 ++++---
 .../budget/rollover/RolloverContext.tsx       | 24 ++++++++---
 .../budget/rollover/TransferTooltip.tsx       | 21 +++++----
 .../rollover/budgetsummary/BudgetSummary.tsx  | 43 +++++++++++--------
 .../rollover/budgetsummary/ToBudget.tsx       | 11 +++--
 .../src/components/budget/util.ts             |  4 +-
 .../src/components/util/AmountInput.tsx       |  4 +-
 packages/loot-core/src/shared/arithmetic.ts   | 11 +++--
 upcoming-release-notes/2453.md                |  6 +++
 14 files changed, 107 insertions(+), 75 deletions(-)
 create mode 100644 upcoming-release-notes/2453.md

diff --git a/packages/desktop-client/src/components/autocomplete/Autocomplete.tsx b/packages/desktop-client/src/components/autocomplete/Autocomplete.tsx
index 0e923d8d4..ba3b746ff 100644
--- a/packages/desktop-client/src/components/autocomplete/Autocomplete.tsx
+++ b/packages/desktop-client/src/components/autocomplete/Autocomplete.tsx
@@ -197,7 +197,7 @@ type SingleAutocompleteProps<T extends Item> = {
   onSelect: (id: T['id'], value: string) => void;
   tableBehavior?: boolean;
   closeOnBlur?: boolean;
-  value: T | T['id'];
+  value: null | T | T['id'];
   isMulti?: boolean;
 };
 function SingleAutocomplete<T extends Item>({
diff --git a/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx
index de58e530e..d83f82de5 100644
--- a/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx
+++ b/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx
@@ -115,7 +115,9 @@ function CategoryList({
   );
 }
 
-type CategoryAutocompleteProps = ComponentProps<typeof Autocomplete> & {
+type CategoryAutocompleteProps = ComponentProps<
+  typeof Autocomplete<CategoryGroupEntity>
+> & {
   categoryGroups: Array<CategoryGroupEntity>;
   showSplitOption?: boolean;
   renderSplitTransactionButton?: (
diff --git a/packages/desktop-client/src/components/budget/rollover/BalanceTooltip.tsx b/packages/desktop-client/src/components/budget/rollover/BalanceTooltip.tsx
index 6376207ca..5ad625120 100644
--- a/packages/desktop-client/src/components/budget/rollover/BalanceTooltip.tsx
+++ b/packages/desktop-client/src/components/budget/rollover/BalanceTooltip.tsx
@@ -1,4 +1,3 @@
-// @ts-strict-ignore
 import React, { useState } from 'react';
 
 import { rolloverBudget } from 'loot-core/src/client/queries';
@@ -67,11 +66,15 @@ export function BalanceTooltip({
                   ? 'Remove overspending rollover'
                   : 'Rollover overspending',
               },
-              balance < 0 && {
-                name: 'cover',
-                text: 'Cover overspending',
-              },
-            ].filter(x => x)}
+              ...(balance < 0
+                ? [
+                    {
+                      name: 'cover',
+                      text: 'Cover overspending',
+                    },
+                  ]
+                : []),
+            ]}
           />
         </Tooltip>
       )}
diff --git a/packages/desktop-client/src/components/budget/rollover/CoverTooltip.tsx b/packages/desktop-client/src/components/budget/rollover/CoverTooltip.tsx
index 6ac8a4ab6..3ee4c086d 100644
--- a/packages/desktop-client/src/components/budget/rollover/CoverTooltip.tsx
+++ b/packages/desktop-client/src/components/budget/rollover/CoverTooltip.tsx
@@ -1,4 +1,3 @@
-// @ts-strict-ignore
 import React, { type ComponentProps, useState } from 'react';
 
 import { useCategories } from '../../../hooks/useCategories';
@@ -19,11 +18,11 @@ export function CoverTooltip({
   onSubmit,
   onClose,
 }: CoverTooltipProps) {
-  let { grouped: categoryGroups } = useCategories();
-  categoryGroups = addToBeBudgetedGroup(
-    categoryGroups.filter(g => !g.is_income),
+  const { grouped } = useCategories();
+  const categoryGroups = addToBeBudgetedGroup(
+    grouped.filter(g => !g.is_income),
   );
-  const [category, setCategory] = useState(null);
+  const [category, setCategory] = useState<string | null>(null);
 
   function submit() {
     if (category) {
@@ -49,7 +48,7 @@ export function CoverTooltip({
             value={null}
             openOnFocus={true}
             onUpdate={() => {}}
-            onSelect={id => setCategory(id)}
+            onSelect={(id: string | undefined) => setCategory(id || null)}
             inputProps={{
               inputRef: node,
               onKeyDown: e => {
diff --git a/packages/desktop-client/src/components/budget/rollover/HoldTooltip.tsx b/packages/desktop-client/src/components/budget/rollover/HoldTooltip.tsx
index 350c7a852..a2a4e6902 100644
--- a/packages/desktop-client/src/components/budget/rollover/HoldTooltip.tsx
+++ b/packages/desktop-client/src/components/budget/rollover/HoldTooltip.tsx
@@ -1,4 +1,3 @@
-// @ts-strict-ignore
 import React, {
   useState,
   useContext,
@@ -30,7 +29,7 @@ export function HoldTooltip({
   const spreadsheet = useSpreadsheet();
   const sheetName = useContext(NamespaceContext);
 
-  const [amount, setAmount] = useState(null);
+  const [amount, setAmount] = useState<string | null>(null);
 
   useEffect(() => {
     (async () => {
@@ -39,8 +38,8 @@ export function HoldTooltip({
     })();
   }, []);
 
-  function submit() {
-    const parsedAmount = evalArithmetic(amount, null);
+  function submit(newAmount: string) {
+    const parsedAmount = evalArithmetic(newAmount);
     if (parsedAmount) {
       onSubmit(amountToInteger(parsedAmount));
     }
@@ -68,7 +67,7 @@ export function HoldTooltip({
             onChange={(e: ChangeEvent<HTMLInputElement>) =>
               setAmount(e.target.value)
             }
-            onEnter={submit}
+            onEnter={() => submit(amount)}
           />
         </InitialFocus>
       </View>
@@ -85,7 +84,7 @@ export function HoldTooltip({
             paddingTop: 3,
             paddingBottom: 3,
           }}
-          onClick={submit}
+          onClick={() => submit(amount)}
         >
           Hold
         </Button>
diff --git a/packages/desktop-client/src/components/budget/rollover/RolloverComponents.tsx b/packages/desktop-client/src/components/budget/rollover/RolloverComponents.tsx
index 2fa62d106..a8e326f96 100644
--- a/packages/desktop-client/src/components/budget/rollover/RolloverComponents.tsx
+++ b/packages/desktop-client/src/components/budget/rollover/RolloverComponents.tsx
@@ -1,4 +1,3 @@
-// @ts-strict-ignore
 import React, { memo, useState } from 'react';
 
 import { rolloverBudget } from 'loot-core/src/client/queries';
@@ -207,7 +206,7 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({
                 width={14}
                 height={14}
                 className="hover-visible"
-                style={menuOpen && { opacity: 1 }}
+                style={menuOpen ? { opacity: 1 } : {}}
               />
             </Button>
             {menuOpen && (
@@ -239,10 +238,14 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({
                       name: 'set-single-12-avg',
                       text: 'Set to yearly average',
                     },
-                    isGoalTemplatesEnabled && {
-                      name: 'apply-single-category-template',
-                      text: 'Apply budget template',
-                    },
+                    ...(isGoalTemplatesEnabled
+                      ? [
+                          {
+                            name: 'apply-single-category-template',
+                            text: 'Apply budget template',
+                          },
+                        ]
+                      : []),
                   ]}
                 />
               </Tooltip>
diff --git a/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx b/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx
index fcbc714cd..583444a47 100644
--- a/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx
+++ b/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx
@@ -1,14 +1,28 @@
-// @ts-strict-ignore
 import React, { type ReactNode, createContext, useContext } from 'react';
 
 import * as monthUtils from 'loot-core/src/shared/months';
 
-const Context = createContext(null);
-
-type RolloverContextProps = {
+type RolloverContextDefinition = {
   summaryCollapsed: boolean;
-  onBudgetAction: (idx: number, action: string, arg?: unknown) => void;
+  onBudgetAction: (idx: string, action: string, arg?: unknown) => void;
   onToggleSummaryCollapse: () => void;
+  currentMonth: string;
+};
+
+const Context = createContext<RolloverContextDefinition>({
+  summaryCollapsed: false,
+  onBudgetAction: () => {
+    throw new Error('Unitialised context method called: onBudgetAction');
+  },
+  onToggleSummaryCollapse: () => {
+    throw new Error(
+      'Unitialised context method called: onToggleSummaryCollapse',
+    );
+  },
+  currentMonth: 'unknown',
+});
+
+type RolloverContextProps = Omit<RolloverContextDefinition, 'currentMonth'> & {
   children: ReactNode;
 };
 export function RolloverContext({
diff --git a/packages/desktop-client/src/components/budget/rollover/TransferTooltip.tsx b/packages/desktop-client/src/components/budget/rollover/TransferTooltip.tsx
index 7806d7ce3..936ccfa3c 100644
--- a/packages/desktop-client/src/components/budget/rollover/TransferTooltip.tsx
+++ b/packages/desktop-client/src/components/budget/rollover/TransferTooltip.tsx
@@ -1,4 +1,3 @@
-// @ts-strict-ignore
 import React, {
   useState,
   useContext,
@@ -24,10 +23,10 @@ type TransferTooltipProps = ComponentPropsWithoutRef<typeof Tooltip> & {
   initialAmount?: number;
   initialAmountName?: string;
   showToBeBudgeted?: boolean;
-  onSubmit: (amount: number, category: unknown) => void;
+  onSubmit: (amount: number, category: string) => void;
 };
 export function TransferTooltip({
-  initialAmount,
+  initialAmount = 0,
   initialAmountName,
   showToBeBudgeted,
   onSubmit,
@@ -44,8 +43,8 @@ export function TransferTooltip({
     categoryGroups = addToBeBudgetedGroup(categoryGroups);
   }
 
-  const [amount, setAmount] = useState(null);
-  const [category, setCategory] = useState(null);
+  const [amount, setAmount] = useState<string | null>(null);
+  const [category, setCategory] = useState<string | null>(null);
 
   useEffect(() => {
     (async () => {
@@ -58,8 +57,8 @@ export function TransferTooltip({
     })();
   }, []);
 
-  function submit() {
-    const parsedAmount = evalArithmetic(amount, null);
+  function submit(newAmount: string) {
+    const parsedAmount = evalArithmetic(newAmount);
     if (parsedAmount && category) {
       onSubmit(amountToInteger(parsedAmount), category);
       onClose();
@@ -88,7 +87,7 @@ export function TransferTooltip({
           <Input
             value={amount}
             onChange={e => setAmount(e.target['value'])}
-            onEnter={submit}
+            onEnter={() => submit(amount)}
           />
         </InitialFocus>
       </View>
@@ -99,8 +98,8 @@ export function TransferTooltip({
         value={null}
         openOnFocus={true}
         onUpdate={() => {}}
-        onSelect={id => setCategory(id)}
-        inputProps={{ onEnter: submit, placeholder: '(none)' }}
+        onSelect={(id: string | undefined) => setCategory(id || null)}
+        inputProps={{ onEnter: () => submit(amount), placeholder: '(none)' }}
         showHiddenItems={true}
       />
 
@@ -117,7 +116,7 @@ export function TransferTooltip({
             paddingTop: 3,
             paddingBottom: 3,
           }}
-          onClick={submit}
+          onClick={() => submit(amount)}
         >
           Transfer
         </Button>
diff --git a/packages/desktop-client/src/components/budget/rollover/budgetsummary/BudgetSummary.tsx b/packages/desktop-client/src/components/budget/rollover/budgetsummary/BudgetSummary.tsx
index d612fe8ed..5a2a70c81 100644
--- a/packages/desktop-client/src/components/budget/rollover/budgetsummary/BudgetSummary.tsx
+++ b/packages/desktop-client/src/components/budget/rollover/budgetsummary/BudgetSummary.tsx
@@ -1,4 +1,3 @@
-// @ts-strict-ignore
 import React, { useState } from 'react';
 
 import { css } from 'glamor';
@@ -161,22 +160,26 @@ export function BudgetSummary({
                         name: 'set-3-avg',
                         text: 'Set budgets to 3 month average',
                       },
-                      isGoalTemplatesEnabled && {
-                        name: 'check-templates',
-                        text: 'Check templates',
-                      },
-                      isGoalTemplatesEnabled && {
-                        name: 'apply-goal-template',
-                        text: 'Apply budget template',
-                      },
-                      isGoalTemplatesEnabled && {
-                        name: 'overwrite-goal-template',
-                        text: 'Overwrite with budget template',
-                      },
-                      isGoalTemplatesEnabled && {
-                        name: 'cleanup-goal-template',
-                        text: 'End of month cleanup',
-                      },
+                      ...(isGoalTemplatesEnabled
+                        ? [
+                            {
+                              name: 'check-templates',
+                              text: 'Check templates',
+                            },
+                            {
+                              name: 'apply-goal-template',
+                              text: 'Apply budget template',
+                            },
+                            {
+                              name: 'overwrite-goal-template',
+                              text: 'Overwrite with budget template',
+                            },
+                            {
+                              name: 'cleanup-goal-template',
+                              text: 'End of month cleanup',
+                            },
+                          ]
+                        : []),
                     ]}
                   />
                 </Tooltip>
@@ -216,7 +219,11 @@ export function BudgetSummary({
               }}
             />
             <View style={{ margin: '23px 0' }}>
-              <ToBudget month={month} onBudgetAction={onBudgetAction} />
+              <ToBudget
+                prevMonthName={prevMonthName}
+                month={month}
+                onBudgetAction={onBudgetAction}
+              />
             </View>
           </>
         )}
diff --git a/packages/desktop-client/src/components/budget/rollover/budgetsummary/ToBudget.tsx b/packages/desktop-client/src/components/budget/rollover/budgetsummary/ToBudget.tsx
index a07a2ee0f..02b1d1da6 100644
--- a/packages/desktop-client/src/components/budget/rollover/budgetsummary/ToBudget.tsx
+++ b/packages/desktop-client/src/components/budget/rollover/budgetsummary/ToBudget.tsx
@@ -1,4 +1,3 @@
-// @ts-strict-ignore
 import React, { useState, type ComponentPropsWithoutRef } from 'react';
 
 import { css } from 'glamor';
@@ -21,9 +20,9 @@ import { TransferTooltip } from '../TransferTooltip';
 import { TotalsList } from './TotalsList';
 
 type ToBudgetProps = {
-  month: string | number;
-  onBudgetAction: (idx: string | number, action: string, arg?: unknown) => void;
-  prevMonthName?: string;
+  month: string;
+  onBudgetAction: (idx: string, action: string, arg?: unknown) => void;
+  prevMonthName: string;
   showTotalsTooltipOnHover?: boolean;
   style?: CSSProperties;
   amountStyle?: CSSProperties;
@@ -44,7 +43,7 @@ export function ToBudget({
   holdTooltipProps,
   transferTooltipProps,
 }: ToBudgetProps) {
-  const [menuOpen, setMenuOpen] = useState(null);
+  const [menuOpen, setMenuOpen] = useState<string | null>(null);
   const sheetName = useSheetName(rolloverBudget.toBudget);
   const sheetValue = useSheetValue({
     name: rolloverBudget.toBudget,
@@ -60,7 +59,7 @@ export function ToBudget({
       <Block>{isNegative ? 'Overbudgeted:' : 'To Budget:'}</Block>
       <View>
         <HoverTarget
-          disabled={!showTotalsTooltipOnHover || menuOpen}
+          disabled={!showTotalsTooltipOnHover || !!menuOpen}
           renderContent={() => (
             <Tooltip position="bottom-center" {...totalsTooltipProps}>
               <TotalsList
diff --git a/packages/desktop-client/src/components/budget/util.ts b/packages/desktop-client/src/components/budget/util.ts
index cb7b926b7..3c5bc8fe1 100644
--- a/packages/desktop-client/src/components/budget/util.ts
+++ b/packages/desktop-client/src/components/budget/util.ts
@@ -6,7 +6,7 @@ import { type Handlers } from 'loot-core/src/types/handlers';
 import { type CategoryGroupEntity } from 'loot-core/src/types/models';
 import { type LocalPrefs } from 'loot-core/src/types/prefs';
 
-import { styles, theme } from '../../style';
+import { type CSSProperties, styles, theme } from '../../style';
 import { type DropPosition } from '../sort';
 
 import { getValidMonthBounds } from './MonthsContext';
@@ -39,7 +39,7 @@ export function separateGroups(categoryGroups: CategoryGroupEntity[]) {
   ];
 }
 
-export function makeAmountGrey(value: number | string) {
+export function makeAmountGrey(value: number | string): CSSProperties {
   return value === 0 || value === '0' || value === '' || value == null
     ? { color: theme.tableTextSubdued }
     : null;
diff --git a/packages/desktop-client/src/components/util/AmountInput.tsx b/packages/desktop-client/src/components/util/AmountInput.tsx
index 60269b440..e968423e3 100644
--- a/packages/desktop-client/src/components/util/AmountInput.tsx
+++ b/packages/desktop-client/src/components/util/AmountInput.tsx
@@ -69,9 +69,7 @@ export function AmountInput({
   }
 
   function getAmount(negate) {
-    const valueOrInitial = Math.abs(
-      amountToInteger(evalArithmetic(value, initialValueAbsolute)),
-    );
+    const valueOrInitial = Math.abs(amountToInteger(evalArithmetic(value)));
     return negate ? valueOrInitial * -1 : valueOrInitial;
   }
 
diff --git a/packages/loot-core/src/shared/arithmetic.ts b/packages/loot-core/src/shared/arithmetic.ts
index 935a0b229..ac8227864 100644
--- a/packages/loot-core/src/shared/arithmetic.ts
+++ b/packages/loot-core/src/shared/arithmetic.ts
@@ -96,12 +96,12 @@ function makeOperatorParser(...ops) {
 // These operators go from high to low order of precedence
 const parseOperator = makeOperatorParser('^', '/', '*', '-', '+');
 
-function parse(expression) {
+function parse(expression: string) {
   const state = { str: expression.replace(/\s/g, ''), index: 0 };
   return parseOperator(state);
 }
 
-function evaluate(ast) {
+function evaluate(ast): number {
   if (typeof ast === 'number') {
     return ast;
   }
@@ -124,13 +124,16 @@ function evaluate(ast) {
   }
 }
 
-export function evalArithmetic(expression, defaultValue = null) {
+export function evalArithmetic(
+  expression: string,
+  defaultValue: number = null,
+) {
   // An empty expression always evals to the default
   if (expression === '') {
     return defaultValue;
   }
 
-  let result;
+  let result: number;
   try {
     result = evaluate(parse(expression));
   } catch (e) {
diff --git a/upcoming-release-notes/2453.md b/upcoming-release-notes/2453.md
new file mode 100644
index 000000000..2e5aff306
--- /dev/null
+++ b/upcoming-release-notes/2453.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [MatissJanis]
+---
+
+Enabled strict TypeScript in rollover budget components.
-- 
GitLab