diff --git a/packages/desktop-client/src/components/common/Select.tsx b/packages/desktop-client/src/components/common/Select.tsx index c9fc00b411d..0ecfde802e1 100644 --- a/packages/desktop-client/src/components/common/Select.tsx +++ b/packages/desktop-client/src/components/common/Select.tsx @@ -28,6 +28,7 @@ type SelectProps = { disabled?: boolean; disabledKeys?: Value[]; style?: CSSProperties; + popoverStyle?: CSSProperties; }; /** @@ -53,6 +54,7 @@ export function Select({ disabled = false, disabledKeys = [], style = {}, + popoverStyle = {}, }: SelectProps) { const targetOption = options .filter(isValueOption) @@ -108,6 +110,7 @@ export function Select({ placement="bottom start" isOpen={isOpen} onOpenChange={() => setIsOpen(false)} + style={popoverStyle} > { diff --git a/packages/desktop-client/src/components/reports/DateRange.tsx b/packages/desktop-client/src/components/reports/DateRange.tsx index faefe46e962..6734a0b8ec5 100644 --- a/packages/desktop-client/src/components/reports/DateRange.tsx +++ b/packages/desktop-client/src/components/reports/DateRange.tsx @@ -40,7 +40,14 @@ export function DateRange({ start, end, type }: DateRangeProps): ReactElement { } let content: string | ReactElement; - if (startDate.getFullYear() !== endDate.getFullYear()) { + if (['budget', 'average'].includes(type || '')) { + content = ( +
+ Compare {d.format(startDate, 'MMM yyyy')} to{' '} + {type === 'budget' ? 'budgeted' : 'average'} +
+ ); + } else if (startDate.getFullYear() !== endDate.getFullYear()) { content = (
{type && 'Compare '} diff --git a/packages/desktop-client/src/components/reports/Header.tsx b/packages/desktop-client/src/components/reports/Header.tsx index b1aad0805f8..d9322c22735 100644 --- a/packages/desktop-client/src/components/reports/Header.tsx +++ b/packages/desktop-client/src/components/reports/Header.tsx @@ -73,7 +73,7 @@ export function Header({ flexShrink: 0, }} > - {!['/reports/custom', '/reports/spending'].includes(path) && ( + {!['/reports/custom'].includes(path) && ( ) : item.type === 'spending-card' ? ( onMetaChange(item, newMeta)} diff --git a/packages/desktop-client/src/components/reports/ReportRouter.tsx b/packages/desktop-client/src/components/reports/ReportRouter.tsx index 08dfa109e89..71719624faf 100644 --- a/packages/desktop-client/src/components/reports/ReportRouter.tsx +++ b/packages/desktop-client/src/components/reports/ReportRouter.tsx @@ -17,6 +17,7 @@ export function ReportRouter() { } /> } /> } /> + } /> ); } diff --git a/packages/desktop-client/src/components/reports/graphs/SpendingGraph.tsx b/packages/desktop-client/src/components/reports/graphs/SpendingGraph.tsx index 4c6660e40cb..b8c03529146 100644 --- a/packages/desktop-client/src/components/reports/graphs/SpendingGraph.tsx +++ b/packages/desktop-client/src/components/reports/graphs/SpendingGraph.tsx @@ -1,5 +1,5 @@ // @ts-strict-ignore -import React from 'react'; +import React, { type ComponentProps } from 'react'; import { useTranslation } from 'react-i18next'; import { css } from 'glamor'; @@ -32,19 +32,22 @@ type PayloadItem = { totalDebts: number | string; totalTotals: number | string; day: string; - months: { - date: string; - cumulative: number | string; - }; + months: Record< + string, + { + date: string; + cumulative: number; + } + >; }; }; type CustomTooltipProps = { active?: boolean; payload?: PayloadItem[]; - balanceTypeOp?: string; - selection?: string; - compare?: string; + balanceTypeOp: 'cumulative'; + selection: string | 'budget' | 'average'; + compare: string; }; const CustomTooltip = ({ @@ -59,7 +62,7 @@ const CustomTooltip = ({ if (active && payload && payload.length) { const comparison = ['average', 'budget'].includes(selection) ? payload[0].payload[selection] * -1 - : payload[0].payload.months[selection].cumulative * -1; + : payload[0].payload.months[selection]?.cumulative * -1; return (
- {payload[0].payload.months[compare].cumulative ? ( + {payload[0].payload.months[compare]?.cumulative ? ( ) : null} @@ -102,11 +105,11 @@ const CustomTooltip = ({ right={amountToCurrency(comparison)} /> )} - {payload[0].payload.months[compare].cumulative ? ( + {payload[0].payload.months[compare]?.cumulative ? ( @@ -122,7 +125,7 @@ type SpendingGraphProps = { style?: CSSProperties; data: SpendingEntity; compact?: boolean; - mode: string; + mode: 'single-month' | 'budget' | 'average'; compare: string; compareTo: string; }; @@ -138,27 +141,30 @@ export function SpendingGraph({ const privacyMode = usePrivacyMode(); const balanceTypeOp = 'cumulative'; - const selection = mode === 'singleMonth' ? compareTo : mode; + const selection = mode === 'single-month' ? compareTo : mode; const thisMonthMax = data.intervalData.reduce((a, b) => - a.months[compare][balanceTypeOp] < b.months[compare][balanceTypeOp] ? a : b, - ).months[compare][balanceTypeOp]; + a.months[compare]?.[balanceTypeOp] < b.months[compare]?.[balanceTypeOp] + ? a + : b, + ).months[compare]?.[balanceTypeOp]; const selectionMax = ['average', 'budget'].includes(selection) ? data.intervalData[27][selection] : data.intervalData.reduce((a, b) => - a.months[selection][balanceTypeOp] < b.months[selection][balanceTypeOp] + a.months[selection]?.[balanceTypeOp] < + b.months[selection]?.[balanceTypeOp] ? a : b, - ).months[selection][balanceTypeOp]; + ).months[selection]?.[balanceTypeOp]; const maxYAxis = selectionMax > thisMonthMax; const dataMax = Math.max( - ...data.intervalData.map(i => i.months[compare].cumulative), + ...data.intervalData.map(i => i.months[compare]?.cumulative), ); const dataMin = Math.min( - ...data.intervalData.map(i => i.months[compare].cumulative), + ...data.intervalData.map(i => i.months[compare]?.cumulative), ); - const tickFormatter = tick => { + const tickFormatter: ComponentProps['tickFormatter'] = tick => { if (!privacyMode) return `${amountToCurrencyNoDecimal(tick)}`; // Formats the tick values as strings with commas return '...'; }; @@ -179,7 +185,7 @@ export function SpendingGraph({ return obj[month] && -1 * obj[month]; } else { return ( - obj.months[month][balanceTypeOp] && + obj.months[month]?.[balanceTypeOp] && -1 * obj.months[month][balanceTypeOp] ); } diff --git a/packages/desktop-client/src/components/reports/reportRanges.ts b/packages/desktop-client/src/components/reports/reportRanges.ts index 7c3004894af..1fa289b5f72 100644 --- a/packages/desktop-client/src/components/reports/reportRanges.ts +++ b/packages/desktop-client/src/components/reports/reportRanges.ts @@ -166,7 +166,7 @@ export function getLatestRange(offset: number) { } export function calculateTimeRange( - timeFrame?: TimeFrame, + timeFrame?: Partial, defaultTimeFrame?: TimeFrame, ) { const start = @@ -181,8 +181,48 @@ export function calculateTimeRange( return getFullRange(start); } if (mode === 'sliding-window') { - return getLatestRange(monthUtils.differenceInCalendarMonths(end, start)); + const offset = monthUtils.differenceInCalendarMonths(end, start); + + if (start > end) { + return [ + monthUtils.currentMonth(), + monthUtils.subMonths(monthUtils.currentMonth(), -offset), + 'sliding-window', + ] as const; + } + + return getLatestRange(offset); } return [start, end, 'static'] as const; } + +export function calculateSpendingReportTimeRange({ + compare, + compareTo, + isLive = true, + mode = 'single-month', +}: { + compare?: string; + compareTo?: string; + isLive?: boolean; + mode?: 'budget' | 'average' | 'single-month'; +}): [string, string] { + if (['budget', 'average'].includes(mode) && isLive) { + return [monthUtils.currentMonth(), monthUtils.currentMonth()]; + } + + const [start, end] = calculateTimeRange( + { + start: compare, + end: compareTo, + mode: (isLive ?? true) ? 'sliding-window' : 'static', + }, + { + start: monthUtils.currentMonth(), + end: monthUtils.subMonths(monthUtils.currentMonth(), 1), + mode: 'sliding-window', + }, + ); + return [start, end]; +} diff --git a/packages/desktop-client/src/components/reports/reports/Spending.tsx b/packages/desktop-client/src/components/reports/reports/Spending.tsx index ec6c8100850..4f5407a4a1e 100644 --- a/packages/desktop-client/src/components/reports/reports/Spending.tsx +++ b/packages/desktop-client/src/components/reports/reports/Spending.tsx @@ -1,14 +1,20 @@ import React, { useState, useMemo, useEffect } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; +import { useParams } from 'react-router-dom'; import * as d from 'date-fns'; +import { addNotification } from 'loot-core/client/actions'; +import { useWidget } from 'loot-core/client/data-hooks/widget'; import { send } from 'loot-core/src/platform/client/fetch'; import * as monthUtils from 'loot-core/src/shared/months'; import { amountToCurrency } from 'loot-core/src/shared/util'; +import { type SpendingWidget } from 'loot-core/types/models'; import { type RuleConditionEntity } from 'loot-core/types/models/rule'; +import { useFeatureFlag } from '../../../hooks/useFeatureFlag'; import { useFilters } from '../../../hooks/useFilters'; -import { useLocalPref } from '../../../hooks/useLocalPref'; import { useNavigate } from '../../../hooks/useNavigate'; import { useResponsive } from '../../../ResponsiveProvider'; import { theme, styles } from '../../../style'; @@ -28,11 +34,34 @@ import { PrivacyFilter } from '../../PrivacyFilter'; import { SpendingGraph } from '../graphs/SpendingGraph'; import { LoadingIndicator } from '../LoadingIndicator'; import { ModeButton } from '../ModeButton'; +import { calculateSpendingReportTimeRange } from '../reportRanges'; import { createSpendingSpreadsheet } from '../spreadsheets/spending-spreadsheet'; import { useReport } from '../useReport'; import { fromDateRepr } from '../util'; export function Spending() { + const params = useParams(); + const { data: widget, isLoading } = useWidget( + params.id ?? '', + 'spending-card', + ); + + if (isLoading) { + return ; + } + + return ; +} + +type SpendingInternalProps = { + widget: SpendingWidget; +}; + +function SpendingInternal({ widget }: SpendingInternalProps) { + const isDashboardsFeatureEnabled = useFeatureFlag('dashboards'); + const dispatch = useDispatch(); + const { t } = useTranslation(); + const { conditions, conditionsOp, @@ -40,42 +69,23 @@ export function Spending() { onDelete: onDeleteFilter, onUpdate: onUpdateFilter, onConditionsOpChange, - } = useFilters(); + } = useFilters( + widget?.meta?.conditions, + widget?.meta?.conditionsOp, + ); const emptyIntervals: { name: string; pretty: string }[] = []; const [allIntervals, setAllIntervals] = useState(emptyIntervals); - const [spendingReportFilter = '', setSpendingReportFilter] = useLocalPref( - 'spendingReportFilter', - ); - const [spendingReportMode = 'singleMonth', setSpendingReportMode] = - useLocalPref('spendingReportMode'); - const [ - spendingReportCompare = monthUtils.currentMonth(), - setSpendingReportCompare, - ] = useLocalPref('spendingReportCompare'); - const [ - spendingReportCompareTo = monthUtils.currentMonth(), - setSpendingReportCompareTo, - ] = useLocalPref('spendingReportCompareTo'); - - const isDateValid = monthUtils.parseDate(spendingReportCompare); - const [dataCheck, setDataCheck] = useState(false); - const [mode, setMode] = useState(spendingReportMode); - const [compare, setCompare] = useState( - isDateValid.toString() === 'Invalid Date' - ? monthUtils.currentMonth() - : spendingReportCompare, + const initialReportMode = widget?.meta?.mode ?? 'single-month'; + const [initialCompare, initialCompareTo] = calculateSpendingReportTimeRange( + widget?.meta ?? {}, ); - const [compareTo, setCompareTo] = useState(spendingReportCompareTo); + const [compare, setCompare] = useState(initialCompare); + const [compareTo, setCompareTo] = useState(initialCompareTo); + const [isLive, setIsLive] = useState(widget?.meta?.isLive ?? true); - const parseFilter = spendingReportFilter && JSON.parse(spendingReportFilter); - const filterSaved = - JSON.stringify(parseFilter.conditions) === JSON.stringify(conditions) && - parseFilter.conditionsOp === conditionsOp && - spendingReportMode === mode && - spendingReportCompare === compare && - spendingReportCompareTo === compareTo; + const [reportMode, setReportMode] = useState(initialReportMode); useEffect(() => { async function run() { @@ -104,43 +114,47 @@ export function Spending() { setAllIntervals(allMonths); } run(); - const checkFilter = - spendingReportFilter && JSON.parse(spendingReportFilter); - if (checkFilter.conditions) { - onApplyFilter(checkFilter); - } - }, [onApplyFilter, spendingReportFilter]); - - const getGraphData = useMemo(() => { - setDataCheck(false); - return createSpendingSpreadsheet({ - conditions, - conditionsOp, - setDataCheck, - compare, - compareTo, - }); - }, [conditions, conditionsOp, compare, compareTo]); + }, []); + + const getGraphData = useMemo( + () => + createSpendingSpreadsheet({ + conditions, + conditionsOp, + compare, + compareTo, + }), + [conditions, conditionsOp, compare, compareTo], + ); const data = useReport('default', getGraphData); const navigate = useNavigate(); const { isNarrowWidth } = useResponsive(); - if (!data) { - return null; - } - - const saveFilter = () => { - setSpendingReportFilter( - JSON.stringify({ - conditionsOp, + async function onSaveWidget() { + await send('dashboard-update-widget', { + id: widget?.id, + meta: { + ...(widget.meta ?? {}), conditions, + conditionsOp, + compare, + compareTo, + isLive, + mode: reportMode, + }, + }); + dispatch( + addNotification({ + type: 'message', + message: t('Dashboard widget successfully saved.'), }), ); - setSpendingReportMode(mode); - setSpendingReportCompare(compare); - setSpendingReportCompareTo(compareTo); - }; + } + + if (!data) { + return null; + } const showAverage = data.intervalData[27].months[monthUtils.subMonths(compare, 3)] && @@ -156,169 +170,230 @@ export function Spending() { : monthUtils.getDay(monthUtils.currentDay()) - 1; const showCompareTo = Math.abs(data.intervalData[27].compareTo) > 0; - const showCompare = - compare === monthUtils.currentMonth() || - Math.abs(data.intervalData[27].compare) > 0; + + const title = widget?.meta?.name ?? t('Monthly Spending'); + return ( navigate('/reports')} /> } /> ) : ( - + ) } padding={0} > - - - Compare - - { - setCompareTo(e); - }} - options={allIntervals.map(({ name, pretty }) => [name, pretty])} - disabled={mode !== 'singleMonth'} - /> - {!isNarrowWidth && ( - )} - - setMode('singleMonth')} - > - Single month - - setMode('budget')} - style={{ - backgroundColor: 'inherit', - }} - > - Budgeted - - setMode('average')} - style={{ - backgroundColor: 'inherit', + flexDirection: 'row', + alignItems: 'center', + flexShrink: 0, }} > - Average - - - {!isNarrowWidth && ( - - )} - - - - Save compare and filter options} - style={{ - ...styles.tooltip, - lineHeight: 1.5, - padding: '6px 10px', - marginLeft: 10, - }} - > - + + + + )} + + + + Compare + + [name, pretty]) + : [ + [ + 'label', + reportMode === 'budget' + ? t('Budgeted') + : t('Average spent'), + ], + ] + } + disabled={reportMode !== 'single-month'} + style={{ width: 150 }} + popoverStyle={{ width: 150 }} + /> + + + + + - {filterSaved ? 'Saved' : 'Save'} - - - + { + setReportMode('single-month'); + }} + > + Single month + + { + setReportMode('budget'); + }} + style={{ + backgroundColor: 'inherit', + }} + > + Budgeted + + { + setReportMode('average'); + }} + style={{ + backgroundColor: 'inherit', + }} + > + Average + + + + + + + + + + {widget && ( + + Save compare and filter options + + } + style={{ + ...styles.tooltip, + lineHeight: 1.5, + padding: '6px 10px', + marginLeft: 10, + }} + > + + + )} + + + )} + + {conditions && conditions.length > 0 && ( + + + + )} - {conditions && conditions.length > 0 && ( - - - - )} )} - {mode === 'singleMonth' && ( + {reportMode === 'single-month' && ( - {!showCompare || - (mode === 'singleMonth' && !showCompareTo) || - (mode === 'average' && !showAverage) ? ( - -

Additional data required to generate graph

- - Currently, there is insufficient data to display selected - information regarding your spending. Please adjust selection - options to enable graph visualization. - -
- ) : dataCheck ? ( + {data ? ( ) : ( - + )} {showAverage && ( - - - How are “Average” and “Spent Average MTD” calculated? - - - - They are both the average cumulative spending by day for the - three months before the selected “compare” month. - + + + + How are “Average” and “Spent Average MTD” calculated? + + + + They are both the average cumulative spending by day for + the three months before the selected “compare” month. + + )}
diff --git a/packages/desktop-client/src/components/reports/reports/SpendingCard.tsx b/packages/desktop-client/src/components/reports/reports/SpendingCard.tsx index dd3cffe0699..306d92dca64 100644 --- a/packages/desktop-client/src/components/reports/reports/SpendingCard.tsx +++ b/packages/desktop-client/src/components/reports/reports/SpendingCard.tsx @@ -6,7 +6,6 @@ import { amountToCurrency } from 'loot-core/src/shared/util'; import { type SpendingWidget } from 'loot-core/src/types/models'; import { useFeatureFlag } from '../../../hooks/useFeatureFlag'; -import { useLocalPref } from '../../../hooks/useLocalPref'; import { styles } from '../../../style/styles'; import { theme } from '../../../style/theme'; import { Block } from '../../common/Block'; @@ -17,12 +16,14 @@ import { SpendingGraph } from '../graphs/SpendingGraph'; import { LoadingIndicator } from '../LoadingIndicator'; import { ReportCard } from '../ReportCard'; import { ReportCardName } from '../ReportCardName'; +import { calculateSpendingReportTimeRange } from '../reportRanges'; import { createSpendingSpreadsheet } from '../spreadsheets/spending-spreadsheet'; import { useReport } from '../useReport'; import { MissingReportCard } from './MissingReportCard'; type SpendingCardProps = { + widgetId: string; isEditing?: boolean; meta?: SpendingWidget['meta']; onMetaChange: (newMeta: SpendingWidget['meta']) => void; @@ -30,50 +31,36 @@ type SpendingCardProps = { }; export function SpendingCard({ + widgetId, isEditing, - meta, + meta = {}, onMetaChange, onRemove, }: SpendingCardProps) { + const isDashboardsFeatureEnabled = useFeatureFlag('dashboards'); const { t } = useTranslation(); + const [compare, compareTo] = calculateSpendingReportTimeRange(meta ?? {}); + const [isCardHovered, setIsCardHovered] = useState(false); - const [spendingReportFilter = ''] = useLocalPref('spendingReportFilter'); - const [spendingReportMode = 'singleMonth'] = - useLocalPref('spendingReportMode'); - const [spendingReportCompare = monthUtils.currentMonth()] = useLocalPref( - 'spendingReportCompare', - ); - const [spendingReportCompareTo = monthUtils.currentMonth()] = useLocalPref( - 'spendingReportCompareTo', - ); + const spendingReportMode = meta?.mode ?? 'single-month'; const [nameMenuOpen, setNameMenuOpen] = useState(false); const selection = - spendingReportMode === 'singleMonth' ? 'compareTo' : spendingReportMode; - const parseFilter = spendingReportFilter && JSON.parse(spendingReportFilter); - const isDateValid = monthUtils.parseDate(spendingReportCompare); + spendingReportMode === 'single-month' ? 'compareTo' : spendingReportMode; const getGraphData = useMemo(() => { return createSpendingSpreadsheet({ - conditions: parseFilter.conditions, - conditionsOp: parseFilter.conditionsOp, - compare: - isDateValid.toString() === 'Invalid Date' - ? monthUtils.currentMonth() - : spendingReportCompare, - compareTo: spendingReportCompareTo, + conditions: meta?.conditions, + conditionsOp: meta?.conditionsOp, + compare, + compareTo, }); - }, [ - parseFilter, - spendingReportCompare, - spendingReportCompareTo, - isDateValid, - ]); + }, [meta?.conditions, meta?.conditionsOp, compare, compareTo]); const data = useReport('default', getGraphData); const todayDay = - spendingReportCompare !== monthUtils.currentMonth() + compare !== monthUtils.currentMonth() ? 27 : monthUtils.getDay(monthUtils.currentDay()) - 1 >= 28 ? 27 @@ -82,7 +69,6 @@ export function SpendingCard({ data && data.intervalData[todayDay][selection] - data.intervalData[todayDay].compare; - const showCompareTo = data && Math.abs(data.intervalData[27].compareTo) > 0; const spendingReportFeatureFlag = useFeatureFlag('spendingReport'); @@ -98,7 +84,11 @@ export function SpendingCard({ return ( setNameMenuOpen(false)} />
- {data && showCompareTo && ( + {data && ( )} - {!showCompareTo || isDateValid.toString() === 'Invalid Date' ? ( - -

- Additional data required to generate graph -

-
- ) : data ? ( + {data ? ( ) : ( - + )}
diff --git a/packages/desktop-client/src/components/reports/spreadsheets/spending-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/spending-spreadsheet.ts index 0ffa6d7422e..70619281fdd 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/spending-spreadsheet.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/spending-spreadsheet.ts @@ -18,7 +18,6 @@ import { makeQuery } from './makeQuery'; type createSpendingSpreadsheetProps = { conditions?: RuleConditionEntity[]; conditionsOp?: string; - setDataCheck?: (value: boolean) => void; compare?: string; compareTo?: string; }; @@ -26,7 +25,6 @@ type createSpendingSpreadsheetProps = { export function createSpendingSpreadsheet({ conditions = [], conditionsOp, - setDataCheck, compare, compareTo, }: createSpendingSpreadsheetProps) { @@ -256,6 +254,5 @@ export function createSpendingSpreadsheet({ totalAssets: integerToAmount(totalAssets), totalTotals: integerToAmount(totalAssets + totalDebts), }); - setDataCheck?.(true); }; } diff --git a/packages/loot-core/src/types/models/dashboard.d.ts b/packages/loot-core/src/types/models/dashboard.d.ts index a2d7252b1b1..0b79913ea98 100644 --- a/packages/loot-core/src/types/models/dashboard.d.ts +++ b/packages/loot-core/src/types/models/dashboard.d.ts @@ -41,7 +41,15 @@ export type CashFlowWidget = AbstractWidget< >; export type SpendingWidget = AbstractWidget< 'spending-card', - { name?: string } | null + { + name?: string; + conditions?: RuleConditionEntity[]; + conditionsOp?: 'and' | 'or'; + compare?: string; + compareTo?: string; + isLive?: boolean; + mode?: 'single-month' | 'budget' | 'average'; + } | null >; export type CustomReportWidget = AbstractWidget< 'custom-report', diff --git a/packages/loot-core/src/types/models/reports.d.ts b/packages/loot-core/src/types/models/reports.d.ts index 465352a8e30..e59e655241d 100644 --- a/packages/loot-core/src/types/models/reports.d.ts +++ b/packages/loot-core/src/types/models/reports.d.ts @@ -30,8 +30,6 @@ export type balanceTypeOpType = | 'netAssets' | 'netDebts'; -export type spendingReportModeType = 'singleMonth' | 'average' | 'budget'; - export type SpendingMonthEntity = Record< string | number, { diff --git a/packages/loot-core/src/types/prefs.d.ts b/packages/loot-core/src/types/prefs.d.ts index 984225da3db..f1066746732 100644 --- a/packages/loot-core/src/types/prefs.d.ts +++ b/packages/loot-core/src/types/prefs.d.ts @@ -1,5 +1,3 @@ -import { spendingReportModeType } from './models/reports'; - export type FeatureFlag = | 'dashboards' | 'reportBudget' @@ -65,10 +63,6 @@ export type LocalPrefs = Partial<{ reportsViewLegend: boolean; reportsViewSummary: boolean; reportsViewLabel: boolean; - spendingReportFilter: string; - spendingReportMode: spendingReportModeType; - spendingReportCompare: string; - spendingReportCompareTo: string; sidebarWidth: number; 'mobile.showSpentColumn': boolean; }>; diff --git a/upcoming-release-notes/3432.md b/upcoming-release-notes/3432.md new file mode 100644 index 00000000000..afdbed30aed --- /dev/null +++ b/upcoming-release-notes/3432.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [MatissJanis] +--- + +Dashboards: ability to save filters & time-range on spending widgets.