From 000c65cd703f1c3186432f7eddccd6e0cb934e62 Mon Sep 17 00:00:00 2001 From: Kesin Ryan Dehejia Date: Fri, 17 Jan 2025 08:59:49 -0800 Subject: [PATCH 1/4] chore: establish a clean `tsconfig.json` This commit is not intended to be final - there are many errors to be cleaned-up --- tsconfig.json | 126 +++++++++++--------------------------------------- 1 file changed, 27 insertions(+), 99 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index beb3ea424..7a490690f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,106 +1,34 @@ { "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - /* Language and Environment */ - "target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - "jsx": "react-jsx" /* Specify what JSX code is generated. */, - // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - /* Modules */ - "module": "commonjs" /* Specify what module code is generated. */, - // "rootDir": "./", /* Specify the root folder within your source files. */ - // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ - // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ - // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ - // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - "resolveJsonModule": true /* Enable importing .json files. */, - // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true /* Disable emitting files from a compilation. */, - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - /* Type Checking */ - "strict": true /* Enable all strict type-checking options. */, - "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */, - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - "useUnknownInCatchVariables": true /* Default catch clause variables as 'unknown' instead of 'any'. */, - "alwaysStrict": true /* Ensure 'use strict' is always emitted. */, - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true, /* Skip type checking all .d.ts files. */ - "lib": [ + /* Base Options: */ + "esModuleInterop": true, + "skipLibCheck": true, + "target": "es2022", + "allowJs": true, + "resolveJsonModule": true, + "moduleDetection": "force", + + // Uncomment and resolve ASAP + // "isolatedModules": true, + // "verbatimModuleSyntax": true, + + /* Strictness */ + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + /* We are transpiling with esbuild, NOT TypeScript */ + "module": "preserve", + "noEmit": true, + + /* Our code runs in the DOM */ + "lib": [ "es2022", "dom", "dom.iterable" - ] + ], + + /* JSX */ + "jsx": "react-jsx", } } \ No newline at end of file From fbee6efb2de547e72f13f69dc6e105e8c1897224 Mon Sep 17 00:00:00 2001 From: Kesin Ryan Dehejia Date: Tue, 20 May 2025 10:52:13 -0700 Subject: [PATCH 2/4] chore: fix ts issues outside of components --- .../useAugmentedBankTransactions.tsx | 13 ++++------- src/hooks/useElementSize/useElementSize.ts | 22 ++++++++++++------- src/hooks/useIsVisible/useIsVisible.ts | 8 ++++--- src/hooks/useProfitAndLossComparison/utils.ts | 2 +- src/hooks/useWindowSize/useWindowSize.ts | 4 +++- src/utils/colors.ts | 10 ++++----- .../request/toDefinedSearchParameters.ts | 2 +- .../internal/TransactionsToReview.tsx | 6 +++-- src/views/Reports/Reports.tsx | 12 ++++++---- 9 files changed, 45 insertions(+), 34 deletions(-) diff --git a/src/hooks/useBankTransactions/useAugmentedBankTransactions.tsx b/src/hooks/useBankTransactions/useAugmentedBankTransactions.tsx index 67ce82d9b..3d407e8f2 100644 --- a/src/hooks/useBankTransactions/useAugmentedBankTransactions.tsx +++ b/src/hooks/useBankTransactions/useAugmentedBankTransactions.tsx @@ -175,17 +175,12 @@ export const useAugmentedBankTransactions = ( return undefined }, [rawResponseData]) - const lastMetadata = useMemo(() => { - if (rawResponseData && rawResponseData.length > 0) { - return rawResponseData[rawResponseData.length - 1].meta - } - - return undefined - }, [rawResponseData]) + const lastMetadata = useMemo(() => rawResponseData?.[rawResponseData.length - 1]?.meta, [rawResponseData]) const hasMore = useMemo(() => { - if (rawResponseData && rawResponseData.length > 0) { - const lastElement = rawResponseData[rawResponseData.length - 1] + const lastElement = rawResponseData?.[rawResponseData.length - 1] + + if (lastElement) { return Boolean( lastElement.meta?.pagination?.cursor && lastElement.meta?.pagination?.has_more, diff --git a/src/hooks/useElementSize/useElementSize.ts b/src/hooks/useElementSize/useElementSize.ts index cf2729b3d..e75512c2b 100644 --- a/src/hooks/useElementSize/useElementSize.ts +++ b/src/hooks/useElementSize/useElementSize.ts @@ -22,18 +22,24 @@ export const useElementSize = ( return } - const observer = new ResizeObserver((entries) => { + const observer = new ResizeObserver(([entry]) => { + if (!entry) { + return + } + if (resizeTimeout.current) { clearTimeout(resizeTimeout.current) } resizeTimeout.current = window.setTimeout(() => { - const entry = entries[0] - callback(element, entry, { - width: element.offsetWidth, - height: element.offsetHeight, - clientWidth: element.clientWidth, - clientHeight: element.clientHeight, - }) + callback( + element, + entry, + { + width: element.offsetWidth, + height: element.offsetHeight, + clientWidth: element.clientWidth, + clientHeight: element.clientHeight, + }) }, 100) }) observer.observe(element) diff --git a/src/hooks/useIsVisible/useIsVisible.ts b/src/hooks/useIsVisible/useIsVisible.ts index 2bc409426..74bb6c499 100644 --- a/src/hooks/useIsVisible/useIsVisible.ts +++ b/src/hooks/useIsVisible/useIsVisible.ts @@ -17,9 +17,11 @@ export const useIsVisible = (ref: React.RefObject) => { return } - const observer = new IntersectionObserver(([entry]) => - setIntersecting(entry.isIntersecting), - ) + const observer = new IntersectionObserver(([entry]) => { + if (entry) { + setIntersecting(entry.isIntersecting) + } + }) observer.observe(ref.current) return () => { diff --git a/src/hooks/useProfitAndLossComparison/utils.ts b/src/hooks/useProfitAndLossComparison/utils.ts index 99c3a4429..2bf9e9786 100644 --- a/src/hooks/useProfitAndLossComparison/utils.ts +++ b/src/hooks/useProfitAndLossComparison/utils.ts @@ -41,7 +41,7 @@ export function prepareFiltersBody(compareOptions: TagComparisonOption[]): Reado } const allFilters = [ - noneFilters.length > 0 + noneFilters[0] !== undefined ? { structure: noneFilters[0].tagFilterConfig.structure, required_tags: [], diff --git a/src/hooks/useWindowSize/useWindowSize.ts b/src/hooks/useWindowSize/useWindowSize.ts index ecca93e65..1a599838d 100644 --- a/src/hooks/useWindowSize/useWindowSize.ts +++ b/src/hooks/useWindowSize/useWindowSize.ts @@ -2,7 +2,8 @@ import { useState, useLayoutEffect, useMemo } from 'react' import { BREAKPOINTS } from '../../config/general' export const useWindowSize = () => { - const [size, setSize] = useState([0, 0]) + const [size, setSize] = useState(() => [0, 0] as [number, number]) + useLayoutEffect(() => { function updateSize() { setSize([window.innerWidth, window.innerHeight]) @@ -14,6 +15,7 @@ export const useWindowSize = () => { return () => window.removeEventListener('resize', updateSize) }, []) + return size } diff --git a/src/utils/colors.ts b/src/utils/colors.ts index 7631a4988..cfe48a64a 100644 --- a/src/utils/colors.ts +++ b/src/utils/colors.ts @@ -196,9 +196,9 @@ const hexToRgb = (hex: string) => { } return { - r: values[0], - g: values[1], - b: values[2], + r: values[0] ?? 0, + g: values[1] ?? 0, + b: values[2] ?? 0, } } @@ -288,7 +288,7 @@ const hslToRgb = (hsl: ColorHSLNumberConfig): ColorRGBNumberConfig => { /** * Convert HSL to HEX */ -const hslToHex = (hsl: ColorHSLNumberConfig): string => { +const hslToHex = (hsl: ColorHSLNumberConfig) => { const l = hsl.l / 100 const s = hsl.s const a = (s * Math.min(l, 1 - l)) / 100 @@ -299,5 +299,5 @@ const hslToHex = (hsl: ColorHSLNumberConfig): string => { .toString(16) .padStart(2, '0') } - return `#${f(0)}${f(8)}${f(4)}` + return `#${f(0)}${f(8)}${f(4)}` as const } diff --git a/src/utils/request/toDefinedSearchParameters.ts b/src/utils/request/toDefinedSearchParameters.ts index 13da9a262..2ca5d0e97 100644 --- a/src/utils/request/toDefinedSearchParameters.ts +++ b/src/utils/request/toDefinedSearchParameters.ts @@ -35,7 +35,7 @@ export function toDefinedSearchParameters( return [[key, value]] }) - .filter(([_, value]) => value !== '') + .filter((entry): entry is [string, string] => entry.length === 2 && entry[1] !== '') .map(([key, value]) => [toSnakeCase(key), value]) return new URLSearchParams(definedParameterPairs) diff --git a/src/views/AccountingOverview/internal/TransactionsToReview.tsx b/src/views/AccountingOverview/internal/TransactionsToReview.tsx index 5f9ff8e3f..4e8630b17 100644 --- a/src/views/AccountingOverview/internal/TransactionsToReview.tsx +++ b/src/views/AccountingOverview/internal/TransactionsToReview.tsx @@ -61,8 +61,10 @@ export function TransactionsToReview({ x.month - 1 === getMonth(dateRange.startDate) && x.year === getYear(dateRange.startDate), ) - if (monthTx.length > 0) { - setToReview(monthTx[0].uncategorized_transactions) + + const firstMonth = monthTx[0] + if (firstMonth) { + setToReview(firstMonth.uncategorized_transactions) } } } diff --git a/src/views/Reports/Reports.tsx b/src/views/Reports/Reports.tsx index 690a06c87..cf698e52b 100644 --- a/src/views/Reports/Reports.tsx +++ b/src/views/Reports/Reports.tsx @@ -14,6 +14,7 @@ import { useElementViewSize } from '../../hooks/useElementViewSize/useElementVie import { View as ViewType } from '../../types/general' import type { TimeRangePickerConfig } from './reportTypes' import { ProfitAndLossCompareConfig } from '../../types/profit_and_loss' +import type { ReadonlyArrayWithAtLeastOne } from '../../utils/array/getArrayWithAtLeastOneOrFallback' type ViewBreakpoint = ViewType | undefined @@ -29,10 +30,13 @@ export interface ReportsStringOverrides { } export interface ReportsProps { - title?: string // deprecated + /** + * @deprecated Use `stringOverrides.title` instead + */ + title?: string showTitle?: boolean stringOverrides?: ReportsStringOverrides - enabledReports?: ReportType[] + enabledReports?: ReadonlyArrayWithAtLeastOne comparisonConfig?: ProfitAndLossCompareConfig profitAndLossConfig?: TimeRangePickerConfig statementOfCashFlowConfig?: TimeRangePickerConfig @@ -49,7 +53,7 @@ export interface ReportsPanelProps { view: ViewBreakpoint } -const getOptions = (enabledReports: ReportType[]) => { +const getOptions = (enabledReports: ReadonlyArray) => { return [ enabledReports.includes('profitAndLoss') ? { @@ -88,7 +92,7 @@ export const Reports = ({ const defaultTitle = enabledReports.length > 1 ? 'Reports' - : options.find(option => (option.value = enabledReports[0]))?.label + : options.find(option => (option.value === enabledReports[0]))?.label return ( Date: Tue, 20 May 2025 14:40:09 -0700 Subject: [PATCH 3/4] chore: clean-up `noUncheckedIndexedAccess` across components --- .../BusinessForm.tsx | 8 +++- .../BankTransactionMobileList/MatchForm.tsx | 17 ++++--- .../PersonalForm.tsx | 8 +++- .../BankTransactionMobileList/SplitForm.tsx | 47 +++++++++++++++---- .../BankTransactionMobileList/utils.ts | 9 ++-- .../BankTransactionReceipts.tsx | 21 ++++++++- .../BankTransactionRow/BankTransactionRow.tsx | 1 + .../CategorySelect/CategorySelect.tsx | 12 +++-- .../ErrorBoundary/ErrorBoundary.tsx | 4 +- .../ExpandedBankTransactionRow.tsx | 47 ++++++++++++++----- .../useUpdateOpeningBalanceAndDate.ts | 2 +- src/components/PeriodPicker/PeriodPicker.tsx | 3 ++ .../PlatformOnboarding/PlatformOnboarding.tsx | 4 +- .../DetailedChart.tsx | 4 +- .../DetailedTable.tsx | 31 +++++++----- .../ProfitAndLossSummariesMiniChart.tsx | 2 +- .../ProfitAndLossCompareTable.tsx | 10 ++-- src/components/Tabs/Tabs.tsx | 2 +- src/components/Toggle/Toggle.tsx | 18 ++++--- src/components/Typography/Text.tsx | 6 +-- tsconfig.json | 1 + 21 files changed, 179 insertions(+), 78 deletions(-) diff --git a/src/components/BankTransactionMobileList/BusinessForm.tsx b/src/components/BankTransactionMobileList/BusinessForm.tsx index 8eefd9599..aa65a1553 100644 --- a/src/components/BankTransactionMobileList/BusinessForm.tsx +++ b/src/components/BankTransactionMobileList/BusinessForm.tsx @@ -167,7 +167,13 @@ export const BusinessForm = ({
{showReceiptUploads && ( receiptsRef.current?.uploadReceipt(files[0])} + onUpload={(files) => { + const firstFile = files[0] + + if (firstFile) { + receiptsRef.current?.uploadReceipt(firstFile) + } + }} text='Upload receipt' iconOnly={true} icon={} diff --git a/src/components/BankTransactionMobileList/MatchForm.tsx b/src/components/BankTransactionMobileList/MatchForm.tsx index d85670f25..13dfabb03 100644 --- a/src/components/BankTransactionMobileList/MatchForm.tsx +++ b/src/components/BankTransactionMobileList/MatchForm.tsx @@ -28,13 +28,10 @@ export const MatchForm = ({ const { match: matchBankTransaction, isLoading } = useBankTransactionsContext() - const [selectedMatchId, setSelectedMatchId] = useState( - isAlreadyMatched(bankTransaction) - ?? (bankTransaction.suggested_matches - && bankTransaction.suggested_matches?.length > 0 - ? bankTransaction.suggested_matches[0].id - : undefined), + const [selectedMatchId, setSelectedMatchId] = useState(() => + isAlreadyMatched(bankTransaction) ?? (bankTransaction.suggested_matches?.[0]?.id), ) + const [formError, setFormError] = useState() const showRetry = Boolean(bankTransaction.error) @@ -106,7 +103,13 @@ export const MatchForm = ({
{showReceiptUploads && ( receiptsRef.current?.uploadReceipt(files[0])} + onUpload={(files) => { + const firstFile = files[0] + + if (firstFile) { + receiptsRef.current?.uploadReceipt(firstFile) + } + }} text='Upload receipt' iconOnly={true} icon={} diff --git a/src/components/BankTransactionMobileList/PersonalForm.tsx b/src/components/BankTransactionMobileList/PersonalForm.tsx index e6d6c15e0..51cea6379 100644 --- a/src/components/BankTransactionMobileList/PersonalForm.tsx +++ b/src/components/BankTransactionMobileList/PersonalForm.tsx @@ -101,7 +101,13 @@ export const PersonalForm = ({
{showReceiptUploads && ( receiptsRef.current?.uploadReceipt(files[0])} + onUpload={(files) => { + const firstFile = files[0] + + if (firstFile) { + receiptsRef.current?.uploadReceipt(firstFile) + } + }} text='Upload receipt' iconOnly={true} icon={} diff --git a/src/components/BankTransactionMobileList/SplitForm.tsx b/src/components/BankTransactionMobileList/SplitForm.tsx index 4e6aca73a..3bc63d922 100644 --- a/src/components/BankTransactionMobileList/SplitForm.tsx +++ b/src/components/BankTransactionMobileList/SplitForm.tsx @@ -114,8 +114,11 @@ export const SplitForm = ({ return sum + amount }, 0) const remaining = bankTransaction.amount - splitTotal - newSplits[0].amount = remaining - newSplits[0].inputValue = formatMoney(remaining) + + if (newSplits[0]) { + newSplits[0].amount = remaining + newSplits[0].inputValue = formatMoney(remaining) + } updateRowState({ ...rowState, @@ -134,10 +137,16 @@ export const SplitForm = ({ return sum + amount }, 0) const remaining = bankTransaction.amount - splitTotal - rowState.splits[rowNumber].amount = newAmount - rowState.splits[rowNumber].inputValue = newDisplaying - rowState.splits[0].amount = remaining - rowState.splits[0].inputValue = formatMoney(remaining) + + if (rowState.splits[rowNumber]) { + rowState.splits[rowNumber].amount = newAmount + rowState.splits[rowNumber].inputValue = newDisplaying + } + if (rowState.splits[0]) { + rowState.splits[0].amount = remaining + rowState.splits[0].inputValue = formatMoney(remaining) + } + updateRowState({ ...rowState }) setFormError(undefined) } @@ -145,14 +154,25 @@ export const SplitForm = ({ const onBlur = (event: React.FocusEvent) => { if (event.target.value === '') { const [_, index] = event.target.name.split('-') - rowState.splits[parseInt(index)].inputValue = '0.00' + if (!index) { + return + } + + const parsedIndex = parseInt(index) + + if (rowState.splits[parsedIndex]) { + rowState.splits[parsedIndex].inputValue = '0.00' + } + updateRowState({ ...rowState }) setFormError(undefined) } } const changeCategory = (index: number, newValue: CategoryOption) => { - rowState.splits[index].category = newValue + if (rowState.splits[index]) { + rowState.splits[index].category = newValue + } updateRowState({ ...rowState }) setFormError(undefined) } @@ -204,7 +224,7 @@ export const SplitForm = ({ await categorizeBankTransaction( bankTransaction.id, - rowState.splits.length === 1 && rowState?.splits[0].category + rowState.splits.length === 1 && rowState?.splits[0]?.category ? ({ type: 'Category', category: getCategorizePayload(rowState?.splits[0].category), @@ -314,7 +334,14 @@ export const SplitForm = ({
{showReceiptUploads && ( receiptsRef.current?.uploadReceipt(files[0])} + onUpload={(files) => { + const firstFile = files[0] + if (!firstFile) { + return + } + + receiptsRef.current?.uploadReceipt(firstFile) + }} text='Upload receipt' iconOnly={true} icon={} diff --git a/src/components/BankTransactionMobileList/utils.ts b/src/components/BankTransactionMobileList/utils.ts index 49400464b..8720e9085 100644 --- a/src/components/BankTransactionMobileList/utils.ts +++ b/src/components/BankTransactionMobileList/utils.ts @@ -85,10 +85,11 @@ export const getAssignedValue = ( } if (hasSuggestions(bankTransaction.categorization_flow)) { - const firstSuggestion = ( - bankTransaction.categorization_flow - ).suggestions[0] - return mapCategoryToOption(firstSuggestion) + const firstSuggestion = bankTransaction.categorization_flow.suggestions[0] + + if (firstSuggestion) { + return mapCategoryToOption(firstSuggestion) + } } return diff --git a/src/components/BankTransactionReceipts/BankTransactionReceipts.tsx b/src/components/BankTransactionReceipts/BankTransactionReceipts.tsx index 2258e18b8..f233ad5d7 100644 --- a/src/components/BankTransactionReceipts/BankTransactionReceipts.tsx +++ b/src/components/BankTransactionReceipts/BankTransactionReceipts.tsx @@ -102,7 +102,17 @@ const BankTransactionReceipts = forwardRef< : null} {!hideUploadButtons && (!receiptUrls || receiptUrls.length === 0) ? ( - void uploadReceipt(files[0])} text='Upload receipt' /> + { + const firstFile = files[0] + if (!firstFile) { + return + } + + void uploadReceipt(firstFile) + }} + text='Upload receipt' + /> ) : null} {receiptUrls.map((url, index) => ( @@ -132,7 +142,14 @@ const BankTransactionReceipts = forwardRef< ? ( void uploadReceipt(files[0])} + onUpload={(files) => { + const firstFile = files[0] + if (!firstFile) { + return + } + + void uploadReceipt(firstFile) + }} text='Add next receipt' /> ) diff --git a/src/components/BankTransactionRow/BankTransactionRow.tsx b/src/components/BankTransactionRow/BankTransactionRow.tsx index 7c1900370..ffc7e0fc4 100644 --- a/src/components/BankTransactionRow/BankTransactionRow.tsx +++ b/src/components/BankTransactionRow/BankTransactionRow.tsx @@ -71,6 +71,7 @@ export const getDefaultSelectedCategory = ( if ( hasSuggestions(bankTransaction.categorization_flow) && bankTransaction.categorization_flow.suggestions.length > 0 + && bankTransaction.categorization_flow.suggestions[0] ) { return mapCategoryToOption( bankTransaction.categorization_flow.suggestions[0], diff --git a/src/components/CategorySelect/CategorySelect.tsx b/src/components/CategorySelect/CategorySelect.tsx index 740d283fc..0b2e798d2 100644 --- a/src/components/CategorySelect/CategorySelect.tsx +++ b/src/components/CategorySelect/CategorySelect.tsx @@ -316,15 +316,17 @@ export const CategorySelect = ({ const selected = value ? value - : !excludeMatches - && matchOptions?.length === 1 - && matchOptions[0].options.length === 1 + : ( + !excludeMatches + && matchOptions?.[0]?.options.length === 1 + ) ? matchOptions[0].options[0] : undefined + const matchOptionsCount = matchOptions?.[0]?.options.length ?? 0 const placeholder = - matchOptions?.length === 1 && matchOptions[0].options.length > 1 - ? `${matchOptions[0].options.length} possible matches...` + matchOptionsCount > 1 + ? `${matchOptionsCount} possible matches...` : 'Categorize or match...' if (asDrawer) { diff --git a/src/components/ErrorBoundary/ErrorBoundary.tsx b/src/components/ErrorBoundary/ErrorBoundary.tsx index 7255656fd..8fd5e42db 100644 --- a/src/components/ErrorBoundary/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -27,7 +27,7 @@ export class ErrorBoundary extends Component< return { hasError: true } } - componentDidCatch(error: Error, _info: ErrorInfo) { + override componentDidCatch(error: Error, _info: ErrorInfo) { if (this.onError) { this.onError({ type: 'render', payload: error }) } @@ -36,7 +36,7 @@ export class ErrorBoundary extends Component< } } - render() { + override render() { if (this.state.hasError) { return } diff --git a/src/components/ExpandedBankTransactionRow/ExpandedBankTransactionRow.tsx b/src/components/ExpandedBankTransactionRow/ExpandedBankTransactionRow.tsx index bc28561a3..59e315ce0 100644 --- a/src/components/ExpandedBankTransactionRow/ExpandedBankTransactionRow.tsx +++ b/src/components/ExpandedBankTransactionRow/ExpandedBankTransactionRow.tsx @@ -201,8 +201,11 @@ const ExpandedBankTransactionRow = forwardRef( return sum + amount }, 0) const remaining = bankTransaction.amount - splitTotal - newSplits[0].amount = remaining - newSplits[0].inputValue = formatMoney(remaining) + + if (newSplits[0]) { + newSplits[0].amount = remaining + newSplits[0].inputValue = formatMoney(remaining) + } updateRowState({ ...rowState, @@ -222,7 +225,9 @@ const ExpandedBankTransactionRow = forwardRef( // Limit to two digits after the decimal point if (parts.length === 2) { - sanitized = parts[0] + '.' + parts[1].slice(0, 2) + const secondPart = parts[1]?.slice(0, 2) ?? '' + + sanitized = parts[0] + '.' + secondPart } return sanitized @@ -240,10 +245,16 @@ const ExpandedBankTransactionRow = forwardRef( }, 0) const remaining = bankTransaction.amount - splitTotal - rowState.splits[rowNumber].amount = newAmount - rowState.splits[rowNumber].inputValue = newDisplaying - rowState.splits[0].amount = remaining - rowState.splits[0].inputValue = formatMoney(remaining) + + if (rowState.splits[rowNumber]) { + rowState.splits[rowNumber].amount = newAmount + rowState.splits[rowNumber].inputValue = newDisplaying + } + if (rowState.splits[0]) { + rowState.splits[0].amount = remaining + rowState.splits[0].inputValue = formatMoney(remaining) + } + updateRowState({ ...rowState }) setSplitFormError(undefined) } @@ -251,7 +262,16 @@ const ExpandedBankTransactionRow = forwardRef( const onBlur = (event: React.FocusEvent) => { if (event.target.value === '') { const [_, index] = event.target.name.split('-') - rowState.splits[parseInt(index)].inputValue = '0.00' + if (!index) { + return + } + + const parsedIndex = parseInt(index) + + if (rowState.splits[parsedIndex]) { + rowState.splits[parsedIndex].inputValue = '0.00' + } + updateRowState({ ...rowState }) setSplitFormError(undefined) } @@ -268,7 +288,10 @@ const ExpandedBankTransactionRow = forwardRef( } const changeCategory = (index: number, newValue: CategoryOption) => { - rowState.splits[index].category = newValue + if (rowState.splits[index]) { + rowState.splits[index].category = newValue + } + updateRowState({ ...rowState }) setSplitFormError(undefined) } @@ -302,12 +325,14 @@ const ExpandedBankTransactionRow = forwardRef( return } + const firstCategory = rowState.splits[0]?.category + await categorizeBankTransaction( bankTransaction.id, - rowState.splits.length === 1 && rowState?.splits[0].category + rowState.splits.length === 1 && firstCategory ? ({ type: 'Category', - category: getCategorizePayload(rowState?.splits[0].category), + category: getCategorizePayload(firstCategory), } as SingleCategoryUpdate) : ({ type: 'Split', diff --git a/src/components/LinkedAccounts/OpeningBalanceModal/useUpdateOpeningBalanceAndDate.ts b/src/components/LinkedAccounts/OpeningBalanceModal/useUpdateOpeningBalanceAndDate.ts index c2404f915..e9f74891c 100644 --- a/src/components/LinkedAccounts/OpeningBalanceModal/useUpdateOpeningBalanceAndDate.ts +++ b/src/components/LinkedAccounts/OpeningBalanceModal/useUpdateOpeningBalanceAndDate.ts @@ -141,7 +141,7 @@ export function useBulkSetOpeningBalanceAndDate( .then((results) => { const resultsWithIds: OpeningBalanceAPIResponseResult[] = results.map((r, i) => ({ ...r, - accountId: data[i].accountId, + accountId: data?.[i]?.accountId ?? '', })) return onSuccess?.(resultsWithIds) }) diff --git a/src/components/PeriodPicker/PeriodPicker.tsx b/src/components/PeriodPicker/PeriodPicker.tsx index 9c90f1d2c..6849a0b9e 100644 --- a/src/components/PeriodPicker/PeriodPicker.tsx +++ b/src/components/PeriodPicker/PeriodPicker.tsx @@ -213,7 +213,10 @@ export const PeriodPicker = ({ onSelect, defaultValue }: PeriodPickerProps) => { } return { + // @ts-expect-error Guaranteed to produce `YYYY-MM-DD` start_date: startDate.toISOString().split('T')[0], + + // @ts-expect-error Guaranteed to produce `YYYY-MM-DD` end_date: endDate.toISOString().split('T')[0], } } diff --git a/src/components/PlatformOnboarding/PlatformOnboarding.tsx b/src/components/PlatformOnboarding/PlatformOnboarding.tsx index 8e3c13ee9..ee18b6463 100644 --- a/src/components/PlatformOnboarding/PlatformOnboarding.tsx +++ b/src/components/PlatformOnboarding/PlatformOnboarding.tsx @@ -23,7 +23,7 @@ const PLATFORM_ONBOARDING_STEPS = [ id: 'summary', label: 'Summary', }, -] +] as const type PlatformOnboardingStepKey = typeof PLATFORM_ONBOARDING_STEPS[number]['id'] @@ -32,7 +32,7 @@ type PlatformOnboardingProps = { } export const PlatformOnboarding = ({ onComplete }: PlatformOnboardingProps) => { - const [step, setStep] = useState(PLATFORM_ONBOARDING_STEPS[0].id) + const [step, setStep] = useState(() => PLATFORM_ONBOARDING_STEPS[0].id) const isFirstStep = PLATFORM_ONBOARDING_STEPS[0].id === step diff --git a/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx b/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx index 8a421af95..ccb2f5a9e 100644 --- a/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx +++ b/src/components/ProfitAndLossDetailedCharts/DetailedChart.tsx @@ -113,7 +113,7 @@ export const DetailedChart = ({ animationEasing='ease-in-out' > {chartData.map((entry, index) => { - let fill: string | undefined = typeColorMapping[index].color + let fill: string | undefined = typeColorMapping[index]?.color let active = true if (hoveredItem && entry.name !== hoveredItem) { active = false @@ -135,7 +135,7 @@ export const DetailedChart = ({ ? 'url(#layer-pie-dots-pattern)' : fill, }} - opacity={typeColorMapping[index].opacity} + opacity={typeColorMapping[index]?.opacity ?? 1} onMouseEnter={() => setHoveredItem(entry.name)} onMouseLeave={() => setHoveredItem(undefined)} /> diff --git a/src/components/ProfitAndLossDetailedCharts/DetailedTable.tsx b/src/components/ProfitAndLossDetailedCharts/DetailedTable.tsx index c0ffc0867..73c05fdf9 100644 --- a/src/components/ProfitAndLossDetailedCharts/DetailedTable.tsx +++ b/src/components/ProfitAndLossDetailedCharts/DetailedTable.tsx @@ -33,6 +33,11 @@ export interface TypeColorMapping { opacity: number } +const DEFAULT_COLOR_MAPPING = { + color: '#EEEEF0', + opacity: 1, +} + export const mapTypesToColors = ( data: LineBaseItem[], colorList: string[] = DEFAULT_CHART_COLOR_TYPE, @@ -45,27 +50,31 @@ export const mapTypesToColors = ( const type = obj.name ?? obj.type if (type === 'Uncategorized') { - return { - color: '#EEEEF0', - opacity: 1, - } + return DEFAULT_COLOR_MAPPING } if (!typeToColor[type]) { - typeToColor[type] = colorList[colorIndex % colorList.length] + const color = colorList[colorIndex % colorList.length] + if (color) { + typeToColor[type] = color + } colorIndex++ typeToLastOpacity[type] = 1 } else { - typeToLastOpacity[type] -= 0.1 + if (typeToLastOpacity[type]) { + typeToLastOpacity[type] -= 0.1 + } } + const color = typeToColor[type] const opacity = typeToLastOpacity[type] - return { - color: typeToColor[type], - opacity: opacity, + if (!color || !opacity) { + return DEFAULT_COLOR_MAPPING } + + return { color, opacity } }) } @@ -115,8 +124,8 @@ const ValueIcon = ({
) diff --git a/src/components/ProfitAndLossSummaries/internal/ProfitAndLossSummariesMiniChart.tsx b/src/components/ProfitAndLossSummaries/internal/ProfitAndLossSummariesMiniChart.tsx index 1f45b40f3..5608a484e 100644 --- a/src/components/ProfitAndLossSummaries/internal/ProfitAndLossSummariesMiniChart.tsx +++ b/src/components/ProfitAndLossSummaries/internal/ProfitAndLossSummariesMiniChart.tsx @@ -98,7 +98,7 @@ export function ProfitAndLossSummariesMiniChart({ animationEasing='ease-in-out' > {data.map((entry, index) => { - const colorConfig = typeColorMapping[index] + const colorConfig = typeColorMapping[index] ?? { color: '#e6e6e6', opacity: 1 } return ( {comparePeriods - && Array.from({ length: comparePeriods - 1 }, (_, index) => ( - - ))} + && Array.from({ length: comparePeriods - 1 }, (_, index) => ( + + ))} ))} diff --git a/src/components/Tabs/Tabs.tsx b/src/components/Tabs/Tabs.tsx index 18aca4f8e..1bbbcdaba 100644 --- a/src/components/Tabs/Tabs.tsx +++ b/src/components/Tabs/Tabs.tsx @@ -25,7 +25,7 @@ export const Tabs = ({ name, options, selected, onChange }: TabsProps) => { const [thumbPos, setThumbPos] = useState({ left: 0, width: 0 }) const [currentWidth, setCurrentWidth] = useState(0) - const selectedValue = selected || options[0].value + const selectedValue = selected ?? options[0]?.value const baseClassName = classNames( 'Layer__tabs', diff --git a/src/components/Toggle/Toggle.tsx b/src/components/Toggle/Toggle.tsx index 84ae6dda8..ca0066970 100644 --- a/src/components/Toggle/Toggle.tsx +++ b/src/components/Toggle/Toggle.tsx @@ -60,9 +60,7 @@ export const Toggle = ({ const activeOption = useMemo(() => { return selected ? selected - : options.length > 0 - ? options[0].value - : undefined + : options[0]?.value }, [selected, options]) const toggleRef = useElementSize((_a, _b, c) => { @@ -88,18 +86,18 @@ export const Toggle = ({ } const optionsNodes = [...toggleRef.current.children] - .map((x) => { + .map((children) => { if ( - x.className.includes('Layer__tooltip-trigger') - && x.children - && x.children.length > 0 + children.className.includes('Layer__tooltip-trigger') + && children.children + && children.children.length > 0 ) { - return x.children[0] + return children.children[0] } - return x + return children }) - .filter(c => c.className.includes('Layer__toggle-option')) + .filter(children => children?.className.includes('Layer__toggle-option')) let shift = 0 let width = thumbPos.width diff --git a/src/components/Typography/Text.tsx b/src/components/Typography/Text.tsx index 7362fec9c..0ba3e1800 100644 --- a/src/components/Typography/Text.tsx +++ b/src/components/Typography/Text.tsx @@ -98,9 +98,9 @@ export const TextWithTooltip = ({ const textElementRef = useRef() const compareSize = () => { if (textElementRef.current) { - const compare = - textElementRef.current.children[0].scrollWidth - > textElementRef.current.children[0].clientWidth + const onlyChild = textElementRef.current.children[0] + const compare = (onlyChild?.scrollWidth ?? 0) > (onlyChild?.clientWidth ?? 0) + setHover(compare) } } diff --git a/tsconfig.json b/tsconfig.json index 7a490690f..5df40577b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ /* We are transpiling with esbuild, NOT TypeScript */ "module": "preserve", + "moduleResolution": "bundler", "noEmit": true, /* Our code runs in the DOM */ From 5a4f693c4fe8c17706c2fb93d063636c40c87c87 Mon Sep 17 00:00:00 2001 From: Kesin Ryan Dehejia Date: Wed, 25 Jun 2025 11:57:55 -0700 Subject: [PATCH 4/4] fix: a few more `noUncheckedIndexAccess` --- src/components/Bills/useBillsRecordPayment.ts | 2 +- src/components/CsvUpload/CopyTemplateHeadersButtonGroup.tsx | 6 +++--- src/components/CsvUpload/CsvUpload.tsx | 6 ++++++ src/components/CsvUpload/ValidateCsvTable.tsx | 5 +++++ src/hooks/useBills.tsx | 6 +++--- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/components/Bills/useBillsRecordPayment.ts b/src/components/Bills/useBillsRecordPayment.ts index c81b22d36..395086b4f 100644 --- a/src/components/Bills/useBillsRecordPayment.ts +++ b/src/components/Bills/useBillsRecordPayment.ts @@ -89,7 +89,7 @@ export const useBillsRecordPayment = ({ refetchAllBills }: { refetchAllBills?: ( setBillsToPay(prev => [ ...prev.slice(0, index), - { bill, amount: prev[index].amount }, + { bill, amount: prev?.[index]?.amount }, ...prev.slice(index + 1), ]) } diff --git a/src/components/CsvUpload/CopyTemplateHeadersButtonGroup.tsx b/src/components/CsvUpload/CopyTemplateHeadersButtonGroup.tsx index 48e763789..11f2a48c1 100644 --- a/src/components/CsvUpload/CopyTemplateHeadersButtonGroup.tsx +++ b/src/components/CsvUpload/CopyTemplateHeadersButtonGroup.tsx @@ -15,14 +15,14 @@ interface CopyTemplateHeadersButtonGroupProps { export const CopyTemplateHeadersButtonGroup = ({ headers, className }: CopyTemplateHeadersButtonGroupProps) => { return ( - {Object.keys(headers).map(key => ( + {Object.entries(headers).map(([key, header]) => ( ))} diff --git a/src/components/CsvUpload/CsvUpload.tsx b/src/components/CsvUpload/CsvUpload.tsx index 7dadedd38..b19514967 100644 --- a/src/components/CsvUpload/CsvUpload.tsx +++ b/src/components/CsvUpload/CsvUpload.tsx @@ -108,6 +108,12 @@ export const CsvUpload = ({ file, onFileSelected, replaceDropTarget = false }: C return } + if (!firstFile) { + onFileSelected(null) + setErrorMessage(undefined) + return + } + const maybeErrorMessage = validateCsvFile(firstFile) if (!maybeErrorMessage) { onFileSelected(firstFile) diff --git a/src/components/CsvUpload/ValidateCsvTable.tsx b/src/components/CsvUpload/ValidateCsvTable.tsx index eda7a9bf9..ea292a296 100644 --- a/src/components/CsvUpload/ValidateCsvTable.tsx +++ b/src/components/CsvUpload/ValidateCsvTable.tsx @@ -144,6 +144,11 @@ export function ValidateCsvTable( /> {rowVirtualizer.getVirtualItems().map((virtualRow) => { const row = rows[virtualRow.index] + + if (!row) { + return null + } + return ( ) diff --git a/src/hooks/useBills.tsx b/src/hooks/useBills.tsx index 7579c675c..5b3f9eafa 100644 --- a/src/hooks/useBills.tsx +++ b/src/hooks/useBills.tsx @@ -139,7 +139,7 @@ export const useBills: UseBills = () => { const lastMetadata = useMemo(() => { if (rawResponseData && rawResponseData.length > 0) { - return rawResponseData[rawResponseData.length - 1].meta + return rawResponseData[rawResponseData.length - 1]?.meta } return undefined @@ -149,8 +149,8 @@ export const useBills: UseBills = () => { if (rawResponseData && rawResponseData.length > 0) { const lastElement = rawResponseData[rawResponseData.length - 1] return Boolean( - lastElement.meta?.pagination?.cursor - && lastElement.meta?.pagination?.has_more, + lastElement?.meta?.pagination?.cursor + && lastElement.meta.pagination.has_more, ) }