diff --git a/packages/desktop-client/src/components/common/Block.tsx b/packages/desktop-client/src/components/common/Block.tsx index a1603aef0cf..7be0dd3590b 100644 --- a/packages/desktop-client/src/components/common/Block.tsx +++ b/packages/desktop-client/src/components/common/Block.tsx @@ -2,8 +2,11 @@ import { type HTMLProps, type Ref } from 'react'; import { css } from 'glamor'; +import { type CSSProperties } from '../../style'; + type BlockProps = HTMLProps & { innerRef?: Ref; + style?: CSSProperties; }; export function Block(props: BlockProps) { diff --git a/packages/desktop-client/src/components/reports/ChooseGraph.tsx b/packages/desktop-client/src/components/reports/ChooseGraph.tsx index c02c5d8890c..2019433b483 100644 --- a/packages/desktop-client/src/components/reports/ChooseGraph.tsx +++ b/packages/desktop-client/src/components/reports/ChooseGraph.tsx @@ -12,7 +12,6 @@ import { LineGraph } from './graphs/LineGraph'; import { StackedBarGraph } from './graphs/StackedBarGraph'; import { ReportTable } from './graphs/tableGraph/ReportTable'; import { ReportTableHeader } from './graphs/tableGraph/ReportTableHeader'; -import { ReportTableList } from './graphs/tableGraph/ReportTableList'; import { ReportTableTotals } from './graphs/tableGraph/ReportTableTotals'; import { ReportOptions } from './ReportOptions'; @@ -42,6 +41,12 @@ export function ChooseGraph({ viewLabels, }: ChooseGraphProps) { const balanceTypeOp = ReportOptions.balanceTypeMap.get(balanceType); + const groupByData = + groupBy === 'Category' + ? 'groupedData' + : ['Month', 'Year'].includes(groupBy) + ? 'monthData' + : 'data'; const saveScrollWidth = value => { setScrollWidth(!value ? 0 : value); @@ -128,16 +133,12 @@ export function ChooseGraph({ saveScrollWidth={saveScrollWidth} listScrollRef={listScrollRef} handleScroll={handleScroll} - > - - + balanceTypeOp={balanceTypeOp} + groupBy={groupBy} + data={data[groupByData]} + mode={mode} + monthsCount={months.length} + /> ; - monthData: Array; - groupedData: Array; + data: GroupedEntity[]; + monthData: GroupedEntity[]; + groupedData: GroupedEntity[]; legend: LegendEntity[]; startDate: string; endDate: string; @@ -31,7 +31,7 @@ export type MonthData = { totalTotals: number; }; -type GroupedEntity = { +export type GroupedEntity = { id: string; name: string; date?: string; diff --git a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx index a022eee4a00..d087bea38ad 100644 --- a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx +++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx @@ -1,29 +1,42 @@ // @ts-strict-ignore import React, { - type UIEventHandler, + useCallback, useLayoutEffect, useRef, - type ReactNode, + type UIEventHandler, } from 'react'; import { type RefProp } from 'react-spring'; import { type CSSProperties } from '../../../../style'; +import { Block } from '../../../common/Block'; import { View } from '../../../common/View'; +import { type GroupedEntity } from '../../entities'; + +import { ReportTableList } from './ReportTableList'; +import { ReportTableRow } from './ReportTableRow'; type ReportTableProps = { - saveScrollWidth?: (value: number) => void; - listScrollRef?: RefProp; + saveScrollWidth: (value: number) => void; + listScrollRef: RefProp; + handleScroll: UIEventHandler; style?: CSSProperties; - children?: ReactNode; - handleScroll?: UIEventHandler; + groupBy: string; + balanceTypeOp: 'totalDebts' | 'totalTotals' | 'totalAssets'; + data: GroupedEntity[]; + mode: string; + monthsCount: number; }; export function ReportTable({ saveScrollWidth, listScrollRef, - style, - children, handleScroll, + style, + groupBy, + balanceTypeOp, + data, + mode, + monthsCount, }: ReportTableProps) { const contentRef = useRef(null); @@ -33,25 +46,56 @@ export function ReportTable({ } }); + const renderItem = useCallback( + ({ item, groupByItem, mode, style, key, monthsCount }) => { + return ( + + ); + }, + [], + ); + return ( - -
{children}
-
+ + +
); } diff --git a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableHeader.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableHeader.tsx index 7138c4ba908..5a16c916b38 100644 --- a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableHeader.tsx +++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableHeader.tsx @@ -5,15 +5,15 @@ import { type RefProp } from 'react-spring'; import { styles, theme } from '../../../../style'; import { View } from '../../../common/View'; import { Row, Cell } from '../../../table'; -import { type MonthData } from '../../entities'; +import { type GroupedEntity } from '../../entities'; type ReportTableHeaderProps = { scrollWidth?: number; groupBy: string; - interval?: MonthData[]; + interval?: GroupedEntity[]; balanceType: string; headerScrollRef: RefProp; - handleScroll?: UIEventHandler; + handleScroll: UIEventHandler; }; export function ReportTableHeader({ diff --git a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableList.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableList.tsx index 3034fb293ca..8d27ccfeb36 100644 --- a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableList.tsx +++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableList.tsx @@ -1,244 +1,90 @@ // @ts-strict-ignore -import React, { memo } from 'react'; +import React from 'react'; -import { - amountToCurrency, - amountToInteger, - integerToCurrency, -} from 'loot-core/src/shared/util'; - -import { type CSSProperties, styles, theme } from '../../../../style'; +import { type CSSProperties, theme } from '../../../../style'; import { View } from '../../../common/View'; -import { Row, Cell } from '../../../table'; +import { Cell, Row } from '../../../table'; +import { type GroupedEntity } from '../../entities'; -type TableRowProps = { - item: { - date: string; - name: string; - monthData: []; - totalAssets: number; - totalDebts: number; - }; - balanceTypeOp?: string; - groupByItem: string; - mode: string; - monthsCount: number; - style?: CSSProperties; +type ReportTableListProps = { + data: GroupedEntity[]; + mode?: string; + monthsCount?: number; + groupBy: string; + renderItem; }; -const TableRow = memo( - ({ - item, - balanceTypeOp, - groupByItem, - mode, - monthsCount, - style, - }: TableRowProps) => { - const average = amountToInteger(item[balanceTypeOp]) / monthsCount; - return ( - - 12 && item[groupByItem]} - style={{ - width: 120, - flexShrink: 0, - ...styles.tnum, - }} - /> - {item.monthData && mode === 'time' - ? item.monthData.map(month => { - return ( - 100000 && - amountToCurrency(month[balanceTypeOp]) - } - width="flex" - privacyFilter - /> - ); - }) - : balanceTypeOp === 'totalTotals' && ( - <> - 100000 && - amountToCurrency(item.totalAssets) - } - width="flex" - style={{ - minWidth: 85, - ...styles.tnum, - }} - /> - 100000 && - amountToCurrency(item.totalDebts) - } - width="flex" - style={{ - minWidth: 85, - ...styles.tnum, - }} - /> - - )} - 100000 && - amountToCurrency(item[balanceTypeOp]) - } - style={{ - fontWeight: 600, - minWidth: 85, - ...styles.tnum, - }} - width="flex" - privacyFilter - /> - 100000 && - integerToCurrency(Math.round(average)) - } - style={{ - fontWeight: 600, - minWidth: 85, - ...styles.tnum, - }} - width="flex" - privacyFilter - /> - - ); - }, -); - -function GroupedTableRow({ - item, - balanceTypeOp, - groupByItem, - mode, - monthsCount, - empty, -}) { - return ( - <> - - - {item.categories - .filter(i => - !empty - ? balanceTypeOp === 'totalTotals' - ? i.totalAssets !== 0 || - i.totalDebts !== 0 || - i.totalTotals !== 0 - : i[balanceTypeOp] !== 0 - : true, - ) - .map(cat => { - return ( - - ); - })} - - - - ); -} - export function ReportTableList({ data, - empty, monthsCount, - balanceTypeOp, mode, groupBy, -}) { + renderItem, +}: ReportTableListProps) { const groupByItem = ['Month', 'Year'].includes(groupBy) ? 'date' : 'name'; - const groupByData = - groupBy === 'Category' - ? 'groupedData' - : ['Month', 'Year'].includes(groupBy) - ? 'monthData' - : 'data'; + + type RenderRowProps = { + key: string; + index: number; + parent_index?: number; + style?: CSSProperties; + }; + function RenderRow({ index, parent_index, style, key }: RenderRowProps) { + const item = parent_index + ? data[parent_index].categories[index] + : data[index]; + + return renderItem({ + item, + groupByItem, + mode, + style, + key, + monthsCount, + }); + } return ( - {data[groupByData] - .filter(i => - !empty - ? balanceTypeOp === 'totalTotals' - ? i.totalAssets !== 0 || i.totalDebts !== 0 || i.totalTotals !== 0 - : i[balanceTypeOp] !== 0 - : true, - ) - .map(item => { - if (groupBy === 'Category') { - return ( - - ); - } else { - return ( - - ); - } - })} + {data.map((item, index) => { + return ( + + {data ? ( + <> + + {item.categories && ( + <> + + {item.categories.map((category, i) => { + return ( + + ); + })} + + + + )} + + ) : ( + + )} + + ); + })} ); } diff --git a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableRow.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableRow.tsx new file mode 100644 index 00000000000..63c275a6e29 --- /dev/null +++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableRow.tsx @@ -0,0 +1,135 @@ +import React, { memo } from 'react'; + +import { + amountToCurrency, + amountToInteger, + integerToCurrency, +} from 'loot-core/src/shared/util'; + +import { type CSSProperties, styles, theme } from '../../../../style'; +import { Row, Cell } from '../../../table'; +import { type GroupedEntity } from '../../entities'; + +type ReportTableRowProps = { + item: GroupedEntity; + balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals'; + groupByItem: 'id' | 'name'; + mode: string; + style?: CSSProperties; + monthsCount: number; +}; + +export const ReportTableRow = memo( + ({ + item, + balanceTypeOp, + groupByItem, + mode, + style, + monthsCount, + }: ReportTableRowProps) => { + const average = amountToInteger(item[balanceTypeOp]) / monthsCount; + return ( + + 12 ? item[groupByItem] : undefined} + style={{ + width: 120, + flexShrink: 0, + ...styles.tnum, + }} + /> + {item.monthData && mode === 'time' + ? item.monthData.map(month => { + return ( + 100000 + ? amountToCurrency(month[balanceTypeOp]) + : undefined + } + width="flex" + privacyFilter + /> + ); + }) + : balanceTypeOp === 'totalTotals' && ( + <> + 100000 + ? amountToCurrency(item.totalAssets) + : undefined + } + width="flex" + privacyFilter + style={{ + minWidth: 85, + ...styles.tnum, + }} + /> + 100000 + ? amountToCurrency(item.totalDebts) + : undefined + } + width="flex" + privacyFilter + style={{ + minWidth: 85, + ...styles.tnum, + }} + /> + + )} + 100000 + ? amountToCurrency(item[balanceTypeOp]) + : undefined + } + style={{ + fontWeight: 600, + minWidth: 85, + ...styles.tnum, + }} + width="flex" + privacyFilter + /> + 100000 + ? integerToCurrency(Math.round(average)) + : undefined + } + style={{ + fontWeight: 600, + minWidth: 85, + ...styles.tnum, + }} + width="flex" + privacyFilter + /> + + ); + }, +); diff --git a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx index 4b708a985fa..5b396202518 100644 --- a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx +++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx @@ -110,6 +110,7 @@ export function ReportTableTotals({ amountToCurrency(data.totalAssets) } width="flex" + privacyFilter /> )} diff --git a/packages/loot-core/src/server/accounts/sync.ts b/packages/loot-core/src/server/accounts/sync.ts index ae04efa6758..fa6d3134057 100644 --- a/packages/loot-core/src/server/accounts/sync.ts +++ b/packages/loot-core/src/server/accounts/sync.ts @@ -309,14 +309,15 @@ async function normalizeGoCardlessTransactions(transactions, acctId) { // that it matters whether the amount is a positive or negative zero. if (trans.amount > 0 || Object.is(Number(trans.amount), 0)) { const nameParts = []; - nameParts.push( - title( - trans.debtorName || - trans.remittanceInformationUnstructured || - (trans.remittanceInformationUnstructuredArray || []).join(', ') || - trans.additionalInformation, - ), - ); + const name = + trans.debtorName || + trans.remittanceInformationUnstructured || + (trans.remittanceInformationUnstructuredArray || []).join(', ') || + trans.additionalInformation; + + if (name) { + nameParts.push(title(name)); + } if (trans.debtorAccount && trans.debtorAccount.iban) { nameParts.push( '(' + @@ -329,14 +330,15 @@ async function normalizeGoCardlessTransactions(transactions, acctId) { payee_name = nameParts.join(' '); } else { const nameParts = []; - nameParts.push( - title( - trans.creditorName || - trans.remittanceInformationUnstructured || - (trans.remittanceInformationUnstructuredArray || []).join(', ') || - trans.additionalInformation, - ), - ); + const name = + trans.creditorName || + trans.remittanceInformationUnstructured || + (trans.remittanceInformationUnstructuredArray || []).join(', ') || + trans.additionalInformation; + + if (name) { + nameParts.push(title(name)); + } if (trans.creditorAccount && trans.creditorAccount.iban) { nameParts.push( '(' + diff --git a/packages/loot-core/src/shared/async.ts b/packages/loot-core/src/shared/async.ts index 70cf5150d60..13ad731dc08 100644 --- a/packages/loot-core/src/shared/async.ts +++ b/packages/loot-core/src/shared/async.ts @@ -4,24 +4,25 @@ import { type HandlerFunctions } from '../types/handlers'; export function sequential( fn: T, ): (...args: Parameters) => Promise>> { - const sequenceState = { + const sequenceState: { + running: Promise>> | null; + queue: Array<{ args: Parameters; resolve; reject }>; + } = { running: null, queue: [], }; function pump() { - if (sequenceState.queue.length > 0) { - const next = sequenceState.queue.shift(); + const next = sequenceState.queue.shift(); + if (next !== undefined) { run(next.args, next.resolve, next.reject); } else { sequenceState.running = null; } } - function run(args, resolve, reject) { - sequenceState.running = fn.apply(null, args); - - sequenceState.running.then( + function run(args: Parameters, resolve, reject) { + sequenceState.running = fn.apply(null, args).then( val => { pump(); resolve(val); @@ -48,9 +49,9 @@ export function sequential( export function once( fn: T, -): (...args: Parameters) => Promise>> { - let promise = null; - return (...args) => { +): (...args: Parameters) => Promise>> | null { + let promise: Promise>> | null = null; + return (...args: Parameters) => { if (!promise) { promise = fn.apply(null, args).finally(() => { promise = null; diff --git a/packages/loot-core/src/shared/months.ts b/packages/loot-core/src/shared/months.ts index a481e0a038f..5655104eae9 100644 --- a/packages/loot-core/src/shared/months.ts +++ b/packages/loot-core/src/shared/months.ts @@ -183,7 +183,7 @@ export function _range( end: DateLike, inclusive = false, ): string[] { - const months = []; + const months: string[] = []; let month = monthFromDate(start); while (d.isBefore(_parse(month), _parse(end))) { months.push(month); @@ -210,15 +210,15 @@ export function _dayRange( end: DateLike, inclusive = false, ): string[] { - const days = []; + const days: string[] = []; let day = start; while (d.isBefore(_parse(day), _parse(end))) { - days.push(day); + days.push(dayFromDate(day)); day = addDays(day, 1); } if (inclusive) { - days.push(day); + days.push(dayFromDate(day)); } return days; diff --git a/packages/loot-core/src/shared/query.ts b/packages/loot-core/src/shared/query.ts index bf5dd06eeeb..0dd129b586b 100644 --- a/packages/loot-core/src/shared/query.ts +++ b/packages/loot-core/src/shared/query.ts @@ -1,6 +1,6 @@ // @ts-strict-ignore export type QueryState = { - filterExpressions: Array; + filterExpressions: Array; selectExpressions: Array; groupExpressions: Array; orderExpressions: Array; diff --git a/packages/loot-core/src/shared/schedules.ts b/packages/loot-core/src/shared/schedules.ts index e3f4cb6592d..d8628ddfa0d 100644 --- a/packages/loot-core/src/shared/schedules.ts +++ b/packages/loot-core/src/shared/schedules.ts @@ -4,7 +4,11 @@ import type { IRuleOptions } from '@rschedule/core'; import * as monthUtils from './months'; import { q } from './query'; -export function getStatus(nextDate, completed, hasTrans) { +export function getStatus( + nextDate: string, + completed: boolean, + hasTrans: boolean, +) { const today = monthUtils.currentDay(); if (completed) { @@ -45,7 +49,7 @@ export function getHasTransactionsQuery(schedules) { .select(['schedule', 'date']); } -function makeNumberSuffix(num) { +function makeNumberSuffix(num: number) { // Slight abuse of date-fns to turn a number like "1" into the full // form "1st" but formatting a date with that number return monthUtils.format(new Date(2020, 0, num, 12), 'do'); @@ -128,7 +132,7 @@ export function getRecurringDescription(config, dateFormat) { desc += ' on the '; - const strs = []; + const strs: string[] = []; const uniqueDays = new Set(patterns.map(p => p.type)); const isSameDay = uniqueDays.size === 1 && !uniqueDays.has('day'); diff --git a/packages/loot-core/src/shared/test-helpers.ts b/packages/loot-core/src/shared/test-helpers.ts index ec73e05230a..fbdf98896e3 100644 --- a/packages/loot-core/src/shared/test-helpers.ts +++ b/packages/loot-core/src/shared/test-helpers.ts @@ -1,9 +1,8 @@ -// @ts-strict-ignore -export let tracer = null; +export let tracer: null | ReturnType = null; -function timeout(promise, n) { - let resolve; - const timeoutPromise = new Promise(_ => (resolve = _)); +function timeout>(promise: T, n: number) { + let resolve: (response: string) => void; + const timeoutPromise = new Promise(_ => (resolve = _)); const timer = setTimeout(() => resolve(`timeout(${n})`), n); return Promise.race([ @@ -19,16 +18,20 @@ export function resetTracer() { tracer = execTracer(); } -export function execTracer() { - const queue = []; +export function execTracer() { + const queue: Array<{ name: string; data?: T }> = []; let hasStarted = false; - let waitingFor = null; + let waitingFor: null | { + name: string; + reject: (error: Error) => void; + resolve: (data?: T) => void; + } = null; let ended = false; const log = false; return { - event(name: string, data?: unknown) { + event(name: string, data?: T) { if (!hasStarted) { return; } else if (log) { @@ -57,7 +60,7 @@ export function execTracer() { } }, - wait(name) { + wait(name: string) { if (waitingFor) { throw new Error( `Already waiting for ${waitingFor.name}, cannot wait for multiple events`, @@ -69,18 +72,22 @@ export function execTracer() { }); }, - expectWait(name, data) { + expectWait(name: string, data?: T) { if (!hasStarted) { throw new Error(`Expected “${name}” but tracer hasn’t started yet`); } else if (log) { console.log(`--- expectWait(${name}) ---`); } - let promise = this.wait(name); + const promise = this.wait(name); if (data === undefined) { // We want to ignore the result - promise = promise.then(() => true); - data = true; + return expect( + timeout( + promise.then(() => true), + 1000, + ), + ).resolves.toEqual(true); } if (typeof data === 'function') { @@ -97,20 +104,20 @@ export function execTracer() { } }, - expectNow(name, data) { + expectNow(name: string, data?: T) { if (!hasStarted) { throw new Error(`Expected “${name}” but tracer hasn’t started yet`); } else if (log) { console.log(`--- expectNow(${name}) ---`); } - if (queue.length === 0) { + const entry = queue.shift(); + + if (!entry) { throw new Error( `Expected event “${name}” but none found - has it happened yet?`, ); - } else if (queue[0].name === name) { - const entry = queue.shift(); - + } else if (entry.name === name) { if (typeof data === 'function') { data(entry.data); } else { @@ -123,7 +130,7 @@ export function execTracer() { } }, - expect(name: string, data?: unknown) { + expect(name: string, data?: T) { if (queue.length === 0) { return this.expectWait(name, data); } diff --git a/upcoming-release-notes/2192.md b/upcoming-release-notes/2192.md new file mode 100644 index 00000000000..ff341b26d62 --- /dev/null +++ b/upcoming-release-notes/2192.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [carkom] +--- + +Fix table graph rendering issue for custom reports. diff --git a/upcoming-release-notes/2228.md b/upcoming-release-notes/2228.md new file mode 100644 index 00000000000..e12eb6fb1f7 --- /dev/null +++ b/upcoming-release-notes/2228.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [MatissJanis] +--- + +TypeScript: fix some `strictNullChecks: true` issues (pt.2) diff --git a/upcoming-release-notes/2238.md b/upcoming-release-notes/2238.md new file mode 100644 index 00000000000..a38296a0bc2 --- /dev/null +++ b/upcoming-release-notes/2238.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [MatissJanis] +--- + +GoCardless: fix sync not working if `additionalInformation` fallback field is null