From c66d6e00f5c91d4a7dc03fd69a6299bec11463d8 Mon Sep 17 00:00:00 2001 From: Neil <55785687+carkom@users.noreply.github.com> Date: Sat, 20 Jan 2024 10:22:28 +0000 Subject: [PATCH 1/4] Custom Reports - add schema (#2246) * Add schema work * notes * merge fixes * add to handlers * notes update * Update packages/loot-core/src/server/reports/app.ts Co-authored-by: Matiss Janis Aboltins * review changes * type updates --------- Co-authored-by: Matiss Janis Aboltins --- .../src/components/common/AnchorLink.tsx | 5 + .../src/components/reports/Overview.jsx | 5 +- .../src/components/reports/ReportCard.tsx | 26 ++++- .../src/components/reports/ReportOptions.ts | 31 +++++- .../reports/reports/CustomReport.jsx | 37 ++++--- .../reports/reports/CustomReportCard.jsx | 4 +- .../src/client/data-hooks/reports.ts | 48 +++++++++ .../loot-core/src/server/aql/schema/index.ts | 20 ++++ packages/loot-core/src/server/main.ts | 11 +- packages/loot-core/src/server/reports/app.ts | 100 ++++++++++++++++++ .../src/server/reports/types/handlers.ts | 9 ++ packages/loot-core/src/types/handlers.d.ts | 2 + .../loot-core/src/types/models/reports.d.ts | 11 +- upcoming-release-notes/2246.md | 6 ++ 14 files changed, 284 insertions(+), 31 deletions(-) create mode 100644 packages/loot-core/src/client/data-hooks/reports.ts create mode 100644 packages/loot-core/src/server/reports/app.ts create mode 100644 packages/loot-core/src/server/reports/types/handlers.ts create mode 100644 upcoming-release-notes/2246.md diff --git a/packages/desktop-client/src/components/common/AnchorLink.tsx b/packages/desktop-client/src/components/common/AnchorLink.tsx index 5defecba686..ed596b00d28 100644 --- a/packages/desktop-client/src/components/common/AnchorLink.tsx +++ b/packages/desktop-client/src/components/common/AnchorLink.tsx @@ -3,6 +3,8 @@ import { NavLink, useMatch } from 'react-router-dom'; import { css } from 'glamor'; +import { type CustomReportEntity } from 'loot-core/src/types/models'; + import { type CSSProperties, styles } from '../../style'; type AnchorLinkProps = { @@ -10,6 +12,7 @@ type AnchorLinkProps = { style?: CSSProperties; activeStyle?: CSSProperties; children?: ReactNode; + report?: CustomReportEntity; }; export function AnchorLink({ @@ -17,12 +20,14 @@ export function AnchorLink({ style, activeStyle, children, + report, }: AnchorLinkProps) { const match = useMatch({ path: to }); return ( } {sankeyFeatureFlag && } {customReportsFeatureFlag ? ( - + ) : (
)} diff --git a/packages/desktop-client/src/components/reports/ReportCard.tsx b/packages/desktop-client/src/components/reports/ReportCard.tsx index db8b6b2429e..ad9fe83d189 100644 --- a/packages/desktop-client/src/components/reports/ReportCard.tsx +++ b/packages/desktop-client/src/components/reports/ReportCard.tsx @@ -1,11 +1,26 @@ -// @ts-strict-ignore -import React from 'react'; +import React, { type ReactNode } from 'react'; -import { theme } from '../../style'; +import { type CustomReportEntity } from 'loot-core/src/types/models'; + +import { type CSSProperties, theme } from '../../style'; import { AnchorLink } from '../common/AnchorLink'; import { View } from '../common/View'; -export function ReportCard({ flex, to, style, children }) { +type ReportCardProps = { + to: string; + report: CustomReportEntity; + children: ReactNode; + flex?: string; + style?: CSSProperties; +}; + +export function ReportCard({ + to, + report, + children, + flex, + style, +}: ReportCardProps) { const containerProps = { flex, margin: 15 }; const content = ( @@ -34,7 +49,8 @@ export function ReportCard({ flex, to, style, children }) { return ( {content} diff --git a/packages/desktop-client/src/components/reports/ReportOptions.ts b/packages/desktop-client/src/components/reports/ReportOptions.ts index f32dbe5e1fe..7cc13b86e27 100644 --- a/packages/desktop-client/src/components/reports/ReportOptions.ts +++ b/packages/desktop-client/src/components/reports/ReportOptions.ts @@ -1,11 +1,32 @@ -// @ts-strict-ignore +import * as monthUtils from 'loot-core/src/shared/months'; import { + type CustomReportEntity, type AccountEntity, type CategoryEntity, type CategoryGroupEntity, type PayeeEntity, } from 'loot-core/src/types/models'; +const startDate = monthUtils.subMonths(monthUtils.currentMonth(), 5); +const endDate = monthUtils.currentMonth(); + +export const defaultState: CustomReportEntity = { + id: undefined, + mode: 'total', + groupBy: 'Category', + balanceType: 'Payment', + showEmpty: false, + showOffBudgetHidden: false, + showUncategorized: false, + graphType: 'BarGraph', + startDate, + endDate, + selectedCategories: null, + isDateStatic: false, + conditionsOp: 'and', + name: 'Default', +}; + const balanceTypeOptions = [ { description: 'Payment', format: 'totalDebts' as const }, { description: 'Deposit', format: 'totalAssets' as const }, @@ -83,7 +104,7 @@ export type UncategorizedEntity = Pick< const uncategorizedCategory: UncategorizedEntity = { name: 'Uncategorized', - id: null, + id: undefined, uncategorized_id: '1', hidden: false, is_off_budget: false, @@ -92,7 +113,7 @@ const uncategorizedCategory: UncategorizedEntity = { }; const transferCategory: UncategorizedEntity = { name: 'Transfers', - id: null, + id: undefined, uncategorized_id: '2', hidden: false, is_off_budget: false, @@ -101,7 +122,7 @@ const transferCategory: UncategorizedEntity = { }; const offBudgetCategory: UncategorizedEntity = { name: 'Off Budget', - id: null, + id: undefined, uncategorized_id: '3', hidden: false, is_off_budget: true, @@ -118,7 +139,7 @@ type UncategorizedGroupEntity = Pick< const uncategorizedGroup: UncategorizedGroupEntity = { name: 'Uncategorized & Off Budget', - id: null, + id: undefined, hidden: false, categories: [uncategorizedCategory, transferCategory, offBudgetCategory], }; diff --git a/packages/desktop-client/src/components/reports/reports/CustomReport.jsx b/packages/desktop-client/src/components/reports/reports/CustomReport.jsx index 476057dcade..009a66e8826 100644 --- a/packages/desktop-client/src/components/reports/reports/CustomReport.jsx +++ b/packages/desktop-client/src/components/reports/reports/CustomReport.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect, useMemo } from 'react'; import { useSelector } from 'react-redux'; +import { useLocation } from 'react-router-dom'; import * as d from 'date-fns'; @@ -23,7 +24,7 @@ import { ChooseGraph } from '../ChooseGraph'; import { Header } from '../Header'; import { LoadingIndicator } from '../LoadingIndicator'; import { ReportLegend } from '../ReportLegend'; -import { ReportOptions } from '../ReportOptions'; +import { ReportOptions, defaultState } from '../ReportOptions'; import { ReportSidebar } from '../ReportSidebar'; import { ReportSummary } from '../ReportSummary'; import { ReportTopbar } from '../ReportTopbar'; @@ -52,26 +53,34 @@ export function CustomReport() { onCondOpChange, } = useFilters(); - const [selectedCategories, setSelectedCategories] = useState(null); + const location = useLocation(); + const loadReport = location.state.report ?? defaultState; + const [allMonths, setAllMonths] = useState(null); const [typeDisabled, setTypeDisabled] = useState(['Net']); - const [startDate, setStartDate] = useState( - monthUtils.subMonths(monthUtils.currentMonth(), 5), + + const [selectedCategories, setSelectedCategories] = useState( + loadReport.selectedCategories, + ); + const [startDate, setStartDate] = useState(loadReport.startDate); + const [endDate, setEndDate] = useState(loadReport.endDate); + const [mode, setMode] = useState(loadReport.mode); + const [isDateStatic, setIsDateStatic] = useState(loadReport.isDateStatic); + const [groupBy, setGroupBy] = useState(loadReport.groupBy); + const [balanceType, setBalanceType] = useState(loadReport.balanceType); + const [showEmpty, setShowEmpty] = useState(loadReport.showEmpty); + const [showOffBudgetHidden, setShowOffBudgetHidden] = useState( + loadReport.showOffBudgetHidden, ); - const [endDate, setEndDate] = useState(monthUtils.currentMonth()); + const [showUncategorized, setShowUncategorized] = useState( + loadReport.showUncategorized, + ); + const [graphType, setGraphType] = useState(loadReport.graphType); - const [mode, setMode] = useState('total'); - const [isDateStatic, setIsDateStatic] = useState(false); - const [groupBy, setGroupBy] = useState('Category'); - const [balanceType, setBalanceType] = useState('Payment'); - const [showEmpty, setShowEmpty] = useState(false); - const [showOffBudgetHidden, setShowOffBudgetHidden] = useState(false); - const [showUncategorized, setShowUncategorized] = useState(false); const [dateRange, setDateRange] = useState('Last 6 months'); const [dataCheck, setDataCheck] = useState(false); - - const [graphType, setGraphType] = useState('BarGraph'); const dateRangeLine = ReportOptions.dateRange.length - 3; + const months = monthUtils.rangeInclusive(startDate, endDate); useEffect(() => { diff --git a/packages/desktop-client/src/components/reports/reports/CustomReportCard.jsx b/packages/desktop-client/src/components/reports/reports/CustomReportCard.jsx index 7269529b4e2..6a1e6457262 100644 --- a/packages/desktop-client/src/components/reports/reports/CustomReportCard.jsx +++ b/packages/desktop-client/src/components/reports/reports/CustomReportCard.jsx @@ -13,7 +13,7 @@ import { ReportCard } from '../ReportCard'; import { createCustomSpreadsheet } from '../spreadsheets/custom-spreadsheet'; import { useReport } from '../useReport'; -export function CustomReportCard() { +export function CustomReportCard(reports) { const categories = useCategories(); const endDate = monthUtils.currentMonth(); @@ -32,7 +32,7 @@ export function CustomReportCard() { const data = useReport('default', getGraphData); return ( - + diff --git a/packages/loot-core/src/client/data-hooks/reports.ts b/packages/loot-core/src/client/data-hooks/reports.ts new file mode 100644 index 00000000000..1810a91f73a --- /dev/null +++ b/packages/loot-core/src/client/data-hooks/reports.ts @@ -0,0 +1,48 @@ +import { useMemo } from 'react'; + +import { q } from '../../shared/query'; +import { + type CustomReportData, + type CustomReportEntity, +} from '../../types/models'; +import { useLiveQuery } from '../query-hooks'; + +function toJS(rows: CustomReportData[]) { + const reports: CustomReportEntity[] = rows.map(row => { + const test: CustomReportEntity = { + ...row, + conditionsOp: row.conditions_op ?? 'and', + filters: row.conditions, + }; + return test; + }); + return reports; +} + +/* +leaving as a placeholder for saved reports implementation return an +empty array because "reports" db table doesn't exist yet +*/ +export function useReports(): CustomReportEntity[] { + const reports: CustomReportEntity[] = toJS( + //useLiveQuery(() => q('reports').select('*'), []) || [], + useLiveQuery(() => q('transaction_filters').select('*'), []) || [], + ); + + /** Sort reports by alphabetical order */ + function sort(reports: CustomReportEntity[]) { + return reports.sort((a, b) => + a.name + .trim() + .localeCompare(b.name.trim(), undefined, { ignorePunctuation: true }), + ); + } + + //return useMemo(() => sort(reports), [reports]); + + //everything below this line will be removed once db table is created + const order: CustomReportEntity[] = useMemo(() => sort(reports), [reports]); + const flag = true; + const emptyReports: CustomReportEntity[] = flag ? [] : order; + return emptyReports; +} diff --git a/packages/loot-core/src/server/aql/schema/index.ts b/packages/loot-core/src/server/aql/schema/index.ts index 005eada6860..9d893e6b3da 100644 --- a/packages/loot-core/src/server/aql/schema/index.ts +++ b/packages/loot-core/src/server/aql/schema/index.ts @@ -125,6 +125,26 @@ export const schema = { conditions: f('json'), tombstone: f('boolean'), }, + custom_reports: { + id: f('id'), + name: f('string'), + start_date: f('string', { default: '2023-06' }), + end_date: f('string', { default: '2023-09' }), + mode: f('string', { default: 'total' }), + group_by: f('string', { default: 'Category' }), + balance_type: f('string', { default: 'Expense' }), + interval: f('string', { default: 'Monthly' }), + show_empty: f('integer', { default: 0 }), + show_offbudgethidden: f('integer', { default: 0 }), + show_uncategorized: f('integer', { default: 0 }), + selected_categories: f('json'), + graph_type: f('string', { default: 'BarGraph' }), + conditions: f('json'), + conditions_op: f('string'), + metadata: f('json'), + color_scheme: f('json'), + tombstone: f('boolean'), + }, reflect_budgets: { id: f('id'), month: f('integer'), diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts index 03d661f06df..89bf0896895 100644 --- a/packages/loot-core/src/server/main.ts +++ b/packages/loot-core/src/server/main.ts @@ -48,6 +48,7 @@ import { app as notesApp } from './notes/app'; import * as Platform from './platform'; import { get, post } from './post'; import * as prefs from './prefs'; +import { app as reportsApp } from './reports/app'; import { app as rulesApp } from './rules/app'; import { app as schedulesApp } from './schedules/app'; import { getServer, setServer } from './server-config'; @@ -2147,7 +2148,15 @@ injectAPI.override((name, args) => runHandler(app.handlers[name], args)); // A hack for now until we clean up everything app.handlers = handlers; -app.combine(schedulesApp, budgetApp, notesApp, toolsApp, filtersApp, rulesApp); +app.combine( + schedulesApp, + budgetApp, + notesApp, + toolsApp, + filtersApp, + reportsApp, + rulesApp, +); function getDefaultDocumentDir() { if (Platform.isMobile) { diff --git a/packages/loot-core/src/server/reports/app.ts b/packages/loot-core/src/server/reports/app.ts new file mode 100644 index 00000000000..612bc23ba73 --- /dev/null +++ b/packages/loot-core/src/server/reports/app.ts @@ -0,0 +1,100 @@ +import { v4 as uuidv4 } from 'uuid'; + +import { + type CustomReportData, + type CustomReportEntity, +} from '../../types/models'; +import { parseConditionsOrActions } from '../accounts/transaction-rules'; +import { createApp } from '../app'; +import * as db from '../db'; +import { requiredFields } from '../models'; +import { mutator } from '../mutators'; +import { undoable } from '../undo'; + +import { ReportsHandlers } from './types/handlers'; + +const reportModel = { + validate(report: CustomReportEntity, { update }: { update?: boolean } = {}) { + requiredFields('reports', report, ['conditions'], update); + + if (!update || 'conditionsOp' in report) { + if (!['and', 'or'].includes(report.conditionsOp)) { + throw new Error('Invalid filter conditionsOp: ' + report.conditionsOp); + } + } + + return report; + }, + + toJS(row: CustomReportData) { + return { + ...row, + conditionsOp: row.conditions_op, + filters: parseConditionsOrActions(row.conditions), + }; + }, + + fromJS(report: CustomReportEntity) { + const { filters, conditionsOp, ...row }: CustomReportData = report; + if (conditionsOp) { + row.conditions_op = conditionsOp; + row.conditions = filters; + } + return row; + }, +}; + +async function reportNameExists( + name: string, + reportId: string | undefined, + newItem: boolean, +) { + if (!name) { + throw new Error('Report name is required'); + } + + if (!reportId) { + throw new Error('Report recall error'); + } + + const idForName: { id: string } = await db.first( + 'SELECT id from reports WHERE tombstone = 0 AND name = ?', + [name], + ); + + if (!newItem && idForName.id !== reportId) { + throw new Error('There is already a report named ' + name); + } +} + +async function createReport(report: CustomReportEntity) { + const reportId = uuidv4(); + const item: CustomReportData = { + ...report, + id: reportId, + }; + + reportNameExists(item.name, item.id, true); + + // Create the report here based on the info + await db.insertWithSchema('reports', reportModel.fromJS(item)); + + return reportId; +} + +async function updateReport(item: CustomReportEntity) { + reportNameExists(item.name, item.id, false); + + await db.insertWithSchema('reports', reportModel.fromJS(item)); +} + +async function deleteReport(id: string) { + await db.delete_('reports', id); +} + +// Expose functions to the client +export const app = createApp(); + +app.method('report/create', mutator(undoable(createReport))); +app.method('report/update', mutator(undoable(updateReport))); +app.method('report/delete', mutator(undoable(deleteReport))); diff --git a/packages/loot-core/src/server/reports/types/handlers.ts b/packages/loot-core/src/server/reports/types/handlers.ts new file mode 100644 index 00000000000..4c8d3fbc6a0 --- /dev/null +++ b/packages/loot-core/src/server/reports/types/handlers.ts @@ -0,0 +1,9 @@ +import { type CustomReportEntity } from '../../../types/models'; + +export interface ReportsHandlers { + 'report/create': (report: CustomReportEntity) => Promise; + + 'report/update': (report: CustomReportEntity) => Promise; + + 'report/delete': (id: string) => Promise; +} diff --git a/packages/loot-core/src/types/handlers.d.ts b/packages/loot-core/src/types/handlers.d.ts index 7d131d44554..5eb8f281af6 100644 --- a/packages/loot-core/src/types/handlers.d.ts +++ b/packages/loot-core/src/types/handlers.d.ts @@ -1,6 +1,7 @@ import type { BudgetHandlers } from '../server/budget/types/handlers'; import type { FiltersHandlers } from '../server/filters/types/handlers'; import type { NotesHandlers } from '../server/notes/types/handlers'; +import type { ReportsHandlers } from '../server/reports/types/handlers'; import type { RulesHandlers } from '../server/rules/types/handlers'; import type { SchedulesHandlers } from '../server/schedules/types/handlers'; import type { ToolsHandlers } from '../server/tools/types/handlers'; @@ -14,6 +15,7 @@ export interface Handlers BudgetHandlers, FiltersHandlers, NotesHandlers, + ReportsHandlers, RulesHandlers, SchedulesHandlers, ToolsHandlers {} diff --git a/packages/loot-core/src/types/models/reports.d.ts b/packages/loot-core/src/types/models/reports.d.ts index 7d15406b0d5..588b475a35d 100644 --- a/packages/loot-core/src/types/models/reports.d.ts +++ b/packages/loot-core/src/types/models/reports.d.ts @@ -1,7 +1,7 @@ import { type RuleConditionEntity } from './rule'; export interface CustomReportEntity { - reportId?: string; + id: string | undefined; mode: string; groupBy: string; balanceType: string; @@ -10,13 +10,13 @@ export interface CustomReportEntity { showUncategorized: boolean; graphType: string; selectedCategories; - filters: RuleConditionEntity; + filters?: RuleConditionEntity[]; conditionsOp: string; name: string; startDate: string; endDate: string; isDateStatic: boolean; - data: GroupedEntity; + data?: GroupedEntity; tombstone?: boolean; } @@ -67,3 +67,8 @@ export interface DataEntity { export type Month = { month: string; }; + +export interface CustomReportData extends CustomReportEntity { + conditions_op?: string; + conditions?: RuleConditionEntity[]; +} diff --git a/upcoming-release-notes/2246.md b/upcoming-release-notes/2246.md new file mode 100644 index 00000000000..e3bf2978ef6 --- /dev/null +++ b/upcoming-release-notes/2246.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [carkom] +--- + +Add schema and backend functionality for custom reports. This is to enable saving reports in a future PR. From dd254c6c235dea7c5fdbff977119d9b9aa90fe08 Mon Sep 17 00:00:00 2001 From: Matiss Janis Aboltins Date: Sat, 20 Jan 2024 10:40:52 +0000 Subject: [PATCH 2/4] :wrench: add link to community discord for support (#2250) --- .github/ISSUE_TEMPLATE/bug-report.yml | 12 ++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 4 ++++ upcoming-release-notes/2250.md | 6 ++++++ 3 files changed, 22 insertions(+) create mode 100644 upcoming-release-notes/2250.md diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 254c18462bc..85235450ebf 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -18,6 +18,18 @@ body: required: true validations: required: true + - type: checkboxes + id: bank-sync-issue + attributes: + label: 'Is this related to GoCardless, Simplefin or another bank-sync provider?' + description: 'Most issues with bank-sync providers are due to a lack of a custom bank-mapper (i.e. payee or other fields not coming through). In such cases you can create a custom bank mapper in [actual-server](https://github.com/actualbudget/actual-server/blob/master/src/app-gocardless/README.md) repository. Other likely issue is misconfigured server - in which case please reach out via the [community Discord](https://discord.gg/pRYNYr4W5A) to get support.' + options: + - label: 'I have checked my server logs and could not see any errors there' + - label: 'I will be attaching my server logs to this issue' + - label: 'I will be attaching my client-side (browser) logs to this issue' + - label: 'I understand that this issue will be automatically closed if insufficient information is provided' + validations: + required: false - type: textarea id: what-happened attributes: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 3ba13e0cec6..150ae8082b4 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1 +1,5 @@ blank_issues_enabled: false +contact_links: + - name: Support + url: https://discord.gg/pRYNYr4W5A + about: Need help with something? Perhaps having issues setting up bank-sync with GoCardless or SimpleFin? Reach out to the community on Discord. diff --git a/upcoming-release-notes/2250.md b/upcoming-release-notes/2250.md new file mode 100644 index 00000000000..fd7227e5c07 --- /dev/null +++ b/upcoming-release-notes/2250.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [MatissJanis] +--- + +Updated Github issues template to direct bug reports to the support channel (Discord) From 0045d9212e083d3393e9d53a1ecb271cafc83675 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Sat, 20 Jan 2024 10:30:23 -0800 Subject: [PATCH 3/4] Bundle loot-core types into the API (#2053) * Bundle loot-core types into the API So we can have loot-core be the source of truth for some types that get passed through - Improves downstream development with API by including types - Use path aliases for dist vs dev tsconfigs - Convert api index to typescript as example - Permit ts-ignore for issues with our version of typescript --------- Co-authored-by: Matiss Janis Aboltins --- .eslintignore | 1 + .eslintrc.js | 5 ++ .gitignore | 1 + packages/api/index.js | 38 ------------- packages/api/index.ts | 53 +++++++++++++++++++ packages/api/{methods.js => methods.ts} | 18 ++++--- packages/api/package.json | 10 ++-- packages/api/tsconfig.dist.json | 8 ++- packages/loot-core/bin/build-api | 21 ++++++++ packages/loot-core/package.json | 2 +- .../loot-core/src/server/importers/ynab5.ts | 4 +- packages/loot-core/src/server/main.ts | 8 ++- packages/loot-core/src/server/mutators.ts | 6 ++- packages/loot-core/src/shared/schedules.ts | 2 + .../loot-core/src/types/api-handlers.d.ts | 9 ++-- packages/loot-core/tsconfig.api.json | 12 +++++ tsconfig.json | 4 ++ upcoming-release-notes/2053.md | 6 +++ yarn.lock | 44 ++++++++++++++- 19 files changed, 190 insertions(+), 62 deletions(-) delete mode 100644 packages/api/index.js create mode 100644 packages/api/index.ts rename packages/api/{methods.js => methods.ts} (86%) create mode 100755 packages/loot-core/bin/build-api create mode 100644 packages/loot-core/tsconfig.api.json create mode 100644 upcoming-release-notes/2053.md diff --git a/.eslintignore b/.eslintignore index f5f8a9e7ccd..2487b54410d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,6 @@ packages/api/app/bundle.api.js packages/api/dist +packages/api/@types packages/api/migrations packages/crdt/dist diff --git a/.eslintrc.js b/.eslintrc.js index d757f5187a8..bf7bb15cb14 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -165,6 +165,11 @@ module.exports = { { patterns: [...restrictedImportPatterns, ...restrictedImportColors] }, ], + '@typescript-eslint/ban-ts-comment': [ + 'error', + { 'ts-ignore': 'allow-with-description' }, + ], + // Rules disable during TS migration '@typescript-eslint/no-var-requires': 'off', 'prefer-const': 'warn', diff --git a/.gitignore b/.gitignore index 691472708e3..9777ed2d001 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ !data/.gitkeep /data2 packages/api/dist +packages/api/@types packages/crdt/dist packages/desktop-electron/client-build packages/desktop-electron/.electron-symbols diff --git a/packages/api/index.js b/packages/api/index.js deleted file mode 100644 index 4473e540c86..00000000000 --- a/packages/api/index.js +++ /dev/null @@ -1,38 +0,0 @@ -// eslint-disable-next-line import/extensions -import * as bundle from './app/bundle.api.js'; -import * as injected from './injected'; -import { validateNodeVersion } from './validateNodeVersion'; - -let actualApp; -export const internal = bundle.lib; - -// DEPRECATED: remove the next line in @actual-app/api v7 -export * as methods from './methods'; - -export * from './methods'; -export * as utils from './utils'; - -export async function init(config = {}) { - if (actualApp) { - return; - } - - validateNodeVersion(); - - global.fetch = (...args) => - import('node-fetch').then(({ default: fetch }) => fetch(...args)); - - await bundle.init(config); - actualApp = bundle.lib; - - injected.override(bundle.lib.send); - return bundle.lib; -} - -export async function shutdown() { - if (actualApp) { - await actualApp.send('sync'); - await actualApp.send('close-budget'); - actualApp = null; - } -} diff --git a/packages/api/index.ts b/packages/api/index.ts new file mode 100644 index 00000000000..be3f92d8bff --- /dev/null +++ b/packages/api/index.ts @@ -0,0 +1,53 @@ +import type { + RequestInfo as FetchInfo, + RequestInit as FetchInit, + // @ts-ignore: false-positive commonjs module error on build until typescript 5.3 +} from 'node-fetch'; // with { 'resolution-mode': 'import' }; + +// loot-core types +import type { InitConfig } from 'loot-core/server/main'; + +// @ts-ignore: bundle not available until we build it +// eslint-disable-next-line import/extensions +import * as bundle from './app/bundle.api.js'; +import * as injected from './injected'; +import { validateNodeVersion } from './validateNodeVersion'; + +let actualApp: null | typeof bundle.lib; +export const internal = bundle.lib; + +// DEPRECATED: remove the next line in @actual-app/api v7 +export * as methods from './methods'; + +export * from './methods'; +export * as utils from './utils'; + +export async function init(config: InitConfig = {}) { + if (actualApp) { + return; + } + + validateNodeVersion(); + + if (!globalThis.fetch) { + globalThis.fetch = (url: URL | RequestInfo, init?: RequestInit) => { + return import('node-fetch').then(({ default: fetch }) => + fetch(url as unknown as FetchInfo, init as unknown as FetchInit), + ) as unknown as Promise; + }; + } + + await bundle.init(config); + actualApp = bundle.lib; + + injected.override(bundle.lib.send); + return bundle.lib; +} + +export async function shutdown() { + if (actualApp) { + await actualApp.send('sync'); + await actualApp.send('close-budget'); + actualApp = null; + } +} diff --git a/packages/api/methods.js b/packages/api/methods.ts similarity index 86% rename from packages/api/methods.js rename to packages/api/methods.ts index 5ca8226c47f..03acaf0f4f6 100644 --- a/packages/api/methods.js +++ b/packages/api/methods.ts @@ -1,8 +1,14 @@ +// @ts-strict-ignore +import type { Handlers } from 'loot-core/src/types/handlers'; + import * as injected from './injected'; export { q } from './app/query'; -function send(name, args) { +function send( + name: K, + args?: Parameters[0], +): Promise>> { return injected.send(name, args); } @@ -21,7 +27,7 @@ export async function loadBudget(budgetId) { return send('api/load-budget', { id: budgetId }); } -export async function downloadBudget(syncId, { password } = {}) { +export async function downloadBudget(syncId, { password }: { password? } = {}) { return send('api/download-budget', { syncId, password }); } @@ -91,7 +97,7 @@ export function getAccounts() { return send('api/accounts-get'); } -export function createAccount(account, initialBalance) { +export function createAccount(account, initialBalance?) { return send('api/account-create', { account, initialBalance }); } @@ -99,7 +105,7 @@ export function updateAccount(id, fields) { return send('api/account-update', { id, fields }); } -export function closeAccount(id, transferAccountId, transferCategoryId) { +export function closeAccount(id, transferAccountId?, transferCategoryId?) { return send('api/account-close', { id, transferAccountId, @@ -123,7 +129,7 @@ export function updateCategoryGroup(id, fields) { return send('api/category-group-update', { id, fields }); } -export function deleteCategoryGroup(id, transferCategoryId) { +export function deleteCategoryGroup(id, transferCategoryId?) { return send('api/category-group-delete', { id, transferCategoryId }); } @@ -139,7 +145,7 @@ export function updateCategory(id, fields) { return send('api/category-update', { id, fields }); } -export function deleteCategory(id, transferCategoryId) { +export function deleteCategory(id, transferCategoryId?) { return send('api/category-delete', { id, transferCategoryId }); } diff --git a/packages/api/package.json b/packages/api/package.json index 7c2d9a01e5b..b0433a638e8 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -7,17 +7,18 @@ "node": ">=18.12.0" }, "main": "dist/index.js", - "types": "dist/index.d.ts", + "types": "@types/index.d.ts", "files": [ "dist" ], "scripts": { "build:app": "yarn workspace loot-core build:api", - "build:node": "tsc --p tsconfig.dist.json", + "build:node": "tsc --p tsconfig.dist.json && tsc-alias -p tsconfig.dist.json", "build:migrations": "cp migrations/*.sql dist/migrations", "build:default-db": "cp default-db.sqlite dist/", - "build": "rm -rf dist && yarn run build:app && yarn run build:node && yarn run build:migrations && yarn run build:default-db", - "test": "yarn run build:app && jest -c jest.config.js" + "build": "yarn run clean && yarn run build:app && yarn run build:node && yarn run build:migrations && yarn run build:default-db", + "test": "yarn run build:app && jest -c jest.config.js", + "clean": "rm -rf dist @types" }, "dependencies": { "better-sqlite3": "^9.2.2", @@ -31,6 +32,7 @@ "@types/jest": "^27.5.0", "@types/uuid": "^9.0.2", "jest": "^27.0.0", + "tsc-alias": "^1.8.8", "typescript": "^5.0.2" } } diff --git a/packages/api/tsconfig.dist.json b/packages/api/tsconfig.dist.json index 6704cb1fb7c..12caac8f770 100644 --- a/packages/api/tsconfig.dist.json +++ b/packages/api/tsconfig.dist.json @@ -8,8 +8,12 @@ "moduleResolution": "Node16", "noEmit": false, "declaration": true, - "outDir": "dist" + "outDir": "dist", + "declarationDir": "@types", + "paths": { + "loot-core/*": ["./@types/loot-core/*"], + } }, "include": ["."], - "exclude": ["dist"] + "exclude": ["**/node_modules/*", "dist", "@types"] } diff --git a/packages/loot-core/bin/build-api b/packages/loot-core/bin/build-api new file mode 100755 index 00000000000..577e6342332 --- /dev/null +++ b/packages/loot-core/bin/build-api @@ -0,0 +1,21 @@ +#!/bin/bash + +set -euo pipefail + +cd "$(dirname "$0")/.." || exit 1 +ROOT="$(pwd -P)" + +yarn tsc -p tsconfig.api.json --outDir ../api/@types/loot-core/ +# Copy existing handwritten .d.ts files, as tsc doesn't move them for us +dest="../../api/@types/loot-core" +cd src +find . -type f -name "*.d.ts" | while read -r f +do + d=$(dirname "${f}") + d="${dest}/${d}" + mkdir -p "${d}" + cp "${f}" "${d}" +done +cd "$ROOT" +yarn webpack --config ./webpack/webpack.api.config.js; +./bin/copy-migrations ../api \ No newline at end of file diff --git a/packages/loot-core/package.json b/packages/loot-core/package.json index 48253fea564..aceb8e1f0a1 100644 --- a/packages/loot-core/package.json +++ b/packages/loot-core/package.json @@ -6,7 +6,7 @@ "scripts": { "build:node": "cross-env NODE_ENV=production webpack --config ./webpack/webpack.desktop.config.js", "watch:node": "cross-env NODE_ENV=development webpack --config ./webpack/webpack.desktop.config.js --watch", - "build:api": "cross-env NODE_ENV=development webpack --config ./webpack/webpack.api.config.js; ./bin/copy-migrations ../api", + "build:api": "cross-env NODE_ENV=development ./bin/build-api", "build:browser": "cross-env NODE_ENV=production ./bin/build-browser", "watch:browser": "cross-env NODE_ENV=development ./bin/build-browser", "test": "npm-run-all -cp 'test:*'", diff --git a/packages/loot-core/src/server/importers/ynab5.ts b/packages/loot-core/src/server/importers/ynab5.ts index f9fb1337e0e..a9e32ebfe35 100644 --- a/packages/loot-core/src/server/importers/ynab5.ts +++ b/packages/loot-core/src/server/importers/ynab5.ts @@ -146,8 +146,8 @@ async function importTransactions( ); const payeesByTransferAcct = payees - .filter((payee: YNAB5.Payee) => payee?.transfer_acct) - .map((payee: YNAB5.Payee) => [payee.transfer_acct, payee]); + .filter(payee => payee?.transfer_acct) + .map(payee => [payee.transfer_acct, payee] as [string, YNAB5.Payee]); const payeeTransferAcctHashMap = new Map( payeesByTransferAcct, ); diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts index 89bf0896895..03540834395 100644 --- a/packages/loot-core/src/server/main.ts +++ b/packages/loot-core/src/server/main.ts @@ -2260,8 +2260,14 @@ export async function initApp(isDev, socketName) { } } +export type InitConfig = { + dataDir?: string; + serverURL?: string; + password?: string; +}; + // eslint-disable-next-line import/no-unused-modules -export async function init(config) { +export async function init(config: InitConfig) { // Get from build let dataDir, serverURL; diff --git a/packages/loot-core/src/server/mutators.ts b/packages/loot-core/src/server/mutators.ts index abd44cf829e..f5eac7b1d80 100644 --- a/packages/loot-core/src/server/mutators.ts +++ b/packages/loot-core/src/server/mutators.ts @@ -51,7 +51,9 @@ export async function runHandler( } if (mutatingMethods.has(handler)) { - return runMutator(() => handler(args), { undoTag }); + return runMutator(() => handler(args), { undoTag }) as Promise< + ReturnType + >; } // When closing a file, it clears out all global state for the file. That @@ -67,7 +69,7 @@ export async function runHandler( promise.then(() => { runningMethods.delete(promise); }); - return promise; + return promise as Promise>; } // These are useful for tests. Only use them in tests. diff --git a/packages/loot-core/src/shared/schedules.ts b/packages/loot-core/src/shared/schedules.ts index fa711d5c34e..d8628ddfa0d 100644 --- a/packages/loot-core/src/shared/schedules.ts +++ b/packages/loot-core/src/shared/schedules.ts @@ -189,11 +189,13 @@ export function getRecurringDescription(config, dateFormat) { export function recurConfigToRSchedule(config) { const base: IRuleOptions = { start: monthUtils.parseDate(config.start), + // @ts-ignore: issues with https://gitlab.com/john.carroll.p/rschedule/-/issues/86 frequency: config.frequency.toUpperCase(), byHourOfDay: [12], }; if (config.interval) { + // @ts-ignore: issues with https://gitlab.com/john.carroll.p/rschedule/-/issues/86 base.interval = config.interval; } diff --git a/packages/loot-core/src/types/api-handlers.d.ts b/packages/loot-core/src/types/api-handlers.d.ts index 1a667eba315..30ecac4cf36 100644 --- a/packages/loot-core/src/types/api-handlers.d.ts +++ b/packages/loot-core/src/types/api-handlers.d.ts @@ -54,10 +54,11 @@ export interface ApiHandlers { payees; }) => Promise; - 'api/transactions-import': (arg: { - accountId; - transactions; - }) => Promise; + 'api/transactions-import': (arg: { accountId; transactions }) => Promise<{ + errors?: { message: string }[]; + added; + updated; + }>; 'api/transactions-add': (arg: { accountId; diff --git a/packages/loot-core/tsconfig.api.json b/packages/loot-core/tsconfig.api.json new file mode 100644 index 00000000000..5ffe224f8c9 --- /dev/null +++ b/packages/loot-core/tsconfig.api.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "declaration": true, + "emitDeclarationOnly": true, + "allowJs": false, + "noEmit": false, + }, + "include": ["./typings", "./src/server/*"], + "exclude": ["**/node_modules/*", "**/build/*", "**/lib-dist/*", "./src/server/bench.ts"], + } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index b7a18248790..a421798af08 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,6 +29,10 @@ "module": "ES2022", // Until/if we build using tsc "noEmit": true, + "paths": { + // until we turn on composite/references + "loot-core/*": ["./packages/loot-core/src/*"], + }, "plugins": [ { "name": "typescript-strict-plugin", diff --git a/upcoming-release-notes/2053.md b/upcoming-release-notes/2053.md new file mode 100644 index 00000000000..1c882269dd3 --- /dev/null +++ b/upcoming-release-notes/2053.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [twk3] +--- + +Bundle loot-core types into the API diff --git a/yarn.lock b/yarn.lock index 63b4df37413..f9272fdf955 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31,6 +31,7 @@ __metadata: compare-versions: "npm:^6.1.0" jest: "npm:^27.0.0" node-fetch: "npm:^3.3.2" + tsc-alias: "npm:^1.8.8" typescript: "npm:^5.0.2" uuid: "npm:^9.0.0" languageName: unknown @@ -6336,7 +6337,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^9.4.1": +"commander@npm:^9.0.0, commander@npm:^9.4.1": version: 9.5.0 resolution: "commander@npm:9.5.0" checksum: 41c49b3d0f94a1fbeb0463c85b13f15aa15a9e0b4d5e10a49c0a1d58d4489b549d62262b052ae0aa6cfda53299bee487bfe337825df15e342114dde543f82906 @@ -9216,7 +9217,7 @@ __metadata: languageName: node linkType: hard -"globby@npm:^11.1.0": +"globby@npm:^11.0.4, globby@npm:^11.1.0": version: 11.1.0 resolution: "globby@npm:11.1.0" dependencies: @@ -12639,6 +12640,13 @@ __metadata: languageName: node linkType: hard +"mylas@npm:^2.1.9": + version: 2.1.13 + resolution: "mylas@npm:2.1.13" + checksum: 37f335424463c422f48d50317aa0a34fe410fabb146cbf27b453a0aa743732b5626f56deaa190bca2ce29836f809d88759007976dc78d5d22b75918a00586577 + languageName: node + linkType: hard + "nanoid@npm:^3.3.7": version: 3.3.7 resolution: "nanoid@npm:3.3.7" @@ -13593,6 +13601,15 @@ __metadata: languageName: node linkType: hard +"plimit-lit@npm:^1.2.6": + version: 1.6.1 + resolution: "plimit-lit@npm:1.6.1" + dependencies: + queue-lit: "npm:^1.5.1" + checksum: e4eaf018dc311fd4d452954c10992cd8a9eb72d168ec2274bb831d86558422703e1405a8978ffdd5c418654e6a25e10a0765a39bf3ce3a84dc799fe6268e0ea4 + languageName: node + linkType: hard + "plist@npm:^3.0.4, plist@npm:^3.0.5": version: 3.1.0 resolution: "plist@npm:3.1.0" @@ -13841,6 +13858,13 @@ __metadata: languageName: node linkType: hard +"queue-lit@npm:^1.5.1": + version: 1.5.2 + resolution: "queue-lit@npm:1.5.2" + checksum: 8dd45c79bd25b33b0c7d587391eb0b4acc4deb797bf92fef62b2d8e7c03b64083f5304f09d52a18267d34d020cc67ccde97a88185b67590eeccb194938ff1f98 + languageName: node + linkType: hard + "queue-microtask@npm:^1.2.2": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" @@ -16108,6 +16132,22 @@ __metadata: languageName: node linkType: hard +"tsc-alias@npm:^1.8.8": + version: 1.8.8 + resolution: "tsc-alias@npm:1.8.8" + dependencies: + chokidar: "npm:^3.5.3" + commander: "npm:^9.0.0" + globby: "npm:^11.0.4" + mylas: "npm:^2.1.9" + normalize-path: "npm:^3.0.0" + plimit-lit: "npm:^1.2.6" + bin: + tsc-alias: dist/bin/index.js + checksum: 145d7bb23a618e1136c8addd4b4ed23a1d503a37d3fc5b3698a993fea9331180a68853b0e78ff50fb3fb7ed95d4996a2d82f77395814bbd1c40adee8a9151d90 + languageName: node + linkType: hard + "tsconfck@npm:^2.1.0": version: 2.1.2 resolution: "tsconfck@npm:2.1.2" From 83f13cbdc85c2c3a7d2404b603398d3a41cbfc28 Mon Sep 17 00:00:00 2001 From: Neil <55785687+carkom@users.noreply.github.com> Date: Sat, 20 Jan 2024 21:18:15 +0000 Subject: [PATCH 4/4] add compact to custom reports (#2258) * add compact * notes --- .../reports/graphs/tableGraph/ReportTable.tsx | 3 ++- .../reports/graphs/tableGraph/ReportTableHeader.tsx | 10 +++++----- .../reports/graphs/tableGraph/ReportTableList.tsx | 6 +++++- .../reports/graphs/tableGraph/ReportTableRow.tsx | 12 +++++++----- .../reports/graphs/tableGraph/ReportTableTotals.tsx | 10 +++++----- .../src/components/reports/reports/CustomReport.jsx | 2 +- upcoming-release-notes/2258.md | 6 ++++++ 7 files changed, 31 insertions(+), 18 deletions(-) create mode 100644 upcoming-release-notes/2258.md 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 4d380951f1d..9aa4e7a923c 100644 --- a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx +++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx @@ -50,7 +50,7 @@ export function ReportTable({ }); const renderItem = useCallback( - ({ item, groupByItem, mode, style, monthsCount }) => { + ({ item, groupByItem, mode, style, monthsCount, compact }) => { return ( ); }, 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 7db0209fde0..ea81dc53438 100644 --- a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableHeader.tsx +++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableHeader.tsx @@ -64,7 +64,7 @@ export function ReportTableHeader({ 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 index b0adffaa2dd..4779f22dfff 100644 --- a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableRow.tsx +++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableRow.tsx @@ -17,6 +17,7 @@ type ReportTableRowProps = { mode: string; style?: CSSProperties; monthsCount: number; + compact: boolean; }; export const ReportTableRow = memo( @@ -27,6 +28,7 @@ export const ReportTableRow = memo( mode, style, monthsCount, + compact, }: ReportTableRowProps) => { const average = amountToInteger(item[balanceTypeOp]) / monthsCount; return ( @@ -54,7 +56,7 @@ export const ReportTableRow = memo( @@ -94,7 +96,7 @@ export const ReportTableRow = memo( width="flex" privacyFilter style={{ - minWidth: 85, + minWidth: compact ? 80 : 125, ...styles.tnum, }} /> @@ -109,7 +111,7 @@ export const ReportTableRow = memo( } style={{ fontWeight: 600, - minWidth: 85, + minWidth: compact ? 80 : 125, ...styles.tnum, }} width="flex" @@ -124,7 +126,7 @@ export const ReportTableRow = memo( } style={{ fontWeight: 600, - minWidth: 85, + minWidth: compact ? 80 : 125, ...styles.tnum, }} width="flex" 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 c149e5d6cca..784f6054e24 100644 --- a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx +++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx @@ -85,7 +85,7 @@ export function ReportTableTotals({ return (