diff --git a/packages/desktop-client/src/components/reports/Overview.tsx b/packages/desktop-client/src/components/reports/Overview.tsx index 0a0d5f7455d5df48a5b5ce8f9dfe49640f9c21fb..0587f6dc7d0bbb90fadd812f0dc6d72e83b56721 100644 --- a/packages/desktop-client/src/components/reports/Overview.tsx +++ b/packages/desktop-client/src/components/reports/Overview.tsx @@ -46,10 +46,9 @@ function isCustomReportWidget(widget: Widget): widget is CustomReportWidget { return widget.type === 'custom-report'; } -function useWidgetLayout(widgets: Widget[]): (Layout & { - type: Widget['type']; - meta: Widget['meta']; -})[] { +type LayoutWidget = Layout & Pick<Widget, 'type' | 'meta'>; + +function useWidgetLayout(widgets: Widget[]): LayoutWidget[] { return widgets.map(widget => ({ i: widget.id, type: widget.type, @@ -290,6 +289,16 @@ export function Overview() { ); }; + const onMetaChange = <T extends LayoutWidget>( + widget: T, + newMeta: T['meta'], + ) => { + send('dashboard-update-widget', { + id: widget.i, + meta: newMeta, + }); + }; + const accounts = useAccounts(); if (isLoading) { @@ -494,16 +503,22 @@ export function Overview() { <NetWorthCard isEditing={isEditing} accounts={accounts} + meta={item.meta && 'name' in item.meta ? item.meta : {}} + onMetaChange={newMeta => onMetaChange(item, newMeta)} onRemove={() => onRemoveWidget(item.i)} /> ) : item.type === 'cash-flow-card' ? ( <CashFlowCard isEditing={isEditing} + meta={item.meta && 'name' in item.meta ? item.meta : {}} + onMetaChange={newMeta => onMetaChange(item, newMeta)} onRemove={() => onRemoveWidget(item.i)} /> ) : item.type === 'spending-card' ? ( <SpendingCard isEditing={isEditing} + meta={item.meta && 'name' in item.meta ? item.meta : {}} + onMetaChange={newMeta => onMetaChange(item, newMeta)} onRemove={() => onRemoveWidget(item.i)} /> ) : item.type === 'custom-report' ? ( diff --git a/packages/desktop-client/src/components/reports/ReportCardName.tsx b/packages/desktop-client/src/components/reports/ReportCardName.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3eac1ece293f46a8afbc55083793be8a9d8e7504 --- /dev/null +++ b/packages/desktop-client/src/components/reports/ReportCardName.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import { styles } from '../../style'; +import { Block } from '../common/Block'; +import { InitialFocus } from '../common/InitialFocus'; +import { Input } from '../common/Input'; + +import { NON_DRAGGABLE_AREA_CLASS_NAME } from './constants'; + +type ReportCardNameProps = { + name: string; + isEditing: boolean; + onChange: (newName: string) => void; + onClose: () => void; +}; + +export const ReportCardName = ({ + name, + isEditing, + onChange, + onClose, +}: ReportCardNameProps) => { + if (isEditing) { + return ( + <InitialFocus> + <Input + className={NON_DRAGGABLE_AREA_CLASS_NAME} + defaultValue={name} + onEnter={e => onChange(e.currentTarget.value)} + onUpdate={onChange} + onEscape={onClose} + style={{ + fontSize: 15, + fontWeight: 500, + marginTop: -6, + marginBottom: -1, + marginLeft: -6, + width: Math.max(20, name.length) + 'ch', + }} + /> + </InitialFocus> + ); + } + + return ( + <Block + style={{ + ...styles.mediumText, + fontWeight: 500, + marginBottom: 5, + }} + role="heading" + > + {name} + </Block> + ); +}; diff --git a/packages/desktop-client/src/components/reports/reports/CashFlowCard.tsx b/packages/desktop-client/src/components/reports/reports/CashFlowCard.tsx index e1aa13f1a9162235071184002e5bfe977af75055..3bf8fe415ea9f3ad1ab33cdba3881040500f8a33 100644 --- a/packages/desktop-client/src/components/reports/reports/CashFlowCard.tsx +++ b/packages/desktop-client/src/components/reports/reports/CashFlowCard.tsx @@ -5,9 +5,9 @@ import { Bar, BarChart, LabelList, ResponsiveContainer } from 'recharts'; import * as monthUtils from 'loot-core/src/shared/months'; import { integerToCurrency } from 'loot-core/src/shared/util'; +import { type CashFlowWidget } from 'loot-core/src/types/models'; -import { theme, styles } from '../../../style'; -import { Block } from '../../common/Block'; +import { theme } from '../../../style'; import { View } from '../../common/View'; import { PrivacyFilter } from '../../PrivacyFilter'; import { Change } from '../Change'; @@ -16,6 +16,7 @@ import { Container } from '../Container'; import { DateRange } from '../DateRange'; import { LoadingIndicator } from '../LoadingIndicator'; import { ReportCard } from '../ReportCard'; +import { ReportCardName } from '../ReportCardName'; import { simpleCashFlow } from '../spreadsheets/cash-flow-spreadsheet'; import { useReport } from '../useReport'; @@ -81,14 +82,23 @@ function CustomLabel({ type CashFlowCardProps = { isEditing?: boolean; + meta?: CashFlowWidget['meta']; + onMetaChange: (newMeta: CashFlowWidget['meta']) => void; onRemove: () => void; }; -export function CashFlowCard({ isEditing, onRemove }: CashFlowCardProps) { +export function CashFlowCard({ + isEditing, + meta, + onMetaChange, + onRemove, +}: CashFlowCardProps) { const { t } = useTranslation(); const end = monthUtils.currentDay(); const start = monthUtils.currentMonth() + '-01'; + const [nameMenuOpen, setNameMenuOpen] = useState(false); + const params = useMemo(() => simpleCashFlow(start, end), [start, end]); const data = useReport('cash_flow_simple', params); @@ -105,6 +115,10 @@ export function CashFlowCard({ isEditing, onRemove }: CashFlowCardProps) { isEditing={isEditing} to="/reports/cash-flow" menuItems={[ + { + name: 'rename', + text: t('Rename'), + }, { name: 'remove', text: t('Remove'), @@ -112,6 +126,9 @@ export function CashFlowCard({ isEditing, onRemove }: CashFlowCardProps) { ]} onMenuSelect={item => { switch (item) { + case 'rename': + setNameMenuOpen(true); + break; case 'remove': onRemove(); break; @@ -127,12 +144,18 @@ export function CashFlowCard({ isEditing, onRemove }: CashFlowCardProps) { > <View style={{ flexDirection: 'row', padding: 20 }}> <View style={{ flex: 1 }}> - <Block - style={{ ...styles.mediumText, fontWeight: 500, marginBottom: 5 }} - role="heading" - > - Cash Flow - </Block> + <ReportCardName + name={meta?.name || t('Cash Flow')} + isEditing={nameMenuOpen} + onChange={newName => { + onMetaChange({ + ...meta, + name: newName, + }); + setNameMenuOpen(false); + }} + onClose={() => setNameMenuOpen(false)} + /> <DateRange start={start} end={end} /> </View> {data && ( diff --git a/packages/desktop-client/src/components/reports/reports/CustomReportListCards.tsx b/packages/desktop-client/src/components/reports/reports/CustomReportListCards.tsx index d654d17c90e840ca47c2a6654a256f150e164ede..a083b991dc1eef7a1c3421f4707f14972db998c7 100644 --- a/packages/desktop-client/src/components/reports/reports/CustomReportListCards.tsx +++ b/packages/desktop-client/src/components/reports/reports/CustomReportListCards.tsx @@ -14,15 +14,12 @@ import { useSyncedPref } from '../../../hooks/useSyncedPref'; import { SvgExclamationSolid } from '../../../icons/v1'; import { styles } from '../../../style/index'; import { theme } from '../../../style/theme'; -import { Block } from '../../common/Block'; -import { InitialFocus } from '../../common/InitialFocus'; -import { Input } from '../../common/Input'; import { Text } from '../../common/Text'; import { Tooltip } from '../../common/Tooltip'; import { View } from '../../common/View'; -import { NON_DRAGGABLE_AREA_CLASS_NAME } from '../constants'; import { DateRange } from '../DateRange'; import { ReportCard } from '../ReportCard'; +import { ReportCardName } from '../ReportCardName'; import { GetCardData } from './GetCardData'; import { MissingReportCard } from './MissingReportCard'; @@ -146,38 +143,12 @@ function CustomReportListCardsInner({ }} > <View style={{ flex: 1 }}> - {nameMenuOpen ? ( - <InitialFocus> - <Input - className={NON_DRAGGABLE_AREA_CLASS_NAME} - defaultValue={report.name} - onEnter={e => - onSaveName((e.target as HTMLInputElement).value) - } - onBlur={e => onSaveName(e.target.value)} - onEscape={() => setNameMenuOpen(false)} - style={{ - fontSize: 15, - fontWeight: 500, - marginTop: -6, - marginBottom: -1, - marginLeft: -6, - width: Math.max(20, report.name.length) + 'ch', - }} - /> - </InitialFocus> - ) : ( - <Block - style={{ - ...styles.mediumText, - fontWeight: 500, - marginBottom: 5, - }} - role="heading" - > - {report.name} - </Block> - )} + <ReportCardName + name={report.name} + isEditing={nameMenuOpen} + onChange={onSaveName} + onClose={() => setNameMenuOpen(false)} + /> {report.isDateStatic ? ( <DateRange start={report.startDate} end={report.endDate} /> ) : ( diff --git a/packages/desktop-client/src/components/reports/reports/NetWorthCard.tsx b/packages/desktop-client/src/components/reports/reports/NetWorthCard.tsx index 877312728e7053d2eb4d59e6f752db1dafe2e91c..5ba5ff082e1fcc1ebfa7982cf94df80a3358b3f8 100644 --- a/packages/desktop-client/src/components/reports/reports/NetWorthCard.tsx +++ b/packages/desktop-client/src/components/reports/reports/NetWorthCard.tsx @@ -3,7 +3,10 @@ import { useTranslation } from 'react-i18next'; import * as monthUtils from 'loot-core/src/shared/months'; import { integerToCurrency } from 'loot-core/src/shared/util'; -import { type AccountEntity } from 'loot-core/src/types/models'; +import { + type AccountEntity, + type NetWorthWidget, +} from 'loot-core/src/types/models'; import { useResponsive } from '../../../ResponsiveProvider'; import { styles } from '../../../style'; @@ -15,23 +18,30 @@ import { DateRange } from '../DateRange'; import { NetWorthGraph } from '../graphs/NetWorthGraph'; import { LoadingIndicator } from '../LoadingIndicator'; import { ReportCard } from '../ReportCard'; +import { ReportCardName } from '../ReportCardName'; import { createSpreadsheet as netWorthSpreadsheet } from '../spreadsheets/net-worth-spreadsheet'; import { useReport } from '../useReport'; type NetWorthCardProps = { isEditing?: boolean; accounts: AccountEntity[]; + meta?: NetWorthWidget['meta']; + onMetaChange: (newMeta: NetWorthWidget['meta']) => void; onRemove: () => void; }; export function NetWorthCard({ isEditing, accounts, + meta = {}, + onMetaChange, onRemove, }: NetWorthCardProps) { const { t } = useTranslation(); const { isNarrowWidth } = useResponsive(); + const [nameMenuOpen, setNameMenuOpen] = useState(false); + const end = monthUtils.currentMonth(); const start = monthUtils.subMonths(end, 5); const [isCardHovered, setIsCardHovered] = useState(false); @@ -49,6 +59,10 @@ export function NetWorthCard({ isEditing={isEditing} to="/reports/net-worth" menuItems={[ + { + name: 'rename', + text: t('Rename'), + }, { name: 'remove', text: t('Remove'), @@ -56,6 +70,9 @@ export function NetWorthCard({ ]} onMenuSelect={item => { switch (item) { + case 'rename': + setNameMenuOpen(true); + break; case 'remove': onRemove(); break; @@ -71,12 +88,18 @@ export function NetWorthCard({ > <View style={{ flexDirection: 'row', padding: 20 }}> <View style={{ flex: 1 }}> - <Block - style={{ ...styles.mediumText, fontWeight: 500, marginBottom: 5 }} - role="heading" - > - Net Worth - </Block> + <ReportCardName + name={meta?.name || t('Net Worth')} + isEditing={nameMenuOpen} + onChange={newName => { + onMetaChange({ + ...meta, + name: newName, + }); + setNameMenuOpen(false); + }} + onClose={() => setNameMenuOpen(false)} + /> <DateRange start={start} end={end} /> </View> {data && ( diff --git a/packages/desktop-client/src/components/reports/reports/SpendingCard.tsx b/packages/desktop-client/src/components/reports/reports/SpendingCard.tsx index 23c1e8a33fb37b28047f83fed8157c9bcb4728c3..3e99ac1edf93f3de571e825bd3b792a798945a22 100644 --- a/packages/desktop-client/src/components/reports/reports/SpendingCard.tsx +++ b/packages/desktop-client/src/components/reports/reports/SpendingCard.tsx @@ -3,6 +3,7 @@ import { Trans, useTranslation } from 'react-i18next'; import * as monthUtils from 'loot-core/src/shared/months'; 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'; @@ -15,6 +16,7 @@ import { DateRange } from '../DateRange'; import { SpendingGraph } from '../graphs/SpendingGraph'; import { LoadingIndicator } from '../LoadingIndicator'; import { ReportCard } from '../ReportCard'; +import { ReportCardName } from '../ReportCardName'; import { createSpendingSpreadsheet } from '../spreadsheets/spending-spreadsheet'; import { useReport } from '../useReport'; @@ -22,10 +24,17 @@ import { MissingReportCard } from './MissingReportCard'; type SpendingCardProps = { isEditing?: boolean; + meta?: SpendingWidget['meta']; + onMetaChange: (newMeta: SpendingWidget['meta']) => void; onRemove: () => void; }; -export function SpendingCard({ isEditing, onRemove }: SpendingCardProps) { +export function SpendingCard({ + isEditing, + meta, + onMetaChange, + onRemove, +}: SpendingCardProps) { const { t } = useTranslation(); const [isCardHovered, setIsCardHovered] = useState(false); @@ -35,6 +44,8 @@ export function SpendingCard({ isEditing, onRemove }: SpendingCardProps) { 'spendingReportCompare', ); + const [nameMenuOpen, setNameMenuOpen] = useState(false); + const parseFilter = spendingReportFilter && JSON.parse(spendingReportFilter); const getGraphData = useMemo(() => { return createSpendingSpreadsheet({ @@ -73,6 +84,10 @@ export function SpendingCard({ isEditing, onRemove }: SpendingCardProps) { isEditing={isEditing} to="/reports/spending" menuItems={[ + { + name: 'rename', + text: t('Rename'), + }, { name: 'remove', text: t('Remove'), @@ -80,6 +95,9 @@ export function SpendingCard({ isEditing, onRemove }: SpendingCardProps) { ]} onMenuSelect={item => { switch (item) { + case 'rename': + setNameMenuOpen(true); + break; case 'remove': onRemove(); break; @@ -95,12 +113,18 @@ export function SpendingCard({ isEditing, onRemove }: SpendingCardProps) { > <View style={{ flexDirection: 'row', padding: 20 }}> <View style={{ flex: 1 }}> - <Block - style={{ ...styles.mediumText, fontWeight: 500, marginBottom: 5 }} - role="heading" - > - Monthly Spending - </Block> + <ReportCardName + name={meta?.name || t('Monthly Spending')} + isEditing={nameMenuOpen} + onChange={newName => { + onMetaChange({ + ...meta, + name: newName, + }); + setNameMenuOpen(false); + }} + onClose={() => setNameMenuOpen(false)} + /> <DateRange start={monthUtils.addMonths(monthUtils.currentMonth(), 1)} end={monthUtils.addMonths(monthUtils.currentMonth(), 1)} diff --git a/packages/loot-core/src/server/dashboard/app.ts b/packages/loot-core/src/server/dashboard/app.ts index db6267d10655526970c87c663b5ff8fee8bac4fb..edb825eee7ba180f49e700be6f99cb30b84fc91b 100644 --- a/packages/loot-core/src/server/dashboard/app.ts +++ b/packages/loot-core/src/server/dashboard/app.ts @@ -117,7 +117,7 @@ async function updateDashboard( async function updateDashboardWidget( widget: EverythingButIdOptional<Omit<Widget, 'tombstone'>>, ) { - await db.update('dashboard', widget); + await db.updateWithSchema('dashboard', widget); } async function resetDashboard() { diff --git a/packages/loot-core/src/types/models/dashboard.d.ts b/packages/loot-core/src/types/models/dashboard.d.ts index 87e809a127a8e7f40f12ef6944d0686aba095aff..2d0547326715511895e6f1d1444ec6d8a3747cf0 100644 --- a/packages/loot-core/src/types/models/dashboard.d.ts +++ b/packages/loot-core/src/types/models/dashboard.d.ts @@ -14,9 +14,18 @@ type AbstractWidget< tombstone: boolean; }; -type NetWorthWidget = AbstractWidget<'net-worth-card'>; -type CashFlowWidget = AbstractWidget<'cash-flow-card'>; -type SpendingWidget = AbstractWidget<'spending-card'>; +export type NetWorthWidget = AbstractWidget< + 'net-worth-card', + { name?: string } | null +>; +export type CashFlowWidget = AbstractWidget< + 'cash-flow-card', + { name?: string } | null +>; +export type SpendingWidget = AbstractWidget< + 'spending-card', + { name?: string } | null +>; export type CustomReportWidget = AbstractWidget< 'custom-report', { id: string } diff --git a/upcoming-release-notes/3284.md b/upcoming-release-notes/3284.md new file mode 100644 index 0000000000000000000000000000000000000000..c6738ce3165acf5377215448393bea8565693b0d --- /dev/null +++ b/upcoming-release-notes/3284.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [Matissjanis] +--- + +Dashboards: ability to rename all the widgets.