From 4c4f2fd42639d1c3fe05b238ef40329ea9ca78eb Mon Sep 17 00:00:00 2001 From: Neil <55785687+carkom@users.noreply.github.com> Date: Mon, 5 Feb 2024 14:40:46 -0800 Subject: [PATCH] Custom Reports: add save reports menu (#2257) * Add schema work * notes * merge fixes * Add Reports Save Menu * merge fixes * updates * notes * updates * updates * save updates fix * typecheck fixes * merge fixes * saveReport strict Typescript * fix sidebar * lint fix * fixing functionality plus clean up * clean up --- .../src/components/reports/ReportOptions.ts | 3 - .../src/components/reports/ReportSidebar.jsx | 15 +- .../src/components/reports/ReportTopbar.jsx | 18 +- .../src/components/reports/SaveReport.tsx | 176 ++++++++++++++---- .../src/components/reports/SaveReportMenu.tsx | 76 ++++++++ .../src/components/reports/SaveReportName.tsx | 74 ++++++++ .../reports/reports/CustomReport.jsx | 38 +++- .../src/client/data-hooks/reports.ts | 8 +- .../loot-core/src/types/models/reports.d.ts | 6 +- upcoming-release-notes/2257.md | 6 + 10 files changed, 361 insertions(+), 59 deletions(-) create mode 100644 packages/desktop-client/src/components/reports/SaveReportMenu.tsx create mode 100644 packages/desktop-client/src/components/reports/SaveReportName.tsx create mode 100644 upcoming-release-notes/2257.md diff --git a/packages/desktop-client/src/components/reports/ReportOptions.ts b/packages/desktop-client/src/components/reports/ReportOptions.ts index 8c2d361db..a79c2059a 100644 --- a/packages/desktop-client/src/components/reports/ReportOptions.ts +++ b/packages/desktop-client/src/components/reports/ReportOptions.ts @@ -11,8 +11,6 @@ const startDate = monthUtils.subMonths(monthUtils.currentMonth(), 5); const endDate = monthUtils.currentMonth(); export const defaultReport: CustomReportEntity = { - id: undefined, - name: 'Default', startDate, endDate, isDateStatic: false, @@ -24,7 +22,6 @@ export const defaultReport: CustomReportEntity = { showOffBudget: false, showHiddenCategories: false, showUncategorized: false, - selectedCategories: [], graphType: 'BarGraph', conditions: [], conditionsOp: 'and', diff --git a/packages/desktop-client/src/components/reports/ReportSidebar.jsx b/packages/desktop-client/src/components/reports/ReportSidebar.jsx index 0ccb5ef2b..46981a2c0 100644 --- a/packages/desktop-client/src/components/reports/ReportSidebar.jsx +++ b/packages/desktop-client/src/components/reports/ReportSidebar.jsx @@ -45,7 +45,7 @@ export function ReportSidebar({ }) { const [menuOpen, setMenuOpen] = useState(false); const onSelectRange = cond => { - onReportChange(null, 'modify'); + onReportChange({ type: 'modify' }); setDateRange(cond); switch (cond) { case 'All time': @@ -79,7 +79,7 @@ export function ReportSidebar({ }; const onChangeMode = cond => { - onReportChange(null, 'modify'); + onReportChange({ type: 'modify' }); setMode(cond); if (cond === 'time') { if (customReportItems.graphType === 'TableGraph') { @@ -110,7 +110,7 @@ export function ReportSidebar({ }; const onChangeSplit = cond => { - onReportChange(null, 'modify'); + onReportChange({ type: 'modify' }); setGroupBy(cond); if (customReportItems.mode === 'total') { if (customReportItems.graphType !== 'TableGraph') { @@ -128,7 +128,7 @@ export function ReportSidebar({ }; const onChangeBalanceType = cond => { - onReportChange(null, 'modify'); + onReportChange({ type: 'modify' }); setBalanceType(cond); }; @@ -275,6 +275,8 @@ export function ReportSidebar({ > <Menu onMenuSelect={type => { + onReportChange({ type: 'modify' }); + if (type === 'show-hidden-categories') { setShowHiddenCategories( !customReportItems.showHiddenCategories, @@ -464,7 +466,10 @@ export function ReportSidebar({ : false; })} selectedCategories={customReportItems.selectedCategories} - setSelectedCategories={setSelectedCategories} + setSelectedCategories={e => { + setSelectedCategories(e); + onReportChange({ type: 'modify' }); + }} showHiddenCategories={customReportItems.showHiddenCategories} /> </View> diff --git a/packages/desktop-client/src/components/reports/ReportTopbar.jsx b/packages/desktop-client/src/components/reports/ReportTopbar.jsx index d8944ae8c..d63744e9b 100644 --- a/packages/desktop-client/src/components/reports/ReportTopbar.jsx +++ b/packages/desktop-client/src/components/reports/ReportTopbar.jsx @@ -14,10 +14,11 @@ import { View } from '../common/View'; import { FilterButton } from '../filters/FiltersMenu'; import { GraphButton } from './GraphButton'; -import { SaveReportMenuButton } from './SaveReport'; +import { SaveReport } from './SaveReport'; export function ReportTopbar({ customReportItems, + report, savedStatus, setGraphType, setTypeDisabled, @@ -29,6 +30,7 @@ export function ReportTopbar({ onApplyFilter, onChangeViews, onReportChange, + onResetReports, }) { return ( <View @@ -43,6 +45,7 @@ export function ReportTopbar({ selected={customReportItems.graphType === 'TableGraph'} title="Data Table" onSelect={() => { + onReportChange({ type: 'modify' }); setGraphType('TableGraph'); onChangeViews('viewLegend', false); setTypeDisabled([]); @@ -60,6 +63,7 @@ export function ReportTopbar({ customReportItems.graphType === 'StackedBarGraph' } onSelect={() => { + onReportChange({ type: 'modify' }); if (customReportItems.mode === 'total') { setGraphType('BarGraph'); if (['Net'].includes(customReportItems.balanceType)) { @@ -84,6 +88,7 @@ export function ReportTopbar({ title="Area Graph" selected={customReportItems.graphType === 'AreaGraph'} onSelect={() => { + onReportChange({ type: 'modify' }); setGraphType('AreaGraph'); setGroupBy('Month'); onChangeViews('viewLegend', false); @@ -98,6 +103,7 @@ export function ReportTopbar({ title="Donut Graph" selected={customReportItems.graphType === 'DonutGraph'} onSelect={() => { + onReportChange({ type: 'modify' }); setGraphType('DonutGraph'); setTypeDisabled(['Net']); setBalanceType('Payment'); @@ -166,11 +172,17 @@ export function ReportTopbar({ hover onApply={e => { onApplyFilter(e); - onReportChange(null, 'modify'); + onReportChange({ type: 'modify' }); }} /> <View style={{ flex: 1 }} /> - <SaveReportMenuButton savedStatus={savedStatus} /> + <SaveReport + customReportItems={customReportItems} + report={report} + savedStatus={savedStatus} + onReportChange={onReportChange} + onResetReports={onResetReports} + /> </View> ); } diff --git a/packages/desktop-client/src/components/reports/SaveReport.tsx b/packages/desktop-client/src/components/reports/SaveReport.tsx index 00b59ada9..e7e26a3a5 100644 --- a/packages/desktop-client/src/components/reports/SaveReport.tsx +++ b/packages/desktop-client/src/components/reports/SaveReport.tsx @@ -1,45 +1,132 @@ -// @ts-strict-ignore -import React, { useState } from 'react'; +import React, { createRef, useState } from 'react'; + +import { v4 as uuidv4 } from 'uuid'; + +//import { send, sendCatch } from 'loot-core/src/platform/client/fetch'; +import { type CustomReportEntity } from 'loot-core/src/types/models'; import { SvgExpandArrow } from '../../icons/v0'; import { Button } from '../common/Button'; -import { Menu } from '../common/Menu'; -import { MenuTooltip } from '../common/MenuTooltip'; import { Text } from '../common/Text'; import { View } from '../common/View'; -function SaveReportMenu({ setMenuOpen }) { - return ( - <MenuTooltip width={150} onClose={() => setMenuOpen(false)}> - <Menu - onMenuSelect={item => { - switch (item) { - case 'save': - case 'clear': - setMenuOpen(false); - break; - default: - } - }} - items={[ - { - name: 'save', - text: 'Save new report', - disabled: true, - }, - { - name: 'clear', - text: 'Clear all', - disabled: true, - }, - ]} - /> - </MenuTooltip> - ); -} +import { SaveReportMenu } from './SaveReportMenu'; +import { SaveReportName } from './SaveReportName'; -export function SaveReportMenuButton({ savedStatus }: { savedStatus: string }) { +type SaveReportProps<T extends CustomReportEntity = CustomReportEntity> = { + customReportItems: T; + report: CustomReportEntity; + savedStatus: string; + onReportChange: ({ + savedReport, + type, + }: { + savedReport?: T; + type: string; + }) => void; + onResetReports: () => void; +}; + +export function SaveReport({ + customReportItems, + report, + savedStatus, + onReportChange, + onResetReports, +}: SaveReportProps) { + const [nameMenuOpen, setNameMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false); + const [menuItem, setMenuItem] = useState(''); + const [err, setErr] = useState(''); + const [res, setRes] = useState(''); + const [name, setName] = useState(report.name); + const inputRef = createRef<HTMLInputElement>(); + + const onAddUpdate = async (menuChoice: string) => { + let savedReport: CustomReportEntity; + //save existing states + savedReport = { + ...report, + ...customReportItems, + }; + + if (menuChoice === 'save-report') { + setRes(''); + //create new flow + /* + res = await sendCatch('report/create', { + ...savedReport, + }); + */ + savedReport = { + ...savedReport, + id: uuidv4(), + name, + }; + } + + if (menuChoice === 'rename-report') { + //rename + savedReport = { + ...savedReport, + name, + }; + } + + if (menuChoice === 'update-report') { + //send update and rename to DB + /* + res = await sendCatch('report/update', { + ...savedReport, + }); + */ + } + if (res !== '') { + setErr(res); + setNameMenuOpen(true); + } else { + setNameMenuOpen(false); + onReportChange({ + savedReport, + type: menuChoice === 'rename-report' ? 'rename' : 'add-update', + }); + } + }; + + const onMenuSelect = async (item: string) => { + setMenuItem(item); + switch (item) { + case 'rename-report': + setErr(''); + setMenuOpen(false); + setNameMenuOpen(true); + break; + case 'delete-report': + setMenuOpen(false); + //await send('report/delete', id); + onResetReports(); + break; + case 'update-report': + setErr(''); + setMenuOpen(false); + onAddUpdate(item); + break; + case 'save-report': + setErr(''); + setMenuOpen(false); + setNameMenuOpen(true); + break; + case 'reload-report': + setMenuOpen(false); + onReportChange({ type: 'reload' }); + break; + case 'reset-report': + setMenuOpen(false); + onResetReports(); + break; + default: + } + }; return ( <View @@ -63,12 +150,29 @@ export function SaveReportMenuButton({ savedStatus }: { savedStatus: string }) { flexShrink: 0, }} > - Unsaved Report + {!report.id ? 'Unsaved report' : report.name} </Text> {savedStatus === 'modified' && <Text>(modified) </Text>} <SvgExpandArrow width={8} height={8} style={{ marginRight: 5 }} /> </Button> - {menuOpen && <SaveReportMenu setMenuOpen={setMenuOpen} />} + {menuOpen && ( + <SaveReportMenu + onClose={() => setMenuOpen(false)} + report={report} + onMenuSelect={onMenuSelect} + savedStatus={savedStatus} + /> + )} + {nameMenuOpen && ( + <SaveReportName + onClose={() => setNameMenuOpen(false)} + menuItem={menuItem} + setName={setName} + inputRef={inputRef} + onAddUpdate={onAddUpdate} + err={err} + /> + )} </View> ); } diff --git a/packages/desktop-client/src/components/reports/SaveReportMenu.tsx b/packages/desktop-client/src/components/reports/SaveReportMenu.tsx new file mode 100644 index 000000000..01e311dd2 --- /dev/null +++ b/packages/desktop-client/src/components/reports/SaveReportMenu.tsx @@ -0,0 +1,76 @@ +import React from 'react'; + +import { type CustomReportEntity } from 'loot-core/src/types/models'; + +import { Menu } from '../common/Menu'; +import { MenuTooltip } from '../common/MenuTooltip'; + +export function SaveReportMenu({ + report, + onClose, + onMenuSelect, + savedStatus, +}: { + report: CustomReportEntity; + onClose: () => void; + onMenuSelect: (item: string) => void; + savedStatus: string; +}) { + return ( + <MenuTooltip width={150} onClose={onClose}> + <Menu + onMenuSelect={item => { + onMenuSelect(item); + }} + items={ + report.id === undefined + ? [ + { + name: 'save-report', + text: 'Save new report', + }, + { + name: 'reset-report', + text: 'Reset to default', + }, + ] + : savedStatus === 'saved' + ? [ + { name: 'rename-report', text: 'Rename' }, + { name: 'delete-report', text: 'Delete' }, + Menu.line, + { + name: 'save-report', + text: 'Save new report', + }, + { + name: 'reset-report', + text: 'Reset to default', + }, + ] + : [ + { name: 'rename-report', text: 'Rename' }, + { + name: 'update-report', + text: 'Update report', + }, + { + name: 'reload-report', + text: 'Revert changes', + }, + { name: 'delete-report', text: 'Delete' }, + Menu.line, + { + name: 'save-report', + text: 'Save new report', + }, + { + name: 'reset-report', + text: 'Reset to default', + }, + ] + } + /> + </MenuTooltip> + ); +} diff --git a/packages/desktop-client/src/components/reports/SaveReportName.tsx b/packages/desktop-client/src/components/reports/SaveReportName.tsx new file mode 100644 index 000000000..34c1d8176 --- /dev/null +++ b/packages/desktop-client/src/components/reports/SaveReportName.tsx @@ -0,0 +1,74 @@ +import React, { type RefObject, useEffect } from 'react'; + +import { theme } from '../../style'; +import { Button } from '../common/Button'; +import { Input } from '../common/Input'; +import { MenuTooltip } from '../common/MenuTooltip'; +import { Stack } from '../common/Stack'; +import { Text } from '../common/Text'; +import { FormField, FormLabel } from '../forms'; + +type SaveReportNameProps = { + onClose: () => void; + menuItem: string; + setName: (name: string) => void; + inputRef: RefObject<HTMLInputElement>; + onAddUpdate: (menuItem: string) => void; + err: string; +}; + +export function SaveReportName({ + onClose, + menuItem, + setName, + inputRef, + onAddUpdate, + err, +}: SaveReportNameProps) { + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, []); + + return ( + <MenuTooltip width={325} onClose={onClose}> + {menuItem !== 'update-report' && ( + <form> + <Stack + direction="row" + justify="flex-end" + align="center" + style={{ padding: 10 }} + > + <FormField style={{ flex: 1 }}> + <FormLabel + title="Report Name" + htmlFor="name-field" + style={{ userSelect: 'none' }} + /> + <Input inputRef={inputRef} onUpdate={setName} /> + </FormField> + <Button + type="primary" + style={{ marginTop: 18 }} + onClick={e => { + e.preventDefault(); + onAddUpdate(menuItem); + }} + > + {menuItem === 'save-report' ? 'Add' : 'Update'} + </Button> + </Stack> + </form> + )} + {err !== '' ? ( + <Stack direction="row" align="center" style={{ padding: 10 }}> + <Text style={{ color: theme.errorText }}>{err}</Text> + </Stack> + ) : ( + <Text /> + )} + </MenuTooltip> + ); +} diff --git a/packages/desktop-client/src/components/reports/reports/CustomReport.jsx b/packages/desktop-client/src/components/reports/reports/CustomReport.jsx index 7e679f233..97fd06aaa 100644 --- a/packages/desktop-client/src/components/reports/reports/CustomReport.jsx +++ b/packages/desktop-client/src/components/reports/reports/CustomReport.jsx @@ -91,7 +91,7 @@ export function CustomReport() { const months = monthUtils.rangeInclusive(startDate, endDate); useEffect(() => { - if (selectedCategories.length === 0 && categories.list.length !== 0) { + if (selectedCategories === undefined && categories.list.length !== 0) { setSelectedCategories(categories.list); } }, [categories, selectedCategories]); @@ -203,8 +203,6 @@ export function CustomReport() { const data = { ...graphData, groupedData }; const customReportItems = { - id: undefined, - name: undefined, startDate, endDate, isDateStatic, @@ -232,7 +230,7 @@ export function CustomReport() { const onChangeDates = (startDate, endDate) => { setStartDate(startDate); setEndDate(endDate); - setSavedStatus('modified'); + onReportChange({ type: 'modify' }); }; const onChangeViews = (viewType, status) => { @@ -247,12 +245,37 @@ export function CustomReport() { } }; + const onResetReports = () => { + const selectAll = []; + categories.grouped.map(categoryGroup => + categoryGroup.categories.map(category => selectAll.push(category)), + ); + + setStartDate(defaultReport.startDate); + setEndDate(defaultReport.endDate); + setIsDateStatic(defaultReport.isDateStatic); + setDateRange(defaultReport.dateRange); + setMode(defaultReport.mode); + setGroupBy(defaultReport.groupBy); + setBalanceType(defaultReport.balanceType); + setShowEmpty(defaultReport.showEmpty); + setShowOffBudget(defaultReport.showOffBudget); + setShowHiddenCategories(defaultReport.showHiddenCategories); + setShowUncategorized(defaultReport.showUncategorized); + setSelectedCategories(selectAll); + setGraphType(defaultReport.graphType); + onApplyFilter(null); + onCondOpChange(defaultReport.conditionsOp); + setReport(defaultReport); + setSavedStatus('new'); + }; + const onChangeAppliedFilter = (filter, changedElement) => { - onReportChange(null, 'modify'); + onReportChange({ type: 'modify' }); return changedElement(filter); }; - const onReportChange = (savedReport, type) => { + const onReportChange = ({ savedReport, type }) => { switch (type) { case 'add-update': setSavedStatus('saved'); @@ -278,6 +301,7 @@ export function CustomReport() { setBalanceType(report.balanceType); setShowEmpty(report.showEmpty); setShowOffBudget(report.showOffBudget); + setShowHiddenCategories(report.showHiddenCategories); setShowUncategorized(report.showUncategorized); setSelectedCategories(report.selectedCategories); setGraphType(report.graphType); @@ -330,6 +354,7 @@ export function CustomReport() { > <ReportTopbar customReportItems={customReportItems} + report={report} savedStatus={savedStatus} setGraphType={setGraphType} setTypeDisabled={setTypeDisabled} @@ -341,6 +366,7 @@ export function CustomReport() { onApplyFilter={onApplyFilter} onChangeViews={onChangeViews} onReportChange={onReportChange} + onResetReports={onResetReports} /> {filters && filters.length > 0 && ( <View diff --git a/packages/loot-core/src/client/data-hooks/reports.ts b/packages/loot-core/src/client/data-hooks/reports.ts index e347c48fc..07142d2cb 100644 --- a/packages/loot-core/src/client/data-hooks/reports.ts +++ b/packages/loot-core/src/client/data-hooks/reports.ts @@ -31,9 +31,11 @@ export function useReports(): CustomReportEntity[] { /** Sort reports by alphabetical order */ function sort(reports: CustomReportEntity[]) { return reports.sort((a, b) => - a.name - .trim() - .localeCompare(b.name.trim(), undefined, { ignorePunctuation: true }), + a.name && b.name + ? a.name.trim().localeCompare(b.name.trim(), undefined, { + ignorePunctuation: true, + }) + : 0, ); } diff --git a/packages/loot-core/src/types/models/reports.d.ts b/packages/loot-core/src/types/models/reports.d.ts index a9b8269a4..ef888a2a8 100644 --- a/packages/loot-core/src/types/models/reports.d.ts +++ b/packages/loot-core/src/types/models/reports.d.ts @@ -2,8 +2,8 @@ import { CategoryEntity } from './category'; import { type RuleConditionEntity } from './rule'; export interface CustomReportEntity { - id: string | undefined; - name: string; + id?: string; + name?: string; startDate: string; endDate: string; isDateStatic: boolean; @@ -15,7 +15,7 @@ export interface CustomReportEntity { showOffBudget: boolean; showHiddenCategories: boolean; showUncategorized: boolean; - selectedCategories: CategoryEntity[]; + selectedCategories?: CategoryEntity[]; graphType: string; conditions?: RuleConditionEntity[]; conditionsOp: string; diff --git a/upcoming-release-notes/2257.md b/upcoming-release-notes/2257.md new file mode 100644 index 000000000..9413515c7 --- /dev/null +++ b/upcoming-release-notes/2257.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [carkom] +--- + +Expanding the menu for saving reports and adding hooks and logic. -- GitLab