Skip to content
Snippets Groups Projects
Unverified Commit 4c4f2fd4 authored by Neil's avatar Neil Committed by GitHub
Browse files

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
parent 7a18827b
No related branches found
No related tags found
No related merge requests found
Showing with 361 additions and 59 deletions
......@@ -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',
......
......@@ -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>
......
......@@ -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>
);
}
// @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&nbsp;
{!report.id ? 'Unsaved report' : report.name}&nbsp;
</Text>
{savedStatus === 'modified' && <Text>(modified)&nbsp;</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>
);
}
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>
);
}
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>
);
}
......@@ -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
......
......@@ -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,
);
}
......
......@@ -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;
......
---
category: Enhancements
authors: [carkom]
---
Expanding the menu for saving reports and adding hooks and logic.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment