Skip to content
Snippets Groups Projects
Unverified Commit b347f03f authored by Matiss Janis Aboltins's avatar Matiss Janis Aboltins Committed by GitHub
Browse files

:sparkles: (dashboards) ability to rename all widgets (#3284)

parent f3660c16
No related branches found
No related tags found
No related merge requests found
...@@ -46,10 +46,9 @@ function isCustomReportWidget(widget: Widget): widget is CustomReportWidget { ...@@ -46,10 +46,9 @@ function isCustomReportWidget(widget: Widget): widget is CustomReportWidget {
return widget.type === 'custom-report'; return widget.type === 'custom-report';
} }
function useWidgetLayout(widgets: Widget[]): (Layout & { type LayoutWidget = Layout & Pick<Widget, 'type' | 'meta'>;
type: Widget['type'];
meta: Widget['meta']; function useWidgetLayout(widgets: Widget[]): LayoutWidget[] {
})[] {
return widgets.map(widget => ({ return widgets.map(widget => ({
i: widget.id, i: widget.id,
type: widget.type, type: widget.type,
...@@ -290,6 +289,16 @@ export function Overview() { ...@@ -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(); const accounts = useAccounts();
if (isLoading) { if (isLoading) {
...@@ -494,16 +503,22 @@ export function Overview() { ...@@ -494,16 +503,22 @@ export function Overview() {
<NetWorthCard <NetWorthCard
isEditing={isEditing} isEditing={isEditing}
accounts={accounts} accounts={accounts}
meta={item.meta && 'name' in item.meta ? item.meta : {}}
onMetaChange={newMeta => onMetaChange(item, newMeta)}
onRemove={() => onRemoveWidget(item.i)} onRemove={() => onRemoveWidget(item.i)}
/> />
) : item.type === 'cash-flow-card' ? ( ) : item.type === 'cash-flow-card' ? (
<CashFlowCard <CashFlowCard
isEditing={isEditing} isEditing={isEditing}
meta={item.meta && 'name' in item.meta ? item.meta : {}}
onMetaChange={newMeta => onMetaChange(item, newMeta)}
onRemove={() => onRemoveWidget(item.i)} onRemove={() => onRemoveWidget(item.i)}
/> />
) : item.type === 'spending-card' ? ( ) : item.type === 'spending-card' ? (
<SpendingCard <SpendingCard
isEditing={isEditing} isEditing={isEditing}
meta={item.meta && 'name' in item.meta ? item.meta : {}}
onMetaChange={newMeta => onMetaChange(item, newMeta)}
onRemove={() => onRemoveWidget(item.i)} onRemove={() => onRemoveWidget(item.i)}
/> />
) : item.type === 'custom-report' ? ( ) : item.type === 'custom-report' ? (
......
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>
);
};
...@@ -5,9 +5,9 @@ import { Bar, BarChart, LabelList, ResponsiveContainer } from 'recharts'; ...@@ -5,9 +5,9 @@ import { Bar, BarChart, LabelList, ResponsiveContainer } from 'recharts';
import * as monthUtils from 'loot-core/src/shared/months'; import * as monthUtils from 'loot-core/src/shared/months';
import { integerToCurrency } from 'loot-core/src/shared/util'; import { integerToCurrency } from 'loot-core/src/shared/util';
import { type CashFlowWidget } from 'loot-core/src/types/models';
import { theme, styles } from '../../../style'; import { theme } from '../../../style';
import { Block } from '../../common/Block';
import { View } from '../../common/View'; import { View } from '../../common/View';
import { PrivacyFilter } from '../../PrivacyFilter'; import { PrivacyFilter } from '../../PrivacyFilter';
import { Change } from '../Change'; import { Change } from '../Change';
...@@ -16,6 +16,7 @@ import { Container } from '../Container'; ...@@ -16,6 +16,7 @@ import { Container } from '../Container';
import { DateRange } from '../DateRange'; import { DateRange } from '../DateRange';
import { LoadingIndicator } from '../LoadingIndicator'; import { LoadingIndicator } from '../LoadingIndicator';
import { ReportCard } from '../ReportCard'; import { ReportCard } from '../ReportCard';
import { ReportCardName } from '../ReportCardName';
import { simpleCashFlow } from '../spreadsheets/cash-flow-spreadsheet'; import { simpleCashFlow } from '../spreadsheets/cash-flow-spreadsheet';
import { useReport } from '../useReport'; import { useReport } from '../useReport';
...@@ -81,14 +82,23 @@ function CustomLabel({ ...@@ -81,14 +82,23 @@ function CustomLabel({
type CashFlowCardProps = { type CashFlowCardProps = {
isEditing?: boolean; isEditing?: boolean;
meta?: CashFlowWidget['meta'];
onMetaChange: (newMeta: CashFlowWidget['meta']) => void;
onRemove: () => void; onRemove: () => void;
}; };
export function CashFlowCard({ isEditing, onRemove }: CashFlowCardProps) { export function CashFlowCard({
isEditing,
meta,
onMetaChange,
onRemove,
}: CashFlowCardProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const end = monthUtils.currentDay(); const end = monthUtils.currentDay();
const start = monthUtils.currentMonth() + '-01'; const start = monthUtils.currentMonth() + '-01';
const [nameMenuOpen, setNameMenuOpen] = useState(false);
const params = useMemo(() => simpleCashFlow(start, end), [start, end]); const params = useMemo(() => simpleCashFlow(start, end), [start, end]);
const data = useReport('cash_flow_simple', params); const data = useReport('cash_flow_simple', params);
...@@ -105,6 +115,10 @@ export function CashFlowCard({ isEditing, onRemove }: CashFlowCardProps) { ...@@ -105,6 +115,10 @@ export function CashFlowCard({ isEditing, onRemove }: CashFlowCardProps) {
isEditing={isEditing} isEditing={isEditing}
to="/reports/cash-flow" to="/reports/cash-flow"
menuItems={[ menuItems={[
{
name: 'rename',
text: t('Rename'),
},
{ {
name: 'remove', name: 'remove',
text: t('Remove'), text: t('Remove'),
...@@ -112,6 +126,9 @@ export function CashFlowCard({ isEditing, onRemove }: CashFlowCardProps) { ...@@ -112,6 +126,9 @@ export function CashFlowCard({ isEditing, onRemove }: CashFlowCardProps) {
]} ]}
onMenuSelect={item => { onMenuSelect={item => {
switch (item) { switch (item) {
case 'rename':
setNameMenuOpen(true);
break;
case 'remove': case 'remove':
onRemove(); onRemove();
break; break;
...@@ -127,12 +144,18 @@ export function CashFlowCard({ isEditing, onRemove }: CashFlowCardProps) { ...@@ -127,12 +144,18 @@ export function CashFlowCard({ isEditing, onRemove }: CashFlowCardProps) {
> >
<View style={{ flexDirection: 'row', padding: 20 }}> <View style={{ flexDirection: 'row', padding: 20 }}>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Block <ReportCardName
style={{ ...styles.mediumText, fontWeight: 500, marginBottom: 5 }} name={meta?.name || t('Cash Flow')}
role="heading" isEditing={nameMenuOpen}
> onChange={newName => {
Cash Flow onMetaChange({
</Block> ...meta,
name: newName,
});
setNameMenuOpen(false);
}}
onClose={() => setNameMenuOpen(false)}
/>
<DateRange start={start} end={end} /> <DateRange start={start} end={end} />
</View> </View>
{data && ( {data && (
......
...@@ -14,15 +14,12 @@ import { useSyncedPref } from '../../../hooks/useSyncedPref'; ...@@ -14,15 +14,12 @@ import { useSyncedPref } from '../../../hooks/useSyncedPref';
import { SvgExclamationSolid } from '../../../icons/v1'; import { SvgExclamationSolid } from '../../../icons/v1';
import { styles } from '../../../style/index'; import { styles } from '../../../style/index';
import { theme } from '../../../style/theme'; 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 { Text } from '../../common/Text';
import { Tooltip } from '../../common/Tooltip'; import { Tooltip } from '../../common/Tooltip';
import { View } from '../../common/View'; import { View } from '../../common/View';
import { NON_DRAGGABLE_AREA_CLASS_NAME } from '../constants';
import { DateRange } from '../DateRange'; import { DateRange } from '../DateRange';
import { ReportCard } from '../ReportCard'; import { ReportCard } from '../ReportCard';
import { ReportCardName } from '../ReportCardName';
import { GetCardData } from './GetCardData'; import { GetCardData } from './GetCardData';
import { MissingReportCard } from './MissingReportCard'; import { MissingReportCard } from './MissingReportCard';
...@@ -146,38 +143,12 @@ function CustomReportListCardsInner({ ...@@ -146,38 +143,12 @@ function CustomReportListCardsInner({
}} }}
> >
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
{nameMenuOpen ? ( <ReportCardName
<InitialFocus> name={report.name}
<Input isEditing={nameMenuOpen}
className={NON_DRAGGABLE_AREA_CLASS_NAME} onChange={onSaveName}
defaultValue={report.name} onClose={() => setNameMenuOpen(false)}
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>
)}
{report.isDateStatic ? ( {report.isDateStatic ? (
<DateRange start={report.startDate} end={report.endDate} /> <DateRange start={report.startDate} end={report.endDate} />
) : ( ) : (
......
...@@ -3,7 +3,10 @@ import { useTranslation } from 'react-i18next'; ...@@ -3,7 +3,10 @@ import { useTranslation } from 'react-i18next';
import * as monthUtils from 'loot-core/src/shared/months'; import * as monthUtils from 'loot-core/src/shared/months';
import { integerToCurrency } from 'loot-core/src/shared/util'; 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 { useResponsive } from '../../../ResponsiveProvider';
import { styles } from '../../../style'; import { styles } from '../../../style';
...@@ -15,23 +18,30 @@ import { DateRange } from '../DateRange'; ...@@ -15,23 +18,30 @@ import { DateRange } from '../DateRange';
import { NetWorthGraph } from '../graphs/NetWorthGraph'; import { NetWorthGraph } from '../graphs/NetWorthGraph';
import { LoadingIndicator } from '../LoadingIndicator'; import { LoadingIndicator } from '../LoadingIndicator';
import { ReportCard } from '../ReportCard'; import { ReportCard } from '../ReportCard';
import { ReportCardName } from '../ReportCardName';
import { createSpreadsheet as netWorthSpreadsheet } from '../spreadsheets/net-worth-spreadsheet'; import { createSpreadsheet as netWorthSpreadsheet } from '../spreadsheets/net-worth-spreadsheet';
import { useReport } from '../useReport'; import { useReport } from '../useReport';
type NetWorthCardProps = { type NetWorthCardProps = {
isEditing?: boolean; isEditing?: boolean;
accounts: AccountEntity[]; accounts: AccountEntity[];
meta?: NetWorthWidget['meta'];
onMetaChange: (newMeta: NetWorthWidget['meta']) => void;
onRemove: () => void; onRemove: () => void;
}; };
export function NetWorthCard({ export function NetWorthCard({
isEditing, isEditing,
accounts, accounts,
meta = {},
onMetaChange,
onRemove, onRemove,
}: NetWorthCardProps) { }: NetWorthCardProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { isNarrowWidth } = useResponsive(); const { isNarrowWidth } = useResponsive();
const [nameMenuOpen, setNameMenuOpen] = useState(false);
const end = monthUtils.currentMonth(); const end = monthUtils.currentMonth();
const start = monthUtils.subMonths(end, 5); const start = monthUtils.subMonths(end, 5);
const [isCardHovered, setIsCardHovered] = useState(false); const [isCardHovered, setIsCardHovered] = useState(false);
...@@ -49,6 +59,10 @@ export function NetWorthCard({ ...@@ -49,6 +59,10 @@ export function NetWorthCard({
isEditing={isEditing} isEditing={isEditing}
to="/reports/net-worth" to="/reports/net-worth"
menuItems={[ menuItems={[
{
name: 'rename',
text: t('Rename'),
},
{ {
name: 'remove', name: 'remove',
text: t('Remove'), text: t('Remove'),
...@@ -56,6 +70,9 @@ export function NetWorthCard({ ...@@ -56,6 +70,9 @@ export function NetWorthCard({
]} ]}
onMenuSelect={item => { onMenuSelect={item => {
switch (item) { switch (item) {
case 'rename':
setNameMenuOpen(true);
break;
case 'remove': case 'remove':
onRemove(); onRemove();
break; break;
...@@ -71,12 +88,18 @@ export function NetWorthCard({ ...@@ -71,12 +88,18 @@ export function NetWorthCard({
> >
<View style={{ flexDirection: 'row', padding: 20 }}> <View style={{ flexDirection: 'row', padding: 20 }}>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Block <ReportCardName
style={{ ...styles.mediumText, fontWeight: 500, marginBottom: 5 }} name={meta?.name || t('Net Worth')}
role="heading" isEditing={nameMenuOpen}
> onChange={newName => {
Net Worth onMetaChange({
</Block> ...meta,
name: newName,
});
setNameMenuOpen(false);
}}
onClose={() => setNameMenuOpen(false)}
/>
<DateRange start={start} end={end} /> <DateRange start={start} end={end} />
</View> </View>
{data && ( {data && (
......
...@@ -3,6 +3,7 @@ import { Trans, useTranslation } from 'react-i18next'; ...@@ -3,6 +3,7 @@ import { Trans, useTranslation } from 'react-i18next';
import * as monthUtils from 'loot-core/src/shared/months'; import * as monthUtils from 'loot-core/src/shared/months';
import { amountToCurrency } from 'loot-core/src/shared/util'; import { amountToCurrency } from 'loot-core/src/shared/util';
import { type SpendingWidget } from 'loot-core/src/types/models';
import { useFeatureFlag } from '../../../hooks/useFeatureFlag'; import { useFeatureFlag } from '../../../hooks/useFeatureFlag';
import { useLocalPref } from '../../../hooks/useLocalPref'; import { useLocalPref } from '../../../hooks/useLocalPref';
...@@ -15,6 +16,7 @@ import { DateRange } from '../DateRange'; ...@@ -15,6 +16,7 @@ import { DateRange } from '../DateRange';
import { SpendingGraph } from '../graphs/SpendingGraph'; import { SpendingGraph } from '../graphs/SpendingGraph';
import { LoadingIndicator } from '../LoadingIndicator'; import { LoadingIndicator } from '../LoadingIndicator';
import { ReportCard } from '../ReportCard'; import { ReportCard } from '../ReportCard';
import { ReportCardName } from '../ReportCardName';
import { createSpendingSpreadsheet } from '../spreadsheets/spending-spreadsheet'; import { createSpendingSpreadsheet } from '../spreadsheets/spending-spreadsheet';
import { useReport } from '../useReport'; import { useReport } from '../useReport';
...@@ -22,10 +24,17 @@ import { MissingReportCard } from './MissingReportCard'; ...@@ -22,10 +24,17 @@ import { MissingReportCard } from './MissingReportCard';
type SpendingCardProps = { type SpendingCardProps = {
isEditing?: boolean; isEditing?: boolean;
meta?: SpendingWidget['meta'];
onMetaChange: (newMeta: SpendingWidget['meta']) => void;
onRemove: () => void; onRemove: () => void;
}; };
export function SpendingCard({ isEditing, onRemove }: SpendingCardProps) { export function SpendingCard({
isEditing,
meta,
onMetaChange,
onRemove,
}: SpendingCardProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [isCardHovered, setIsCardHovered] = useState(false); const [isCardHovered, setIsCardHovered] = useState(false);
...@@ -35,6 +44,8 @@ export function SpendingCard({ isEditing, onRemove }: SpendingCardProps) { ...@@ -35,6 +44,8 @@ export function SpendingCard({ isEditing, onRemove }: SpendingCardProps) {
'spendingReportCompare', 'spendingReportCompare',
); );
const [nameMenuOpen, setNameMenuOpen] = useState(false);
const parseFilter = spendingReportFilter && JSON.parse(spendingReportFilter); const parseFilter = spendingReportFilter && JSON.parse(spendingReportFilter);
const getGraphData = useMemo(() => { const getGraphData = useMemo(() => {
return createSpendingSpreadsheet({ return createSpendingSpreadsheet({
...@@ -73,6 +84,10 @@ export function SpendingCard({ isEditing, onRemove }: SpendingCardProps) { ...@@ -73,6 +84,10 @@ export function SpendingCard({ isEditing, onRemove }: SpendingCardProps) {
isEditing={isEditing} isEditing={isEditing}
to="/reports/spending" to="/reports/spending"
menuItems={[ menuItems={[
{
name: 'rename',
text: t('Rename'),
},
{ {
name: 'remove', name: 'remove',
text: t('Remove'), text: t('Remove'),
...@@ -80,6 +95,9 @@ export function SpendingCard({ isEditing, onRemove }: SpendingCardProps) { ...@@ -80,6 +95,9 @@ export function SpendingCard({ isEditing, onRemove }: SpendingCardProps) {
]} ]}
onMenuSelect={item => { onMenuSelect={item => {
switch (item) { switch (item) {
case 'rename':
setNameMenuOpen(true);
break;
case 'remove': case 'remove':
onRemove(); onRemove();
break; break;
...@@ -95,12 +113,18 @@ export function SpendingCard({ isEditing, onRemove }: SpendingCardProps) { ...@@ -95,12 +113,18 @@ export function SpendingCard({ isEditing, onRemove }: SpendingCardProps) {
> >
<View style={{ flexDirection: 'row', padding: 20 }}> <View style={{ flexDirection: 'row', padding: 20 }}>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Block <ReportCardName
style={{ ...styles.mediumText, fontWeight: 500, marginBottom: 5 }} name={meta?.name || t('Monthly Spending')}
role="heading" isEditing={nameMenuOpen}
> onChange={newName => {
Monthly Spending onMetaChange({
</Block> ...meta,
name: newName,
});
setNameMenuOpen(false);
}}
onClose={() => setNameMenuOpen(false)}
/>
<DateRange <DateRange
start={monthUtils.addMonths(monthUtils.currentMonth(), 1)} start={monthUtils.addMonths(monthUtils.currentMonth(), 1)}
end={monthUtils.addMonths(monthUtils.currentMonth(), 1)} end={monthUtils.addMonths(monthUtils.currentMonth(), 1)}
......
...@@ -117,7 +117,7 @@ async function updateDashboard( ...@@ -117,7 +117,7 @@ async function updateDashboard(
async function updateDashboardWidget( async function updateDashboardWidget(
widget: EverythingButIdOptional<Omit<Widget, 'tombstone'>>, widget: EverythingButIdOptional<Omit<Widget, 'tombstone'>>,
) { ) {
await db.update('dashboard', widget); await db.updateWithSchema('dashboard', widget);
} }
async function resetDashboard() { async function resetDashboard() {
......
...@@ -14,9 +14,18 @@ type AbstractWidget< ...@@ -14,9 +14,18 @@ type AbstractWidget<
tombstone: boolean; tombstone: boolean;
}; };
type NetWorthWidget = AbstractWidget<'net-worth-card'>; export type NetWorthWidget = AbstractWidget<
type CashFlowWidget = AbstractWidget<'cash-flow-card'>; 'net-worth-card',
type SpendingWidget = AbstractWidget<'spending-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< export type CustomReportWidget = AbstractWidget<
'custom-report', 'custom-report',
{ id: string } { id: string }
......
---
category: Enhancements
authors: [Matissjanis]
---
Dashboards: ability to rename all the widgets.
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