mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-28 10:33:02 -05:00
[Mobile] Drag and drop income categories to re-order (#4482)
* [Mobile] Drag and drop income categories to re-order * Fix category group ID * Update borders * Use list item value prop * Cleanup * useCallback * Remvoe usage of useListData * Fix typecheck error * Release notes * Fix import * Update DropIndicator * Update DropIndicator * Fix drop indicator
This commit is contained in:
committed by
GitHub
parent
af068d1f89
commit
e5c2ef47ac
@@ -13,9 +13,11 @@ import {
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import { Button } from '@actual-app/components/button';
|
||||
import { SvgAdd, SvgCheveronRight } from '@actual-app/components/icons/v1';
|
||||
import { styles } from '@actual-app/components/styles';
|
||||
import { Text } from '@actual-app/components/text';
|
||||
import { TextOneLine } from '@actual-app/components/text-one-line';
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
import { View } from '@actual-app/components/view';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
@@ -29,9 +31,7 @@ import { useAccounts } from '../../../hooks/useAccounts';
|
||||
import { useFailedAccounts } from '../../../hooks/useFailedAccounts';
|
||||
import { useNavigate } from '../../../hooks/useNavigate';
|
||||
import { useSyncedPref } from '../../../hooks/useSyncedPref';
|
||||
import { SvgAdd, SvgCheveronRight } from '../../../icons/v1';
|
||||
import { useDispatch, useSelector } from '../../../redux';
|
||||
import { theme } from '../../../style';
|
||||
import { makeAmountFullStyle } from '../../budget/util';
|
||||
import { MobilePageHeader, Page } from '../../Page';
|
||||
import { type Binding, type SheetFields } from '../../spreadsheet';
|
||||
@@ -343,14 +343,14 @@ function AccountList({
|
||||
return (
|
||||
<DropIndicator
|
||||
target={target}
|
||||
style={{
|
||||
backgroundColor: theme.tableBorderSeparator,
|
||||
position: 'absolute',
|
||||
left: 2,
|
||||
right: 2,
|
||||
borderRadius: 3,
|
||||
height: 3,
|
||||
}}
|
||||
className={css({
|
||||
'&[data-drop-target]': {
|
||||
height: 4,
|
||||
backgroundColor: theme.tableBorderSeparator,
|
||||
opacity: 1,
|
||||
borderRadius: 4,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { useCallback, type ComponentPropsWithoutRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Button } from '@actual-app/components/button';
|
||||
import { type CSSProperties } from '@actual-app/components/styles';
|
||||
import { Text } from '@actual-app/components/text';
|
||||
import { View } from '@actual-app/components/view';
|
||||
import { AutoTextSize } from 'auto-text-size';
|
||||
|
||||
import { pushModal } from 'loot-core/client/modals/modalsSlice';
|
||||
import { integerToCurrency } from 'loot-core/shared/util';
|
||||
import { type CategoryEntity } from 'loot-core/types/models';
|
||||
|
||||
import { useNotes } from '../../../hooks/useNotes';
|
||||
import { useSyncedPref } from '../../../hooks/useSyncedPref';
|
||||
import { useUndo } from '../../../hooks/useUndo';
|
||||
import { useDispatch } from '../../../redux';
|
||||
import { makeAmountGrey } from '../../budget/util';
|
||||
import { PrivacyFilter } from '../../PrivacyFilter';
|
||||
import { type SheetFields } from '../../spreadsheet';
|
||||
import { CellValue } from '../../spreadsheet/CellValue';
|
||||
import { useFormat } from '../../spreadsheet/useFormat';
|
||||
|
||||
import { getColumnWidth, PILL_STYLE } from './BudgetTable';
|
||||
|
||||
type BudgetCellProps<
|
||||
SheetFieldName extends SheetFields<'envelope-budget' | 'tracking-budget'>,
|
||||
> = ComponentPropsWithoutRef<
|
||||
typeof CellValue<'envelope-budget' | 'tracking-budget', SheetFieldName>
|
||||
> & {
|
||||
category: CategoryEntity;
|
||||
style?: CSSProperties;
|
||||
month: string;
|
||||
onBudgetAction: (month: string, action: string, args: unknown) => void;
|
||||
};
|
||||
|
||||
export function BudgetCell<
|
||||
SheetFieldName extends SheetFields<'envelope-budget' | 'tracking-budget'>,
|
||||
>({
|
||||
binding,
|
||||
category,
|
||||
month,
|
||||
onBudgetAction,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
}: BudgetCellProps<SheetFieldName>) {
|
||||
const { t } = useTranslation();
|
||||
const columnWidth = getColumnWidth();
|
||||
const dispatch = useDispatch();
|
||||
const format = useFormat();
|
||||
const { showUndoNotification } = useUndo();
|
||||
const [budgetType = 'rollover'] = useSyncedPref('budgetType');
|
||||
const categoryNotes = useNotes(category.id);
|
||||
|
||||
const onOpenCategoryBudgetMenu = useCallback(() => {
|
||||
const modalBudgetType = budgetType === 'rollover' ? 'envelope' : 'tracking';
|
||||
const categoryBudgetMenuModal = `${modalBudgetType}-budget-menu` as const;
|
||||
dispatch(
|
||||
pushModal({
|
||||
modal: {
|
||||
name: categoryBudgetMenuModal,
|
||||
options: {
|
||||
categoryId: category.id,
|
||||
month,
|
||||
onUpdateBudget: amount => {
|
||||
onBudgetAction(month, 'budget-amount', {
|
||||
category: category.id,
|
||||
amount,
|
||||
});
|
||||
showUndoNotification({
|
||||
message: `${category.name} budget has been updated to ${integerToCurrency(amount)}.`,
|
||||
});
|
||||
},
|
||||
onCopyLastMonthAverage: () => {
|
||||
onBudgetAction(month, 'copy-single-last', {
|
||||
category: category.id,
|
||||
});
|
||||
showUndoNotification({
|
||||
message: `${category.name} budget has been set last to month’s budgeted amount.`,
|
||||
});
|
||||
},
|
||||
onSetMonthsAverage: numberOfMonths => {
|
||||
if (
|
||||
numberOfMonths !== 3 &&
|
||||
numberOfMonths !== 6 &&
|
||||
numberOfMonths !== 12
|
||||
) {
|
||||
return;
|
||||
}
|
||||
onBudgetAction(month, `set-single-${numberOfMonths}-avg`, {
|
||||
category: category.id,
|
||||
});
|
||||
showUndoNotification({
|
||||
message: `${category.name} budget has been set to ${numberOfMonths === 12 ? 'yearly' : `${numberOfMonths} month`} average.`,
|
||||
});
|
||||
},
|
||||
onApplyBudgetTemplate: () => {
|
||||
onBudgetAction(month, 'apply-single-category-template', {
|
||||
category: category.id,
|
||||
});
|
||||
showUndoNotification({
|
||||
message: `${category.name} budget templates have been applied.`,
|
||||
pre: categoryNotes ?? undefined,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
}, [
|
||||
budgetType,
|
||||
category.id,
|
||||
category.name,
|
||||
categoryNotes,
|
||||
dispatch,
|
||||
month,
|
||||
onBudgetAction,
|
||||
showUndoNotification,
|
||||
]);
|
||||
|
||||
return (
|
||||
<CellValue
|
||||
binding={binding}
|
||||
type="financial"
|
||||
aria-label={t('Budgeted amount for {{categoryName}} category', {
|
||||
categoryName: category.name,
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
{({ type, name, value }) =>
|
||||
children?.({
|
||||
type,
|
||||
name,
|
||||
value,
|
||||
}) || (
|
||||
<Button
|
||||
variant="bare"
|
||||
style={{
|
||||
...PILL_STYLE,
|
||||
maxWidth: columnWidth,
|
||||
...makeAmountGrey(value),
|
||||
}}
|
||||
onPress={onOpenCategoryBudgetMenu}
|
||||
aria-label={t('Open budget menu for {{categoryName}} category', {
|
||||
categoryName: category.name,
|
||||
})}
|
||||
>
|
||||
<View>
|
||||
<PrivacyFilter>
|
||||
<AutoTextSize
|
||||
key={value}
|
||||
as={Text}
|
||||
minFontSizePx={6}
|
||||
maxFontSizePx={12}
|
||||
mode="oneline"
|
||||
style={{
|
||||
maxWidth: columnWidth,
|
||||
textAlign: 'right',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{format(value, type)}
|
||||
</AutoTextSize>
|
||||
</PrivacyFilter>
|
||||
</View>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</CellValue>
|
||||
);
|
||||
}
|
||||
@@ -25,7 +25,6 @@ import { useFeatureFlag } from '../../../hooks/useFeatureFlag';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import { useLocalPref } from '../../../hooks/useLocalPref';
|
||||
import { useNavigate } from '../../../hooks/useNavigate';
|
||||
import { useNotes } from '../../../hooks/useNotes';
|
||||
import { useSyncedPref } from '../../../hooks/useSyncedPref';
|
||||
import { useUndo } from '../../../hooks/useUndo';
|
||||
import { SvgLogo } from '../../../icons/logo';
|
||||
@@ -51,15 +50,21 @@ import { useSheetValue } from '../../spreadsheet/useSheetValue';
|
||||
import { MOBILE_NAV_HEIGHT } from '../MobileNavTabs';
|
||||
import { PullToRefresh } from '../PullToRefresh';
|
||||
|
||||
import { BudgetCell } from './BudgetCell';
|
||||
import { IncomeGroup } from './IncomeGroup';
|
||||
import { ListItem } from './ListItem';
|
||||
|
||||
const PILL_STYLE = {
|
||||
export const PILL_STYLE = {
|
||||
borderRadius: 16,
|
||||
color: theme.pillText,
|
||||
backgroundColor: theme.pillBackgroundLight,
|
||||
};
|
||||
|
||||
function getColumnWidth({ show3Cols, isSidebar = false, offset = 0 } = {}) {
|
||||
export function getColumnWidth({
|
||||
show3Cols,
|
||||
isSidebar = false,
|
||||
offset = 0,
|
||||
} = {}) {
|
||||
// If show3Cols = 35vw | 20vw | 20vw | 20vw,
|
||||
// Else = 45vw | 25vw | 25vw,
|
||||
if (!isSidebar) {
|
||||
@@ -220,136 +225,6 @@ function Saved({ projected, onPress, show3Cols }) {
|
||||
);
|
||||
}
|
||||
|
||||
function BudgetCell({
|
||||
name,
|
||||
binding,
|
||||
style,
|
||||
category,
|
||||
month,
|
||||
onBudgetAction,
|
||||
children,
|
||||
...props
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const columnWidth = getColumnWidth();
|
||||
const dispatch = useDispatch();
|
||||
const format = useFormat();
|
||||
const { showUndoNotification } = useUndo();
|
||||
const [budgetType = 'rollover'] = useSyncedPref('budgetType');
|
||||
const modalBudgetType = budgetType === 'rollover' ? 'envelope' : 'tracking';
|
||||
|
||||
const categoryBudgetMenuModal = `${modalBudgetType}-budget-menu`;
|
||||
const categoryNotes = useNotes(category.id);
|
||||
|
||||
const onOpenCategoryBudgetMenu = () => {
|
||||
dispatch(
|
||||
pushModal({
|
||||
modal: {
|
||||
name: categoryBudgetMenuModal,
|
||||
options: {
|
||||
categoryId: category.id,
|
||||
month,
|
||||
onUpdateBudget: amount => {
|
||||
onBudgetAction(month, 'budget-amount', {
|
||||
category: category.id,
|
||||
amount,
|
||||
});
|
||||
showUndoNotification({
|
||||
message: `${category.name} budget has been updated to ${integerToCurrency(amount)}.`,
|
||||
});
|
||||
},
|
||||
onCopyLastMonthAverage: () => {
|
||||
onBudgetAction(month, 'copy-single-last', {
|
||||
category: category.id,
|
||||
});
|
||||
showUndoNotification({
|
||||
message: `${category.name} budget has been set last to month’s budgeted amount.`,
|
||||
});
|
||||
},
|
||||
onSetMonthsAverage: numberOfMonths => {
|
||||
if (
|
||||
numberOfMonths !== 3 &&
|
||||
numberOfMonths !== 6 &&
|
||||
numberOfMonths !== 12
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
onBudgetAction(month, `set-single-${numberOfMonths}-avg`, {
|
||||
category: category.id,
|
||||
});
|
||||
showUndoNotification({
|
||||
message: `${category.name} budget has been set to ${numberOfMonths === 12 ? 'yearly' : `${numberOfMonths} month`} average.`,
|
||||
});
|
||||
},
|
||||
onApplyBudgetTemplate: () => {
|
||||
onBudgetAction(month, 'apply-single-category-template', {
|
||||
category: category.id,
|
||||
});
|
||||
showUndoNotification({
|
||||
message: `${category.name} budget templates have been applied.`,
|
||||
pre: categoryNotes,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<CellValue
|
||||
binding={binding}
|
||||
type="financial"
|
||||
aria-label={t('Budgeted amount for {{categoryName}} category', {
|
||||
categoryName: category.name,
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
{({ type, name, value }) =>
|
||||
children?.({
|
||||
type,
|
||||
name,
|
||||
value,
|
||||
onPress: onOpenCategoryBudgetMenu,
|
||||
}) || (
|
||||
<Button
|
||||
variant="bare"
|
||||
style={{
|
||||
...PILL_STYLE,
|
||||
maxWidth: columnWidth,
|
||||
...makeAmountGrey(value),
|
||||
}}
|
||||
onPress={onOpenCategoryBudgetMenu}
|
||||
aria-label={t('Open budget menu for {{categoryName}} category', {
|
||||
categoryName: category.name,
|
||||
})}
|
||||
>
|
||||
<View>
|
||||
<PrivacyFilter>
|
||||
<AutoTextSize
|
||||
key={value}
|
||||
as={Text}
|
||||
minFontSizePx={6}
|
||||
maxFontSizePx={12}
|
||||
mode="oneline"
|
||||
style={{
|
||||
maxWidth: columnWidth,
|
||||
textAlign: 'right',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{format(value, type)}
|
||||
</AutoTextSize>
|
||||
</PrivacyFilter>
|
||||
</View>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</CellValue>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function ExpenseGroupPreview({ group, pending, style }) {
|
||||
return (
|
||||
@@ -1020,294 +895,6 @@ const ExpenseGroupHeader = memo(function ExpenseGroupHeader({
|
||||
// </Droppable>
|
||||
});
|
||||
|
||||
const IncomeGroupHeader = memo(function IncomeGroupHeader({
|
||||
group,
|
||||
budgeted,
|
||||
balance,
|
||||
onEdit,
|
||||
collapsed,
|
||||
onToggleCollapse,
|
||||
style,
|
||||
}) {
|
||||
const listItemRef = useRef();
|
||||
const format = useFormat();
|
||||
const sidebarColumnWidth = getColumnWidth({ isSidebar: true, offset: -13.5 });
|
||||
const columnWidth = getColumnWidth();
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
opacity: !!group.hidden ? 0.5 : undefined,
|
||||
paddingLeft: 0,
|
||||
...style,
|
||||
}}
|
||||
innerRef={listItemRef}
|
||||
data-testid="category-group-row"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
width: sidebarColumnWidth,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="bare"
|
||||
className={css({
|
||||
flexShrink: 0,
|
||||
color: theme.pageTextSubdued,
|
||||
'&[data-pressed]': {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
})}
|
||||
onPress={() => onToggleCollapse?.(group.id)}
|
||||
>
|
||||
<SvgExpandArrow
|
||||
width={8}
|
||||
height={8}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
transition: 'transform .1s',
|
||||
transform: collapsed ? 'rotate(-90deg)' : '',
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="bare"
|
||||
style={{
|
||||
maxWidth: sidebarColumnWidth,
|
||||
}}
|
||||
onPress={() => onEdit?.(group.id)}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
...styles.lineClamp(2),
|
||||
width: sidebarColumnWidth,
|
||||
textAlign: 'left',
|
||||
...styles.smallText,
|
||||
}}
|
||||
data-testid="category-group-name"
|
||||
>
|
||||
{group.name}
|
||||
</Text>
|
||||
<SvgCheveronRight
|
||||
style={{ flexShrink: 0, color: theme.tableTextSubdued }}
|
||||
width={14}
|
||||
height={14}
|
||||
/>
|
||||
</View>
|
||||
</Button>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
paddingRight: 5,
|
||||
}}
|
||||
>
|
||||
{budgeted && (
|
||||
<CellValue binding={budgeted} type="financial">
|
||||
{({ type, value }) => (
|
||||
<View>
|
||||
<PrivacyFilter>
|
||||
<AutoTextSize
|
||||
key={value}
|
||||
as={Text}
|
||||
minFontSizePx={6}
|
||||
maxFontSizePx={12}
|
||||
mode="oneline"
|
||||
style={{
|
||||
width: columnWidth,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-end',
|
||||
paddingLeft: 5,
|
||||
textAlign: 'right',
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
}}
|
||||
>
|
||||
{format(value, type)}
|
||||
</AutoTextSize>
|
||||
</PrivacyFilter>
|
||||
</View>
|
||||
)}
|
||||
</CellValue>
|
||||
)}
|
||||
<CellValue binding={balance} type="financial">
|
||||
{({ type, value }) => (
|
||||
<View>
|
||||
<PrivacyFilter>
|
||||
<AutoTextSize
|
||||
key={value}
|
||||
as={Text}
|
||||
minFontSizePx={6}
|
||||
maxFontSizePx={12}
|
||||
mode="oneline"
|
||||
style={{
|
||||
width: columnWidth,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-end',
|
||||
paddingLeft: 5,
|
||||
textAlign: 'right',
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
}}
|
||||
>
|
||||
{format(value, type)}
|
||||
</AutoTextSize>
|
||||
</PrivacyFilter>
|
||||
</View>
|
||||
)}
|
||||
</CellValue>
|
||||
</View>
|
||||
</ListItem>
|
||||
);
|
||||
});
|
||||
|
||||
const IncomeCategory = memo(function IncomeCategory({
|
||||
index,
|
||||
category,
|
||||
budgeted,
|
||||
balance,
|
||||
month,
|
||||
style,
|
||||
onEdit,
|
||||
onBudgetAction,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const listItemRef = useRef();
|
||||
const format = useFormat();
|
||||
const sidebarColumnWidth = getColumnWidth({ isSidebar: true, offset: -10 });
|
||||
const columnWidth = getColumnWidth();
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'transparent',
|
||||
borderBottomWidth: 0,
|
||||
borderTopWidth: index > 0 ? 1 : 0,
|
||||
opacity: !!category.hidden ? 0.5 : undefined,
|
||||
...style,
|
||||
}}
|
||||
data-testid="category-row"
|
||||
innerRef={listItemRef}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start',
|
||||
width: sidebarColumnWidth,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="bare"
|
||||
style={{
|
||||
maxWidth: sidebarColumnWidth,
|
||||
}}
|
||||
onPress={() => onEdit?.(category.id)}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
...styles.lineClamp(2),
|
||||
width: sidebarColumnWidth,
|
||||
textAlign: 'left',
|
||||
...styles.smallText,
|
||||
}}
|
||||
data-testid="category-name"
|
||||
>
|
||||
{category.name}
|
||||
</Text>
|
||||
<SvgCheveronRight
|
||||
style={{ flexShrink: 0, color: theme.tableTextSubdued }}
|
||||
width={14}
|
||||
height={14}
|
||||
/>
|
||||
</View>
|
||||
</Button>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{budgeted && (
|
||||
<View
|
||||
style={{
|
||||
width: columnWidth,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<BudgetCell
|
||||
binding={budgeted}
|
||||
type="financial"
|
||||
category={category}
|
||||
month={month}
|
||||
onBudgetAction={onBudgetAction}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<CellValue
|
||||
binding={balance}
|
||||
type="financial"
|
||||
aria-label={t('Balance for {{categoryName}} category', {
|
||||
categoryName: category.name,
|
||||
})} // Translated aria-label
|
||||
>
|
||||
{({ type, value }) => (
|
||||
<View>
|
||||
<PrivacyFilter>
|
||||
<AutoTextSize
|
||||
key={value}
|
||||
as={Text}
|
||||
minFontSizePx={6}
|
||||
maxFontSizePx={12}
|
||||
mode="oneline"
|
||||
style={{
|
||||
width: columnWidth,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-end',
|
||||
textAlign: 'right',
|
||||
fontSize: 12,
|
||||
paddingRight: 5,
|
||||
}}
|
||||
>
|
||||
{format(value, type)}
|
||||
</AutoTextSize>
|
||||
</PrivacyFilter>
|
||||
</View>
|
||||
)}
|
||||
</CellValue>
|
||||
</View>
|
||||
</ListItem>
|
||||
);
|
||||
});
|
||||
|
||||
const ExpenseGroup = memo(function ExpenseGroup({
|
||||
type,
|
||||
group,
|
||||
@@ -1456,101 +1043,6 @@ const ExpenseGroup = memo(function ExpenseGroup({
|
||||
);
|
||||
});
|
||||
|
||||
function IncomeGroup({
|
||||
type,
|
||||
group,
|
||||
month,
|
||||
onAddCategory,
|
||||
showHiddenCategories,
|
||||
editMode,
|
||||
onEditGroup,
|
||||
onEditCategory,
|
||||
onBudgetAction,
|
||||
collapsed,
|
||||
onToggleCollapse,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const columnWidth = getColumnWidth();
|
||||
return (
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
marginTop: 50,
|
||||
marginBottom: 5,
|
||||
marginRight: 15,
|
||||
}}
|
||||
>
|
||||
{type === 'report' && (
|
||||
<Label title={t('Budgeted')} style={{ width: columnWidth }} />
|
||||
)}
|
||||
<Label title={t('Received')} style={{ width: columnWidth }} />
|
||||
</View>
|
||||
|
||||
<Card style={{ marginTop: 0 }}>
|
||||
<IncomeGroupHeader
|
||||
group={group}
|
||||
budgeted={
|
||||
type === 'report' ? trackingBudget.groupBudgeted(group.id) : null
|
||||
}
|
||||
balance={
|
||||
type === 'report'
|
||||
? trackingBudget.groupSumAmount(group.id)
|
||||
: envelopeBudget.groupSumAmount(group.id)
|
||||
}
|
||||
onAddCategory={onAddCategory}
|
||||
editMode={editMode}
|
||||
onEdit={onEditGroup}
|
||||
collapsed={collapsed}
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
style={{
|
||||
backgroundColor: monthUtils.isCurrentMonth(month)
|
||||
? theme.budgetHeaderCurrentMonth
|
||||
: theme.budgetHeaderOtherMonth,
|
||||
}}
|
||||
/>
|
||||
|
||||
{group.categories
|
||||
.filter(
|
||||
category =>
|
||||
!collapsed && (!category.hidden || showHiddenCategories),
|
||||
)
|
||||
.map((category, index) => {
|
||||
return (
|
||||
<IncomeCategory
|
||||
key={category.id}
|
||||
index={index}
|
||||
category={category}
|
||||
month={month}
|
||||
type={type}
|
||||
budgeted={
|
||||
type === 'report'
|
||||
? trackingBudget.catBudgeted(category.id)
|
||||
: null
|
||||
}
|
||||
balance={
|
||||
type === 'report'
|
||||
? trackingBudget.catSumAmount(category.id)
|
||||
: envelopeBudget.catSumAmount(category.id)
|
||||
}
|
||||
style={{
|
||||
backgroundColor: monthUtils.isCurrentMonth(month)
|
||||
? theme.budgetCurrentMonth
|
||||
: theme.budgetOtherMonth,
|
||||
}}
|
||||
editMode={editMode}
|
||||
onEdit={onEditCategory}
|
||||
onBudgetAction={onBudgetAction}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Card>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function UncategorizedButton() {
|
||||
const count = useSheetValue(uncategorizedCount());
|
||||
if (count === null || count <= 0) {
|
||||
@@ -1655,18 +1147,13 @@ function BudgetGroups({
|
||||
|
||||
{incomeGroup && (
|
||||
<IncomeGroup
|
||||
type={type}
|
||||
group={incomeGroup}
|
||||
month={month}
|
||||
onAddCategory={onAddCategory}
|
||||
onSaveCategory={onSaveCategory}
|
||||
onDeleteCategory={onDeleteCategory}
|
||||
showHiddenCategories={showHiddenCategories}
|
||||
editMode={editMode}
|
||||
onEditGroup={onEditGroup}
|
||||
onEditCategory={onEditCategory}
|
||||
onBudgetAction={onBudgetAction}
|
||||
collapsed={collapsedGroupIds.includes(incomeGroup.id)}
|
||||
isCollapsed={collapsedGroupIds.includes(incomeGroup.id)}
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
import { type ComponentPropsWithoutRef, useRef } from 'react';
|
||||
import { type DragItem } from 'react-aria';
|
||||
import {
|
||||
DropIndicator,
|
||||
ListBox,
|
||||
ListBoxItem,
|
||||
useDragAndDrop,
|
||||
} from 'react-aria-components';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Button } from '@actual-app/components/button';
|
||||
import { SvgCheveronRight } from '@actual-app/components/icons/v1';
|
||||
import { styles } from '@actual-app/components/styles';
|
||||
import { Text } from '@actual-app/components/text';
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
import { View } from '@actual-app/components/view';
|
||||
import { css } from '@emotion/css';
|
||||
import { AutoTextSize } from 'auto-text-size';
|
||||
|
||||
import { envelopeBudget, trackingBudget } from 'loot-core/client/queries';
|
||||
import { moveCategory } from 'loot-core/client/queries/queriesSlice';
|
||||
import * as monthUtils from 'loot-core/shared/months';
|
||||
import { type CategoryEntity } from 'loot-core/types/models';
|
||||
|
||||
import { useSyncedPref } from '../../../hooks/useSyncedPref';
|
||||
import { useDispatch } from '../../../redux';
|
||||
import { PrivacyFilter } from '../../PrivacyFilter';
|
||||
import { CellValue } from '../../spreadsheet/CellValue';
|
||||
import { useFormat } from '../../spreadsheet/useFormat';
|
||||
|
||||
import { BudgetCell } from './BudgetCell';
|
||||
import { getColumnWidth } from './BudgetTable';
|
||||
|
||||
type IncomeCategoryListProps = {
|
||||
categories: CategoryEntity[];
|
||||
month: string;
|
||||
onEditCategory: (id: string) => void;
|
||||
onBudgetAction: (month: string, action: string, args: unknown) => void;
|
||||
};
|
||||
|
||||
export function IncomeCategoryList({
|
||||
categories,
|
||||
month,
|
||||
onEditCategory,
|
||||
onBudgetAction,
|
||||
}: IncomeCategoryListProps) {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { dragAndDropHooks } = useDragAndDrop({
|
||||
getItems: keys =>
|
||||
[...keys].map(
|
||||
key =>
|
||||
({
|
||||
'text/plain': key as CategoryEntity['id'],
|
||||
}) as DragItem,
|
||||
),
|
||||
renderDropIndicator: target => {
|
||||
return (
|
||||
<DropIndicator
|
||||
target={target}
|
||||
className={css({
|
||||
'&[data-drop-target]': {
|
||||
height: 4,
|
||||
backgroundColor: theme.tableBorderSeparator,
|
||||
opacity: 1,
|
||||
borderRadius: 4,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
},
|
||||
onReorder: e => {
|
||||
const [key] = e.keys;
|
||||
const categoryIdToMove = key as CategoryEntity['id'];
|
||||
const categoryToMove = categories.find(c => c.id === categoryIdToMove);
|
||||
|
||||
if (!categoryToMove) {
|
||||
throw new Error(
|
||||
`Internal error: category with ID ${categoryIdToMove} not found.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!categoryToMove.cat_group) {
|
||||
throw new Error(
|
||||
`Internal error: category ${categoryIdToMove} is not in a group and cannot be moved.`,
|
||||
);
|
||||
}
|
||||
|
||||
const targetCategoryId = e.target.key as CategoryEntity['id'];
|
||||
|
||||
if (e.target.dropPosition === 'before') {
|
||||
dispatch(
|
||||
moveCategory({
|
||||
id: categoryToMove.id,
|
||||
groupId: categoryToMove.cat_group,
|
||||
targetId: targetCategoryId,
|
||||
}),
|
||||
);
|
||||
} else if (e.target.dropPosition === 'after') {
|
||||
const targetCategoryIndex = categories.findIndex(
|
||||
c => c.id === targetCategoryId,
|
||||
);
|
||||
|
||||
if (targetCategoryIndex === -1) {
|
||||
throw new Error(
|
||||
`Internal error: category with ID ${targetCategoryId} not found.`,
|
||||
);
|
||||
}
|
||||
|
||||
const nextToTargetCategory = categories[targetCategoryIndex + 1];
|
||||
|
||||
dispatch(
|
||||
moveCategory({
|
||||
id: categoryToMove.id,
|
||||
groupId: categoryToMove.cat_group,
|
||||
// Due to the way `moveCategory` works, we use the category next to the
|
||||
// actual target category here because `moveCategory` always shoves the
|
||||
// category *before* the target category.
|
||||
// On the other hand, using `null` as `targetId` moves the category
|
||||
// to the end of the list.
|
||||
targetId: nextToTargetCategory?.id || null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ListBox
|
||||
aria-label={t('Income categories')}
|
||||
items={categories}
|
||||
dragAndDropHooks={dragAndDropHooks}
|
||||
>
|
||||
{category => (
|
||||
<IncomeCategoryListItem
|
||||
key={category.id}
|
||||
value={category}
|
||||
month={month}
|
||||
onEdit={onEditCategory}
|
||||
onBudgetAction={onBudgetAction}
|
||||
/>
|
||||
)}
|
||||
</ListBox>
|
||||
);
|
||||
}
|
||||
|
||||
type IncomeCategoryNameProps = {
|
||||
category: CategoryEntity;
|
||||
onEdit: (id: CategoryEntity['id']) => void;
|
||||
};
|
||||
|
||||
function IncomeCategoryName({ category, onEdit }: IncomeCategoryNameProps) {
|
||||
const sidebarColumnWidth = getColumnWidth({
|
||||
show3Cols: false,
|
||||
isSidebar: true,
|
||||
offset: -10,
|
||||
});
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start',
|
||||
width: sidebarColumnWidth,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="bare"
|
||||
style={{
|
||||
maxWidth: sidebarColumnWidth,
|
||||
}}
|
||||
onPress={() => onEdit?.(category.id)}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
...styles.lineClamp(2),
|
||||
width: sidebarColumnWidth,
|
||||
textAlign: 'left',
|
||||
...styles.smallText,
|
||||
}}
|
||||
data-testid="category-name"
|
||||
>
|
||||
{category.name}
|
||||
</Text>
|
||||
<SvgCheveronRight
|
||||
style={{ flexShrink: 0, color: theme.tableTextSubdued }}
|
||||
width={14}
|
||||
height={14}
|
||||
/>
|
||||
</View>
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
type IncomeCategoryCellsProps = {
|
||||
category: CategoryEntity;
|
||||
month: string;
|
||||
onBudgetAction: (month: string, action: string, args: unknown) => void;
|
||||
};
|
||||
|
||||
function IncomeCategoryCells({
|
||||
category,
|
||||
month,
|
||||
onBudgetAction,
|
||||
}: IncomeCategoryCellsProps) {
|
||||
const { t } = useTranslation();
|
||||
const format = useFormat();
|
||||
const columnWidth = getColumnWidth();
|
||||
const [budgetType = 'rollover'] = useSyncedPref('budgetType');
|
||||
|
||||
const budgeted =
|
||||
budgetType === 'report' ? trackingBudget.catBudgeted(category.id) : null;
|
||||
|
||||
const balance =
|
||||
budgetType === 'report'
|
||||
? trackingBudget.catSumAmount(category.id)
|
||||
: envelopeBudget.catSumAmount(category.id);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{budgeted && (
|
||||
<View
|
||||
style={{
|
||||
width: columnWidth,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<BudgetCell
|
||||
binding={budgeted}
|
||||
type="financial"
|
||||
category={category}
|
||||
month={month}
|
||||
onBudgetAction={onBudgetAction}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<CellValue<'envelope-budget' | 'tracking-budget', 'sum-amount'>
|
||||
binding={balance}
|
||||
type="financial"
|
||||
aria-label={t('Balance for {{categoryName}} category', {
|
||||
categoryName: category.name,
|
||||
})} // Translated aria-label
|
||||
>
|
||||
{({ type, value }) => (
|
||||
<View>
|
||||
<PrivacyFilter>
|
||||
<AutoTextSize
|
||||
key={value}
|
||||
as={Text}
|
||||
minFontSizePx={6}
|
||||
maxFontSizePx={12}
|
||||
mode="oneline"
|
||||
style={{
|
||||
width: columnWidth,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-end',
|
||||
textAlign: 'right',
|
||||
fontSize: 12,
|
||||
paddingRight: 5,
|
||||
}}
|
||||
>
|
||||
{format(value, type)}
|
||||
</AutoTextSize>
|
||||
</PrivacyFilter>
|
||||
</View>
|
||||
)}
|
||||
</CellValue>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
type IncomeCategoryListItemProps = ComponentPropsWithoutRef<
|
||||
typeof ListBoxItem<CategoryEntity>
|
||||
> & {
|
||||
month: string;
|
||||
onEdit: (id: CategoryEntity['id']) => void;
|
||||
onBudgetAction: (month: string, action: string, args: unknown) => void;
|
||||
};
|
||||
|
||||
function IncomeCategoryListItem({
|
||||
month,
|
||||
onEdit,
|
||||
onBudgetAction,
|
||||
...props
|
||||
}: IncomeCategoryListItemProps) {
|
||||
const listItemRef = useRef<HTMLDivElement | null>(null);
|
||||
const { value: category } = props;
|
||||
|
||||
if (!category) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ListBoxItem
|
||||
textValue={category.name}
|
||||
data-testid="category-row"
|
||||
{...props}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
height: 50,
|
||||
borderColor: theme.tableBorder,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
zIndex: 1,
|
||||
justifyContent: 'space-between',
|
||||
borderBottomWidth: 0.5,
|
||||
borderTopWidth: 0.5,
|
||||
opacity: !!category.hidden ? 0.5 : undefined,
|
||||
backgroundColor: monthUtils.isCurrentMonth(month)
|
||||
? theme.budgetCurrentMonth
|
||||
: theme.budgetOtherMonth,
|
||||
}}
|
||||
innerRef={listItemRef}
|
||||
>
|
||||
<IncomeCategoryName category={category} onEdit={onEdit} />
|
||||
<IncomeCategoryCells
|
||||
category={category}
|
||||
month={month}
|
||||
onBudgetAction={onBudgetAction}
|
||||
/>
|
||||
</View>
|
||||
</ListBoxItem>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Button } from '@actual-app/components/button';
|
||||
import { Card } from '@actual-app/components/card';
|
||||
import { Label } from '@actual-app/components/label';
|
||||
import { type CSSProperties, styles } from '@actual-app/components/styles';
|
||||
import { Text } from '@actual-app/components/text';
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
import { View } from '@actual-app/components/view';
|
||||
import { css } from '@emotion/css';
|
||||
import { AutoTextSize } from 'auto-text-size';
|
||||
|
||||
import { envelopeBudget, trackingBudget } from 'loot-core/client/queries';
|
||||
import * as monthUtils from 'loot-core/shared/months';
|
||||
import { type CategoryGroupEntity } from 'loot-core/types/models';
|
||||
|
||||
import { useSyncedPref } from '../../../hooks/useSyncedPref';
|
||||
import { SvgExpandArrow } from '../../../icons/v0';
|
||||
import { SvgCheveronRight } from '../../../icons/v1';
|
||||
import { PrivacyFilter } from '../../PrivacyFilter';
|
||||
import { CellValue } from '../../spreadsheet/CellValue';
|
||||
import { useFormat } from '../../spreadsheet/useFormat';
|
||||
|
||||
import { getColumnWidth } from './BudgetTable';
|
||||
import { IncomeCategoryList } from './IncomeCategoryList';
|
||||
import { ListItem } from './ListItem';
|
||||
|
||||
type IncomeGroupProps = {
|
||||
group: CategoryGroupEntity;
|
||||
month: string;
|
||||
showHiddenCategories: boolean;
|
||||
onEditGroup: (id: CategoryGroupEntity['id']) => void;
|
||||
onEditCategory: (id: string) => void;
|
||||
onBudgetAction: (month: string, action: string, args: unknown) => void;
|
||||
isCollapsed: boolean;
|
||||
onToggleCollapse: (id: CategoryGroupEntity['id']) => void;
|
||||
};
|
||||
|
||||
export function IncomeGroup({
|
||||
group,
|
||||
month,
|
||||
showHiddenCategories,
|
||||
onEditGroup,
|
||||
onEditCategory,
|
||||
onBudgetAction,
|
||||
isCollapsed,
|
||||
onToggleCollapse,
|
||||
}: IncomeGroupProps) {
|
||||
const { t } = useTranslation();
|
||||
const columnWidth = getColumnWidth();
|
||||
const [budgetType = 'rollover'] = useSyncedPref('budgetType');
|
||||
|
||||
const categories = useMemo(
|
||||
() =>
|
||||
isCollapsed
|
||||
? []
|
||||
: (group.categories?.filter(
|
||||
category => !category.hidden || showHiddenCategories,
|
||||
) ?? []),
|
||||
[group.categories, isCollapsed, showHiddenCategories],
|
||||
);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
marginTop: 50,
|
||||
marginBottom: 5,
|
||||
marginRight: 15,
|
||||
}}
|
||||
>
|
||||
{budgetType === 'report' && (
|
||||
<Label title={t('Budgeted')} style={{ width: columnWidth }} />
|
||||
)}
|
||||
<Label title={t('Received')} style={{ width: columnWidth }} />
|
||||
</View>
|
||||
|
||||
<Card style={{ marginTop: 0 }}>
|
||||
<IncomeGroupHeader
|
||||
group={group}
|
||||
month={month}
|
||||
onEdit={onEditGroup}
|
||||
isCollapsed={isCollapsed}
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
/>
|
||||
<IncomeCategoryList
|
||||
categories={categories}
|
||||
month={month}
|
||||
onEditCategory={onEditCategory}
|
||||
onBudgetAction={onBudgetAction}
|
||||
/>
|
||||
</Card>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
type IncomeGroupHeaderProps = {
|
||||
group: CategoryGroupEntity;
|
||||
month: string;
|
||||
onEdit: (id: CategoryGroupEntity['id']) => void;
|
||||
isCollapsed: boolean;
|
||||
onToggleCollapse: (id: CategoryGroupEntity['id']) => void;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
function IncomeGroupHeader({
|
||||
group,
|
||||
month,
|
||||
onEdit,
|
||||
isCollapsed,
|
||||
onToggleCollapse,
|
||||
style,
|
||||
}: IncomeGroupHeaderProps) {
|
||||
const listItemRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
opacity: !!group.hidden ? 0.5 : undefined,
|
||||
paddingLeft: 0,
|
||||
backgroundColor: monthUtils.isCurrentMonth(month)
|
||||
? theme.budgetHeaderCurrentMonth
|
||||
: theme.budgetHeaderOtherMonth,
|
||||
...style,
|
||||
}}
|
||||
innerRef={listItemRef}
|
||||
data-testid="category-group-row"
|
||||
>
|
||||
<IncomeGroupName
|
||||
group={group}
|
||||
onEdit={onEdit}
|
||||
isCollapsed={isCollapsed}
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
/>
|
||||
<IncomeGroupCells group={group} />
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
type IncomeGroupNameProps = {
|
||||
group: CategoryGroupEntity;
|
||||
onEdit?: (id: CategoryGroupEntity['id']) => void;
|
||||
isCollapsed: boolean;
|
||||
onToggleCollapse?: (id: CategoryGroupEntity['id']) => void;
|
||||
};
|
||||
|
||||
function IncomeGroupName({
|
||||
group,
|
||||
onEdit,
|
||||
isCollapsed,
|
||||
onToggleCollapse,
|
||||
}: IncomeGroupNameProps) {
|
||||
const sidebarColumnWidth = getColumnWidth({
|
||||
show3Cols: false,
|
||||
isSidebar: true,
|
||||
offset: -13.5,
|
||||
});
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
width: sidebarColumnWidth,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="bare"
|
||||
className={css({
|
||||
flexShrink: 0,
|
||||
color: theme.pageTextSubdued,
|
||||
'&[data-pressed]': {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
})}
|
||||
onPress={() => onToggleCollapse?.(group.id)}
|
||||
>
|
||||
<SvgExpandArrow
|
||||
width={8}
|
||||
height={8}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
transition: 'transform .1s',
|
||||
transform: isCollapsed ? 'rotate(-90deg)' : '',
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="bare"
|
||||
style={{
|
||||
maxWidth: sidebarColumnWidth,
|
||||
}}
|
||||
onPress={() => onEdit?.(group.id)}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
...styles.lineClamp(2),
|
||||
width: sidebarColumnWidth,
|
||||
textAlign: 'left',
|
||||
...styles.smallText,
|
||||
}}
|
||||
data-testid="category-group-name"
|
||||
>
|
||||
{group.name}
|
||||
</Text>
|
||||
<SvgCheveronRight
|
||||
style={{ flexShrink: 0, color: theme.tableTextSubdued }}
|
||||
width={14}
|
||||
height={14}
|
||||
/>
|
||||
</View>
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
type IncomeGroupCellsProps = {
|
||||
group: CategoryGroupEntity;
|
||||
};
|
||||
|
||||
function IncomeGroupCells({ group }: IncomeGroupCellsProps) {
|
||||
const [budgetType = 'rollover'] = useSyncedPref('budgetType');
|
||||
const format = useFormat();
|
||||
|
||||
const budgeted =
|
||||
budgetType === 'report' ? trackingBudget.groupBudgeted(group.id) : null;
|
||||
|
||||
const balance =
|
||||
budgetType === 'report'
|
||||
? trackingBudget.groupSumAmount(group.id)
|
||||
: envelopeBudget.groupSumAmount(group.id);
|
||||
|
||||
const columnWidth = getColumnWidth();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
paddingRight: 5,
|
||||
}}
|
||||
>
|
||||
{budgeted && (
|
||||
<CellValue<'envelope-budget' | 'tracking-budget', 'group-budget'>
|
||||
binding={budgeted}
|
||||
type="financial"
|
||||
>
|
||||
{({ type, value }) => (
|
||||
<View>
|
||||
<PrivacyFilter>
|
||||
<AutoTextSize
|
||||
key={value}
|
||||
as={Text}
|
||||
minFontSizePx={6}
|
||||
maxFontSizePx={12}
|
||||
mode="oneline"
|
||||
style={{
|
||||
width: columnWidth,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-end',
|
||||
paddingLeft: 5,
|
||||
textAlign: 'right',
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
}}
|
||||
>
|
||||
{format(value, type)}
|
||||
</AutoTextSize>
|
||||
</PrivacyFilter>
|
||||
</View>
|
||||
)}
|
||||
</CellValue>
|
||||
)}
|
||||
<CellValue<'envelope-budget' | 'tracking-budget', 'group-sum-amount'>
|
||||
binding={balance}
|
||||
type="financial"
|
||||
>
|
||||
{({ type, value }) => (
|
||||
<View>
|
||||
<PrivacyFilter>
|
||||
<AutoTextSize
|
||||
key={value}
|
||||
as={Text}
|
||||
minFontSizePx={6}
|
||||
maxFontSizePx={12}
|
||||
mode="oneline"
|
||||
style={{
|
||||
width: columnWidth,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-end',
|
||||
paddingLeft: 5,
|
||||
textAlign: 'right',
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
}}
|
||||
>
|
||||
{format(value, type)}
|
||||
</AutoTextSize>
|
||||
</PrivacyFilter>
|
||||
</View>
|
||||
)}
|
||||
</CellValue>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -339,7 +339,7 @@ export const deleteCategory = createAppAsyncThunk(
|
||||
type MoveCategoryPayload = {
|
||||
id: CategoryEntity['id'];
|
||||
groupId: CategoryGroupEntity['id'];
|
||||
targetId: CategoryEntity['id'];
|
||||
targetId: CategoryEntity['id'] | null;
|
||||
};
|
||||
|
||||
export const moveCategory = createAppAsyncThunk(
|
||||
|
||||
@@ -463,7 +463,7 @@ export function updateCategory(category) {
|
||||
export async function moveCategory(
|
||||
id: DbCategory['id'],
|
||||
groupId: DbCategoryGroup['id'],
|
||||
targetId?: DbCategory['id'],
|
||||
targetId: DbCategory['id'] | null,
|
||||
) {
|
||||
if (!groupId) {
|
||||
throw new Error('moveCategory: groupId is required');
|
||||
|
||||
@@ -319,7 +319,7 @@ describe('Sync projections', () => {
|
||||
groupId = await db.insertCategoryGroup({ id: 'group1', name: 'group1' });
|
||||
await db.insertCategoryGroup({ id: 'group2', name: 'group2' });
|
||||
fooId = await db.insertCategory({ name: 'foo', cat_group: 'group1' });
|
||||
await db.moveCategory(fooId, 'group2');
|
||||
await db.moveCategory(fooId, 'group2', null);
|
||||
});
|
||||
|
||||
await sheet.loadSpreadsheet(db);
|
||||
|
||||
6
upcoming-release-notes/4482.md
Normal file
6
upcoming-release-notes/4482.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
[Mobile] Drag and drop to income categories in budget page (only supports for Chromium-based browsers for now)
|
||||
Reference in New Issue
Block a user