diff --git a/packages/desktop-client/src/components/Notes.tsx b/packages/desktop-client/src/components/Notes.tsx index 4e8e4b7243f7dd12f9287150b186785a94f88acf..06dd4c5ff31d63552a8a586b0321d0a00cde7de7 100644 --- a/packages/desktop-client/src/components/Notes.tsx +++ b/packages/desktop-client/src/components/Notes.tsx @@ -75,6 +75,9 @@ const markdownStyles = css({ '& td': { padding: '0.25rem 0.75rem', }, + '& h3': { + fontSize: 15, + }, }); type NotesProps = { diff --git a/packages/desktop-client/src/components/reports/Overview.tsx b/packages/desktop-client/src/components/reports/Overview.tsx index faec350ce6fa49e15b3b89c6625a03eee29f7c58..c775077666f51b99ac7cb798ff2f43c69f73d833 100644 --- a/packages/desktop-client/src/components/reports/Overview.tsx +++ b/packages/desktop-client/src/components/reports/Overview.tsx @@ -15,6 +15,7 @@ import { send } from 'loot-core/src/platform/client/fetch'; import { type CustomReportWidget, type ExportImportDashboard, + type MarkdownWidget, type Widget, } from 'loot-core/src/types/models'; @@ -35,6 +36,7 @@ import { NON_DRAGGABLE_AREA_CLASS_NAME } from './constants'; import { LoadingIndicator } from './LoadingIndicator'; import { CashFlowCard } from './reports/CashFlowCard'; import { CustomReportListCards } from './reports/CustomReportListCards'; +import { MarkdownCard } from './reports/MarkdownCard'; import { NetWorthCard } from './reports/NetWorthCard'; import { SpendingCard } from './reports/SpendingCard'; @@ -46,22 +48,6 @@ function isCustomReportWidget(widget: Widget): widget is CustomReportWidget { return widget.type === 'custom-report'; } -type LayoutWidget = Layout & Pick<Widget, 'type' | 'meta'>; - -function useWidgetLayout(widgets: Widget[]): LayoutWidget[] { - return widgets.map(widget => ({ - i: widget.id, - type: widget.type, - x: widget.x, - y: widget.y, - w: widget.width, - h: widget.height, - minW: isCustomReportWidget(widget) ? 2 : 3, - minH: isCustomReportWidget(widget) ? 1 : 2, - meta: widget.meta, - })); -} - export function Overview() { const { t } = useTranslation(); const dispatch = useDispatch(); @@ -96,7 +82,17 @@ export function Overview() { const isDashboardsFeatureEnabled = useFeatureFlag('dashboards'); const spendingReportFeatureFlag = useFeatureFlag('spendingReport'); - const baseLayout = useWidgetLayout(widgets); + const baseLayout = widgets.map(widget => ({ + i: widget.id, + w: widget.width, + h: widget.height, + minW: + isCustomReportWidget(widget) || widget.type === 'markdown-card' ? 2 : 3, + minH: + isCustomReportWidget(widget) || widget.type === 'markdown-card' ? 1 : 2, + ...widget, + })); + const layout = spendingReportFeatureFlag && !isDashboardsFeatureEnabled && @@ -308,10 +304,7 @@ export function Overview() { ); }; - const onMetaChange = <T extends LayoutWidget>( - widget: T, - newMeta: T['meta'], - ) => { + const onMetaChange = (widget: { i: string }, newMeta: Widget['meta']) => { send('dashboard-update-widget', { id: widget.i, meta: newMeta, @@ -384,7 +377,18 @@ export function Overview() { } if (isExistingCustomReport(item)) { const [, reportId] = item.split('custom-report-'); - onAddWidget('custom-report', { id: reportId }); + onAddWidget<CustomReportWidget>('custom-report', { + id: reportId, + }); + return; + } + + if (item === 'markdown-card') { + onAddWidget<MarkdownWidget>(item, { + content: t( + '### Text Widget\n\nEdit this widget to change the **markdown** content.', + ), + }); return; } @@ -407,6 +411,10 @@ export function Overview() { }, ] : []), + { + name: 'markdown-card' as const, + text: t('Text widget'), + }, { name: 'custom-report' as const, text: t('New custom report'), @@ -522,32 +530,35 @@ export function Overview() { <NetWorthCard isEditing={isEditing} accounts={accounts} - meta={item.meta && 'name' in item.meta ? item.meta : {}} + 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 : {}} + 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 : {}} + meta={item.meta} + onMetaChange={newMeta => onMetaChange(item, newMeta)} + onRemove={() => onRemoveWidget(item.i)} + /> + ) : item.type === 'markdown-card' ? ( + <MarkdownCard + isEditing={isEditing} + meta={item.meta} onMetaChange={newMeta => onMetaChange(item, newMeta)} onRemove={() => onRemoveWidget(item.i)} /> ) : item.type === 'custom-report' ? ( <CustomReportListCards isEditing={isEditing} - report={ - item.meta && 'id' in item.meta - ? customReportMap.get(item.meta.id) - : undefined - } + report={customReportMap.get(item.meta.id)} onRemove={() => onRemoveWidget(item.i)} /> ) : null} diff --git a/packages/desktop-client/src/components/reports/ReportCardName.tsx b/packages/desktop-client/src/components/reports/ReportCardName.tsx index 3eac1ece293f46a8afbc55083793be8a9d8e7504..670ac0a99874e90ba7e328ee68332a27b0b1cf87 100644 --- a/packages/desktop-client/src/components/reports/ReportCardName.tsx +++ b/packages/desktop-client/src/components/reports/ReportCardName.tsx @@ -30,8 +30,7 @@ export const ReportCardName = ({ onUpdate={onChange} onEscape={onClose} style={{ - fontSize: 15, - fontWeight: 500, + ...styles.mediumText, marginTop: -6, marginBottom: -1, marginLeft: -6, @@ -46,7 +45,6 @@ export const ReportCardName = ({ <Block style={{ ...styles.mediumText, - fontWeight: 500, marginBottom: 5, }} role="heading" diff --git a/packages/desktop-client/src/components/reports/reports/MarkdownCard.tsx b/packages/desktop-client/src/components/reports/reports/MarkdownCard.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1ec90260e821cef21c55763c90792b6336de9b4c --- /dev/null +++ b/packages/desktop-client/src/components/reports/reports/MarkdownCard.tsx @@ -0,0 +1,141 @@ +import React, { useState } from 'react'; +import { TextArea } from 'react-aria-components'; +import { useTranslation } from 'react-i18next'; +import ReactMarkdown from 'react-markdown'; + +import { css } from 'glamor'; + +import { type MarkdownWidget } from 'loot-core/src/types/models'; + +import { styles, theme } from '../../../style'; +import { Menu } from '../../common/Menu'; +import { Text } from '../../common/Text'; +import { View } from '../../common/View'; +import { NON_DRAGGABLE_AREA_CLASS_NAME } from '../constants'; +import { ReportCard } from '../ReportCard'; + +const markdownStyles = css({ + paddingRight: 20, + '& h3': styles.mediumText, +}); + +type MarkdownCardProps = { + isEditing?: boolean; + meta: MarkdownWidget['meta']; + onMetaChange: (newMeta: MarkdownWidget['meta']) => void; + onRemove: () => void; +}; + +export function MarkdownCard({ + isEditing, + meta, + onMetaChange, + onRemove, +}: MarkdownCardProps) { + const { t } = useTranslation(); + + const [isVisibleTextArea, setIsVisibleTextArea] = useState(false); + + return ( + <ReportCard + isEditing={isEditing} + menuItems={[ + { + type: Menu.label, + name: t('Text position:'), + text: '', + }, + { + name: 'text-left', + text: t('Left'), + }, + { + name: 'text-center', + text: t('Center'), + }, + { + name: 'text-right', + text: t('Right'), + }, + Menu.line, + { + name: 'edit', + text: t('Edit content'), + }, + { + name: 'remove', + text: t('Remove'), + }, + ]} + onMenuSelect={item => { + switch (item) { + case 'text-left': + onMetaChange({ + ...meta, + text_align: 'left', + }); + break; + case 'text-center': + onMetaChange({ + ...meta, + text_align: 'center', + }); + break; + case 'text-right': + onMetaChange({ + ...meta, + text_align: 'right', + }); + break; + case 'edit': + setIsVisibleTextArea(true); + break; + case 'remove': + onRemove(); + break; + default: + throw new Error(`Unrecognized selection: ${item}`); + } + }} + > + <View + style={{ + flex: 1, + paddingTop: 5, + paddingLeft: 20, + overflowY: 'auto', + height: '100%', + textAlign: meta.text_align, + }} + > + {isVisibleTextArea ? ( + <TextArea + style={{ + height: '100%', + border: 0, + marginTop: 11, + marginBottom: 11, + marginRight: 20, + color: theme.formInputText, + backgroundColor: theme.tableBackground, + }} + className={NON_DRAGGABLE_AREA_CLASS_NAME} + autoFocus + defaultValue={meta.content} + onBlur={event => { + onMetaChange({ + ...meta, + content: event.currentTarget.value, + }); + setIsVisibleTextArea(false); + }} + /> + ) : ( + <Text {...markdownStyles}> + <ReactMarkdown linkTarget="_blank">{meta.content}</ReactMarkdown> + </Text> + )} + </View> + </ReportCard> + ); +} diff --git a/packages/loot-core/src/server/dashboard/app.ts b/packages/loot-core/src/server/dashboard/app.ts index edb825eee7ba180f49e700be6f99cb30b84fc91b..2fe1093e8ff4213851fb6e37d3354bd329e32206 100644 --- a/packages/loot-core/src/server/dashboard/app.ts +++ b/packages/loot-core/src/server/dashboard/app.ts @@ -80,6 +80,7 @@ const exportModel = { 'cash-flow-card', 'spending-card', 'custom-report', + 'markdown-card', ].includes(widget.type) ) { throw new ValidationError( diff --git a/packages/loot-core/src/types/models/dashboard.d.ts b/packages/loot-core/src/types/models/dashboard.d.ts index 2d0547326715511895e6f1d1444ec6d8a3747cf0..6dc20360f4aac4ed6d2656c1e40b1477b8b248e3 100644 --- a/packages/loot-core/src/types/models/dashboard.d.ts +++ b/packages/loot-core/src/types/models/dashboard.d.ts @@ -30,8 +30,16 @@ export type CustomReportWidget = AbstractWidget< 'custom-report', { id: string } >; +export type MarkdownWidget = AbstractWidget< + 'markdown-card', + { content: string; text_align?: 'left' | 'right' | 'center' } +>; -type SpecializedWidget = NetWorthWidget | CashFlowWidget | SpendingWidget; +type SpecializedWidget = + | NetWorthWidget + | CashFlowWidget + | SpendingWidget + | MarkdownWidget; export type Widget = SpecializedWidget | CustomReportWidget; export type NewWidget = Omit<Widget, 'id' | 'tombstone'>; diff --git a/upcoming-release-notes/3288.md b/upcoming-release-notes/3288.md new file mode 100644 index 0000000000000000000000000000000000000000..5444e81af19e7b8c4860b30d86f0efd079ef2329 --- /dev/null +++ b/upcoming-release-notes/3288.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [Matissjanis] +--- + +Dashboards: text widget support.