From 572033debe610a8956ce822b57e5e181267d17fc Mon Sep 17 00:00:00 2001
From: Tom Crasset <25140344+tcrasset@users.noreply.github.com>
Date: Sat, 13 Jul 2024 19:02:01 +0200
Subject: [PATCH] migrate BudgetList to typescript (#3026)

---
 .../{BudgetList.jsx => BudgetList.tsx}        | 134 ++++++++++++------
 .../loot-core/src/client/reducers/budgets.ts  |   4 +-
 packages/loot-core/src/types/file.d.ts        |   2 +
 upcoming-release-notes/3026.md                |   6 +
 4 files changed, 104 insertions(+), 42 deletions(-)
 rename packages/desktop-client/src/components/manager/{BudgetList.jsx => BudgetList.tsx} (77%)
 create mode 100644 upcoming-release-notes/3026.md

diff --git a/packages/desktop-client/src/components/manager/BudgetList.jsx b/packages/desktop-client/src/components/manager/BudgetList.tsx
similarity index 77%
rename from packages/desktop-client/src/components/manager/BudgetList.jsx
rename to packages/desktop-client/src/components/manager/BudgetList.tsx
index b012bdc06..9e30b3a1b 100644
--- a/packages/desktop-client/src/components/manager/BudgetList.jsx
+++ b/packages/desktop-client/src/components/manager/BudgetList.tsx
@@ -1,4 +1,5 @@
-import React, { useState, useRef } from 'react';
+import type React from 'react';
+import { useState, useRef, type CSSProperties } from 'react';
 import { useDispatch, useSelector } from 'react-redux';
 
 import {
@@ -12,6 +13,12 @@ import {
   pushModal,
 } from 'loot-core/client/actions';
 import { isNonProductionEnvironment } from 'loot-core/src/shared/environment';
+import {
+  type File,
+  type LocalFile,
+  type SyncableLocalFile,
+  type SyncedLocalFile,
+} from 'loot-core/types/file';
 
 import { useInitialMount } from '../../hooks/useInitialMount';
 import { useLocalPref } from '../../hooks/useLocalPref';
@@ -32,7 +39,7 @@ import { Popover } from '../common/Popover';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
 
-function getFileDescription(file) {
+function getFileDescription(file: File) {
   if (file.state === 'unknown') {
     return (
       'This is a cloud-based file but its state is unknown because you ' +
@@ -50,8 +57,14 @@ function getFileDescription(file) {
   return null;
 }
 
-function FileMenu({ onDelete, onClose }) {
-  function onMenuSelect(type) {
+function FileMenu({
+  onDelete,
+  onClose,
+}: {
+  onDelete: () => void;
+  onClose: () => void;
+}) {
+  function onMenuSelect(type: string) {
     onClose();
 
     switch (type) {
@@ -83,7 +96,7 @@ function FileMenu({ onDelete, onClose }) {
   );
 }
 
-function FileMenuButton({ state, onDelete }) {
+function FileMenuButton({ onDelete }: { onDelete: () => void }) {
   const triggerRef = useRef(null);
   const [menuOpen, setMenuOpen] = useState(false);
 
@@ -106,17 +119,13 @@ function FileMenuButton({ state, onDelete }) {
         isOpen={menuOpen}
         onOpenChange={() => setMenuOpen(false)}
       >
-        <FileMenu
-          state={state}
-          onDelete={onDelete}
-          onClose={() => setMenuOpen(false)}
-        />
+        <FileMenu onDelete={onDelete} onClose={() => setMenuOpen(false)} />
       </Popover>
     </View>
   );
 }
 
-function FileState({ file }) {
+function FileState({ file }: { file: File }) {
   let Icon;
   let status;
   let color;
@@ -164,10 +173,20 @@ function FileState({ file }) {
   );
 }
 
-function File({ file, quickSwitchMode, onSelect, onDelete }) {
+function FileItem({
+  file,
+  quickSwitchMode,
+  onSelect,
+  onDelete,
+}: {
+  file: File;
+  quickSwitchMode: boolean;
+  onSelect: (file: File) => void;
+  onDelete: (file: File) => void;
+}) {
   const selecting = useRef(false);
 
-  async function _onSelect(file) {
+  async function _onSelect(file: File) {
     // Never allow selecting the file while uploading/downloading, and
     // make sure to never allow duplicate clicks
     if (!selecting.current) {
@@ -180,7 +199,7 @@ function File({ file, quickSwitchMode, onSelect, onDelete }) {
   return (
     <View
       onClick={() => _onSelect(file)}
-      title={getFileDescription(file)}
+      title={getFileDescription(file) || ''}
       style={{
         flexDirection: 'row',
         justifyContent: 'space-between',
@@ -193,7 +212,7 @@ function File({ file, quickSwitchMode, onSelect, onDelete }) {
         flexShrink: 0,
         cursor: 'pointer',
         ':hover': {
-          backgroundColor: theme.hover,
+          backgroundColor: theme.menuItemBackgroundHover,
         },
       }}
     >
@@ -219,15 +238,27 @@ function File({ file, quickSwitchMode, onSelect, onDelete }) {
           />
         )}
 
-        {!quickSwitchMode && (
-          <FileMenuButton state={file.state} onDelete={() => onDelete(file)} />
-        )}
+        {!quickSwitchMode && <FileMenuButton onDelete={() => onDelete(file)} />}
       </View>
     </View>
   );
 }
 
-function BudgetFiles({ files, quickSwitchMode, onSelect, onDelete }) {
+function BudgetFiles({
+  files,
+  quickSwitchMode,
+  onSelect,
+  onDelete,
+}: {
+  files: File[];
+  quickSwitchMode: boolean;
+  onSelect: (file: File) => void;
+  onDelete: (file: File) => void;
+}) {
+  function isLocalFile(file: File): file is LocalFile {
+    return file.state === 'local';
+  }
+
   return (
     <View
       style={{
@@ -252,8 +283,8 @@ function BudgetFiles({ files, quickSwitchMode, onSelect, onDelete }) {
         </Text>
       ) : (
         files.map(file => (
-          <File
-            key={file.id || file.cloudFileId}
+          <FileItem
+            key={isLocalFile(file) ? file.id : file.cloudFileId}
             file={file}
             quickSwitchMode={quickSwitchMode}
             onSelect={onSelect}
@@ -265,7 +296,13 @@ function BudgetFiles({ files, quickSwitchMode, onSelect, onDelete }) {
   );
 }
 
-function RefreshButton({ style, onRefresh }) {
+function RefreshButton({
+  style,
+  onRefresh,
+}: {
+  style?: CSSProperties;
+  onRefresh: () => void;
+}) {
   const [loading, setLoading] = useState(false);
 
   async function _onRefresh() {
@@ -288,7 +325,13 @@ function RefreshButton({ style, onRefresh }) {
   );
 }
 
-function BudgetListHeader({ quickSwitchMode, onRefresh }) {
+function BudgetListHeader({
+  quickSwitchMode,
+  onRefresh,
+}: {
+  quickSwitchMode: boolean;
+  onRefresh: () => void;
+}) {
   return (
     <View
       style={{
@@ -314,7 +357,14 @@ export function BudgetList({ showHeader = true, quickSwitchMode = false }) {
   const allFiles = useSelector(state => state.budgets.allFiles || []);
   const [id] = useLocalPref('id');
 
-  const files = id ? allFiles.filter(f => f.id !== id) : allFiles;
+  // Remote files do not have the 'id' field
+  function isNonRemoteFile(
+    file: File,
+  ): file is LocalFile | SyncableLocalFile | SyncedLocalFile {
+    return file.state !== 'remote';
+  }
+  const nonRemoteFiles = allFiles.filter(isNonRemoteFile);
+  const files = id ? nonRemoteFiles.filter(f => f.id !== id) : allFiles;
 
   const [creating, setCreating] = useState(false);
   const { isNarrowWidth } = useResponsive();
@@ -324,7 +374,7 @@ export function BudgetList({ showHeader = true, quickSwitchMode = false }) {
       }
     : {};
 
-  const onCreate = ({ testMode } = {}) => {
+  const onCreate = ({ testMode = false } = {}) => {
     if (!creating) {
       setCreating(true);
       dispatch(createBudget({ testMode }));
@@ -341,6 +391,22 @@ export function BudgetList({ showHeader = true, quickSwitchMode = false }) {
     refresh();
   }
 
+  const onSelect = (file: File): void => {
+    const isRemoteFile = file.state === 'remote';
+
+    if (!id) {
+      if (isRemoteFile) {
+        dispatch(downloadBudget(file.cloudFileId));
+      } else {
+        dispatch(loadBudget(file.id));
+      }
+    } else if (!isRemoteFile && file.id !== id) {
+      dispatch(closeAndLoadBudget(file.id));
+    } else if (isRemoteFile) {
+      dispatch(closeAndDownloadBudget(file.cloudFileId));
+    }
+  };
+
   return (
     <View
       style={{
@@ -365,21 +431,7 @@ export function BudgetList({ showHeader = true, quickSwitchMode = false }) {
       <BudgetFiles
         files={files}
         quickSwitchMode={quickSwitchMode}
-        onSelect={file => {
-          if (!id) {
-            if (file.state === 'remote') {
-              dispatch(downloadBudget(file.cloudFileId));
-            } else {
-              dispatch(loadBudget(file.id));
-            }
-          } else if (file.id !== id) {
-            if (file.state === 'remote') {
-              dispatch(closeAndDownloadBudget(file.cloudFileId));
-            } else {
-              dispatch(closeAndLoadBudget(file.id));
-            }
-          }
-        }}
+        onSelect={onSelect}
         onDelete={file => dispatch(pushModal('delete-budget', { file }))}
       />
       {!quickSwitchMode && (
@@ -408,7 +460,7 @@ export function BudgetList({ showHeader = true, quickSwitchMode = false }) {
 
           <Button
             type="primary"
-            onClick={onCreate}
+            onClick={() => onCreate()}
             style={{
               ...narrowButtonStyle,
               marginLeft: 10,
diff --git a/packages/loot-core/src/client/reducers/budgets.ts b/packages/loot-core/src/client/reducers/budgets.ts
index 8111f24e0..9465fa0bd 100644
--- a/packages/loot-core/src/client/reducers/budgets.ts
+++ b/packages/loot-core/src/client/reducers/budgets.ts
@@ -47,6 +47,7 @@ function reconcileFiles(
           groupId,
           deleted: false,
           state: 'unknown',
+          hasKey: true,
         };
       }
 
@@ -85,10 +86,11 @@ function reconcileFiles(
           groupId,
           deleted: false,
           state: 'broken',
+          hasKey: true,
         };
       }
     } else {
-      return { ...localFile, deleted: false, state: 'local' };
+      return { ...localFile, deleted: false, state: 'local', hasKey: true };
     }
   });
 
diff --git a/packages/loot-core/src/types/file.d.ts b/packages/loot-core/src/types/file.d.ts
index 9302a7c76..e9db42f96 100644
--- a/packages/loot-core/src/types/file.d.ts
+++ b/packages/loot-core/src/types/file.d.ts
@@ -10,12 +10,14 @@ export type FileState =
 
 export type LocalFile = Omit<Budget, 'cloudFileId' | 'groupId'> & {
   state: 'local';
+  hasKey: boolean;
 };
 
 export type SyncableLocalFile = Budget & {
   cloudFileId: string;
   groupId: string;
   state: 'broken' | 'unknown';
+  hasKey: boolean;
 };
 
 export type SyncedLocalFile = Budget & {
diff --git a/upcoming-release-notes/3026.md b/upcoming-release-notes/3026.md
new file mode 100644
index 000000000..b49a40cf2
--- /dev/null
+++ b/upcoming-release-notes/3026.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [tcrasset]
+---
+
+Migrate BudgetList to Typescript
\ No newline at end of file
-- 
GitLab