diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..0335dba --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "next-devtools": { + "command": "npx", + "args": ["-y", "next-devtools-mcp@latest"] + } + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 09c22f9..cb1f343 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "immutability-helper": "^3.1.1", "jschardet": "^3.1.4", "lodash": "^4.17.23", + "loglevel": "^1.9.2", "lru-cache": "^11.2.6", "moment": "^2.30.1", "next": "^16.1.6", @@ -7752,10 +7753,11 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -8256,9 +8258,13 @@ "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" }, "node_modules/d3-color": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz", - "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } }, "node_modules/d3-ease": { "version": "3.0.1", @@ -8591,9 +8597,9 @@ } }, "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -13998,9 +14004,10 @@ "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", @@ -14022,6 +14029,19 @@ "dev": true, "license": "MIT" }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -26013,9 +26033,9 @@ "integrity": "sha512-dJkTHtGBbOLtrmcm37R44jelbgKalMPXLLmhNceEgeLRJLdDTU2DoEF7L+UqM3m36dve7/Vka4hgaacT7a8Jjw==" }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "requires": { "balanced-match": "^1.0.0", @@ -26358,9 +26378,9 @@ "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" }, "d3-color": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz", - "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" }, "d3-ease": { "version": "3.0.1", @@ -26576,9 +26596,9 @@ "dev": true }, "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true }, "direction": { @@ -30170,9 +30190,9 @@ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==" }, "lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==" }, "lodash.isplainobject": { "version": "4.0.6", @@ -30192,6 +30212,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==" + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", diff --git a/package.json b/package.json index b5ec7fc..e731b30 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "immutability-helper": "^3.1.1", "jschardet": "^3.1.4", "lodash": "^4.17.23", + "loglevel": "^1.9.2", "lru-cache": "^11.2.6", "moment": "^2.30.1", "next": "^16.1.6", diff --git a/src/app/api/currencies/route.ts b/src/app/api/currencies/route.ts index 9bb1733..06d45c3 100644 --- a/src/app/api/currencies/route.ts +++ b/src/app/api/currencies/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { fetchRates } from '../../../utils/eurofx'; +import logger from '../../../utils/logger'; // eslint-disable-next-line @typescript-eslint/no-unused-vars export async function GET(_request: NextRequest) { @@ -9,7 +10,7 @@ export async function GET(_request: NextRequest) { return NextResponse.json(currencies); } catch (error) { - console.error('Error fetching currencies:', error); + logger.error('Error fetching currencies:', error); return NextResponse.json( { error: 'Failed to fetch currencies' }, { status: 500 } diff --git a/src/app/api/currencyRates/route.ts b/src/app/api/currencyRates/route.ts index 07d5724..160b51c 100644 --- a/src/app/api/currencyRates/route.ts +++ b/src/app/api/currencyRates/route.ts @@ -1,11 +1,12 @@ import { NextRequest, NextResponse } from 'next/server'; import { fetchRates } from '../../../utils/eurofx'; +import logger from '../../../utils/logger'; export async function GET(request: NextRequest) { try { const url = new URL(request.url); const currencyParams = url.searchParams.getAll('currencies'); - + const options: { historical: boolean; currencies?: string[] } = { historical: true }; if (currencyParams.length > 0) { options.currencies = currencyParams; @@ -22,7 +23,7 @@ export async function GET(request: NextRequest) { return NextResponse.json(ratesObject); } catch (error) { - console.error('Error fetching currency rates:', error); + logger.error('Error fetching currency rates:', error); return NextResponse.json( { error: 'Failed to fetch currency rates' }, { status: 500 } diff --git a/src/components/Charts.tsx b/src/components/Charts.tsx index 168b3c1..e22aafc 100644 --- a/src/components/Charts.tsx +++ b/src/components/Charts.tsx @@ -4,7 +4,7 @@ import { type AppDispatch, Transaction, Account, Category } from '../types/redux import dynamic from 'next/dynamic'; import moment, { Moment } from 'moment'; import { nest } from 'd3-collection'; -import { TransactionGroup } from '../types/app'; +import { type TransactionGroup, type CategoryExpense } from '../types/app'; import { sum } from 'd3-array'; import { createSelector } from 'reselect'; import { uncategorized } from '../data/categories'; @@ -18,15 +18,6 @@ import AmountSumBar from './charts/AmountSumBar'; import IncomeExpensesLine from './charts/IncomeExpensesLine'; import Loading from './shared/Loading'; -interface CategoryExpense { - key: string; - value: { - amount: number; - transactions: Transaction[]; - category: Category; - }; -} - const CategoryExpenses = dynamic(() => import('./charts/CategoryExpenses'), { loading: () => }); @@ -190,15 +181,15 @@ const getSortedCategoryExpenses = createSelector( [getIncomeAndExpenses], (incomeAndExpenses: Transaction[][]): CategoryExpense[] => { const expenses = incomeAndExpenses[1]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (nest() as any) + return (nest() .key((d: Transaction) => (d.category as unknown as Category).id) + // @ts-expect-error d3-collection types: .key() this-return doesn't track .rollup() generic .rollup((transactions: Transaction[]) => ({ transactions, category: transactions[0].category as unknown as Category, amount: Math.abs(sum(transactions, (d: Transaction) => d.amount)) })) - .entries(expenses) + .entries(expenses) as unknown as CategoryExpense[]) .sort((a: CategoryExpense, b: CategoryExpense) => b.value.amount - a.value.amount); } ); diff --git a/src/components/Intro.test.tsx b/src/components/Intro.test.tsx index acd7ad4..6eb3361 100644 --- a/src/components/Intro.test.tsx +++ b/src/components/Intro.test.tsx @@ -71,7 +71,21 @@ describe('Intro Component', () => { accounts: { data: [] }, - edit: {}, + edit: { + isCategoryGuessing: false, + isParsing: false, + isFetchingCurrencies: false, + isFetchingCurrencyRates: false, + dateSelect: {}, + transactionList: { + page: 1, + pageSize: 50, + sortKey: 'date', + sortAscending: false, + filterCategories: new Set() + }, + charts: {} + }, search: { transactions: { text: '', diff --git a/src/components/Transactions.tsx b/src/components/Transactions.tsx index 42d7909..bc2906e 100644 --- a/src/components/Transactions.tsx +++ b/src/components/Transactions.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useState, useRef } from 'react'; import moment, { Moment } from 'moment'; import { useSelector, useDispatch } from 'react-redux'; import { type AppDispatch, type Transaction, type Category } from '../types/redux'; -import type { TransactionGroup } from '../types/app'; +import type { TransactionGroup, DisplayTransaction, DisplayTransactionGroup } from '../types/app'; import Link from 'next/link'; import { createSearchAction } from 'redux-search'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -37,9 +37,8 @@ export default function Transactions() { return obj; }, {}); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let transactionsData: any[] = state.transactions.data - .map((t: Transaction) => { + let transactionsData: DisplayTransaction[] = state.transactions.data + .map((t: Transaction): DisplayTransaction => { return { categoryGuess: (t.category.guess && categoriesObj[t.category.guess]) || null, categoryConfirmed: (t.category.confirmed && categoriesObj[t.category.confirmed]) || null, @@ -55,8 +54,7 @@ export default function Transactions() { // Create an ID -> transactions mapping for easier tooltip'ing. const transactionGroupsObj = Object.entries(state.transactions.groups || {}) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .reduce((obj: Record, [groupId, group]: [string, TransactionGroup]) => { + .reduce((obj: Record, [groupId, group]: [string, TransactionGroup]) => { obj[group.primaryId] = { groupId, linkedTransactions: group.linkedIds.map((id: string) => transactionsData[reverseTransactionLookup[id]]) diff --git a/src/components/charts/AmountCard.tsx b/src/components/charts/AmountCard.tsx index 02ca6ce..f66603a 100644 --- a/src/components/charts/AmountCard.tsx +++ b/src/components/charts/AmountCard.tsx @@ -5,7 +5,7 @@ import { formatNumber } from '../../util'; interface Account { name: string; - currency: string; + currency?: string; } interface Amounts { diff --git a/src/components/charts/AmountSumBar.tsx b/src/components/charts/AmountSumBar.tsx index a48d576..1f43932 100644 --- a/src/components/charts/AmountSumBar.tsx +++ b/src/components/charts/AmountSumBar.tsx @@ -14,6 +14,7 @@ import { sum, ascending } from 'd3-array'; import color from '../../data/color'; import { formatNumber } from '../../util'; import { Transaction } from '../../types/redux'; +import { type NestEntry } from '../../types/app'; interface Props { transactions: Transaction[]; @@ -23,9 +24,9 @@ export default function AmountSumBar({ transactions }: Props) { const data = nest() .key((d: Transaction) => d.date.substring(0, 7)) .sortKeys(ascending) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .rollup((a: Transaction[]) => sum(a, (d: Transaction) => d.amount) as any) - .entries(transactions); + // @ts-expect-error d3-collection types: .key() this-return doesn't track .rollup() generic + .rollup((a: Transaction[]) => sum(a, (d: Transaction) => d.amount)) + .entries(transactions) as unknown as NestEntry[]; return (
diff --git a/src/components/charts/CategoryExpenses.tsx b/src/components/charts/CategoryExpenses.tsx index 04913ff..89b5f22 100644 --- a/src/components/charts/CategoryExpenses.tsx +++ b/src/components/charts/CategoryExpenses.tsx @@ -3,20 +3,8 @@ import CategorySelect from '../shared/CategorySelect'; import CategoryTreeMap from './CategoryTreeMap'; import CategoryLine from './CategoryLine'; import { Moment } from 'moment'; -import { Transaction, Category } from '../../types/redux'; - -interface CategoryOption { - label: string; - value: string; -} - -interface CategoryExpense { - value: { - amount: number; - category: Category; - transactions: Transaction[]; - }; -} +import { Category } from '../../types/redux'; +import { type CategoryOption, type CategoryExpense } from '../../types/app'; interface Props { handleCategoryChange: (categoryIds: string[]) => void; diff --git a/src/components/charts/CategoryLine.tsx b/src/components/charts/CategoryLine.tsx index ff9c694..76d30c3 100644 --- a/src/components/charts/CategoryLine.tsx +++ b/src/components/charts/CategoryLine.tsx @@ -2,15 +2,8 @@ import React from 'react'; import { schemeCategory10 } from 'd3-scale-chromatic'; import { Moment } from 'moment'; import CustomLineChart from '../shared/CustomLineChart'; -import { Transaction, Category } from '../../types/redux'; - -interface CategoryExpense { - value: { - amount: number; - category: Category; - transactions: Transaction[]; - }; -} +import { Transaction } from '../../types/redux'; +import { type CategoryExpense } from '../../types/app'; interface Props { sortedCategoryExpenses: CategoryExpense[]; diff --git a/src/components/charts/CategoryTreeMap.tsx b/src/components/charts/CategoryTreeMap.tsx index b2a8c54..7066bc2 100644 --- a/src/components/charts/CategoryTreeMap.tsx +++ b/src/components/charts/CategoryTreeMap.tsx @@ -6,14 +6,7 @@ import { } from 'recharts'; import color from '../../data/color'; import { formatNumber } from '../../util'; -import { Category } from '../../types/redux'; - -interface CategoryExpense { - value: { - amount: number; - category: Category; - }; -} +import { type CategoryExpense } from '../../types/app'; interface Props { sortedCategoryExpenses: CategoryExpense[]; diff --git a/src/components/charts/IncomeExpensesLine.tsx b/src/components/charts/IncomeExpensesLine.tsx index 3da2572..4b09edd 100644 --- a/src/components/charts/IncomeExpensesLine.tsx +++ b/src/components/charts/IncomeExpensesLine.tsx @@ -5,6 +5,7 @@ import color from '../../data/color'; import CustomLineChart from '../shared/CustomLineChart'; import { Moment } from 'moment'; import { Transaction } from '../../types/redux'; +import { type NestEntry } from '../../types/app'; interface Props { transactions: Transaction[]; @@ -23,20 +24,20 @@ export default function IncomeExpensesLine({ transactions, startDate, endDate }: const keySelector = endDate.diff(startDate, 'month', true) < 2 ? (d: Transaction) => d.date : (d: Transaction) => d.date.substring(0, 7) - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const data = (nest() as any) + + const data = nest() .key(keySelector) .sortKeys(ascending) + // @ts-expect-error d3-collection types: .key() this-return doesn't track .rollup() generic .rollup((a: Transaction[]) => { return { expenses: Math.abs(sum(a, d => (d.amount < 0 ? d.amount : 0))), income: sum(a, d => (d.amount >= 0 ? d.amount : 0)) } }) - .entries(transactions); + .entries(transactions) as unknown as NestEntry<{ expenses: number; income: number }>[]; - const processedData: ProcessedData[] = data.map((d: { key: string; value: { expenses: number; income: number } }) => ({ + const processedData: ProcessedData[] = data.map((d) => ({ key: d.key, expenses: d.value.expenses, income: d.value.income diff --git a/src/components/charts/Summary.tsx b/src/components/charts/Summary.tsx index 01fd4a4..bca3945 100644 --- a/src/components/charts/Summary.tsx +++ b/src/components/charts/Summary.tsx @@ -3,16 +3,8 @@ import { nest } from 'd3-collection'; import { sum } from 'd3-array'; import { formatNumber } from '../../util'; import AmountCard from './AmountCard'; -import { Transaction, Account, Category } from '../../types/redux'; - -interface CategoryExpense { - key: string; - value: { - amount: number; - transactions: Transaction[]; - category: Category; - }; -} +import { Transaction, Account } from '../../types/redux'; +import { type CategoryExpense, type NestEntry } from '../../types/app'; interface SummaryProps { expenses: Transaction[]; @@ -22,15 +14,15 @@ interface SummaryProps { } const groupAmounts = (transactions: Transaction[], accounts: { [key: string]: Account }) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (nest() as any) + return (nest() .key((t: Transaction) => t.account || '') + // @ts-expect-error d3-collection types: .key() this-return doesn't track .rollup() generic .rollup((t: Transaction[]) => ({ amount: sum(t, d => d.amount), originalAmount: sum(t, d => (d as unknown as Record).originalAmount || 0) })) - .entries(transactions) - .map((e: { key: string; value: { amount: number; originalAmount: number } }) => ({ account: accounts[e.key], amounts: e.value })); + .entries(transactions) as unknown as NestEntry<{ amount: number; originalAmount: number }>[]) + .map((e) => ({ account: accounts[e.key], amounts: e.value })); }; export default function Summary({ expenses, income, sortedCategoryExpenses, accounts }: SummaryProps) { diff --git a/src/components/shared/CategorySelect.tsx b/src/components/shared/CategorySelect.tsx index 3e3a5da..6898189 100644 --- a/src/components/shared/CategorySelect.tsx +++ b/src/components/shared/CategorySelect.tsx @@ -5,11 +5,7 @@ import CreatableSelect from 'react-select/creatable'; import { uncategorized } from '../../data/categories'; import { arrayToObjectLookup } from '../../util'; import type { RootState } from '../../reducers'; - -interface CategoryOption { - label: string; - value: string; -} +import type { CategoryOption } from '../../types/app'; interface Props { onChange: (option: CategoryOption[] | CategoryOption | null) => void; @@ -22,14 +18,20 @@ interface Props { selectOptions?: Record; } -const findValue = (value: Set | string[] | string | CategoryOption | CategoryOption[] | null | undefined, options: CategoryOption[]) => { +const findValue = ( + value: Set | string[] | string | CategoryOption | CategoryOption[] | null | undefined, + options: CategoryOption[] +): CategoryOption | CategoryOption[] | null | undefined => { const normalizedValue = value instanceof Set ? Array.from(value) : value; // If we are given an array of strings, select the options with the value of // those strings. - if (Array.isArray(normalizedValue) && normalizedValue.length && typeof normalizedValue[0] === 'string') { - const stringValues = normalizedValue as string[]; - return options.filter(o => !!stringValues.find((c: string) => c === o.value)); + if (Array.isArray(normalizedValue)) { + if (normalizedValue.length && typeof normalizedValue[0] === 'string') { + const stringValues = normalizedValue as string[]; + return options.filter(o => !!stringValues.find((c: string) => c === o.value)); + } + return normalizedValue as CategoryOption[]; } if (typeof normalizedValue === 'string') { return options.find(o => o.value === normalizedValue); @@ -70,8 +72,9 @@ export default function CategorySelect({ ].concat(categoryOptions); }, [categories]); - const onChangeInternal = (option: CategoryOption | CategoryOption[] | null) => { - onChange(option); + const onChangeInternal = (option: CategoryOption | readonly CategoryOption[] | null) => { + // Convert readonly arrays from react-select to mutable arrays for our onChange prop + onChange(Array.isArray(option) ? [...option] : option as CategoryOption | null); }; const selectedCategoryValue = useMemo( @@ -79,15 +82,13 @@ export default function CategorySelect({ [selectedCategory, categoryOptionsWithUncategorized] ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const commonProps: any = { + const baseProps = { options: categoryOptionsWithUncategorized, placeholder: placeholder, onChange: onChangeInternal, value: selectedCategoryValue, isMulti: isMulti, autoFocus: focus, - ...selectOptions }; // Assume it's a creatable if the onCreate function is given @@ -100,7 +101,8 @@ export default function CategorySelect({ className="category-select creatable" onCreateOption={onCreateOption} - {...commonProps} + {...selectOptions} + {...baseProps} /> ); } @@ -108,7 +110,8 @@ export default function CategorySelect({ return (