mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-11 12:43:09 -05:00
[Mobile] Drag and drop expense category groups to re-order (#4599)
* Update to GridList * VRT - minimal diff between 2 rows * Implement a hidden drag button * Revert VRT * VRT * [Mobile] Drag and drop income categories to re-order * Update drag preview * Release notes * Fix drag preview * Fix typecheck errors * Fix group header margins * Coderabbit suggestion * Fix group * Yarn lint fix
This commit is contained in:
committed by
GitHub
parent
31fe766a2b
commit
925efc4cb6
@@ -57,7 +57,7 @@ import { useSheetValue } from '../../spreadsheet/useSheetValue';
|
||||
import { MOBILE_NAV_HEIGHT } from '../MobileNavTabs';
|
||||
import { PullToRefresh } from '../PullToRefresh';
|
||||
|
||||
import { ExpenseGroup } from './ExpenseGroup';
|
||||
import { ExpenseGroupList } from './ExpenseGroupList';
|
||||
import { IncomeGroup } from './IncomeGroup';
|
||||
|
||||
export const ROW_HEIGHT = 50;
|
||||
@@ -282,26 +282,18 @@ function BudgetGroups({
|
||||
data-testid="budget-groups"
|
||||
style={{ flex: '1 0 auto', overflowY: 'auto', paddingBottom: 15 }}
|
||||
>
|
||||
{expenseGroups
|
||||
.filter(group => !group.hidden || showHiddenCategories)
|
||||
.map(group => {
|
||||
return (
|
||||
<ExpenseGroup
|
||||
// Re-render when columns are toggled.
|
||||
key={`${group.id}|${show3Columns}|${showBudgetedColumn}`}
|
||||
group={group}
|
||||
showBudgetedColumn={showBudgetedColumn}
|
||||
month={month}
|
||||
onEditGroup={onEditGroup}
|
||||
onEditCategory={onEditCategory}
|
||||
onBudgetAction={onBudgetAction}
|
||||
show3Columns={show3Columns}
|
||||
showHiddenCategories={showHiddenCategories}
|
||||
isCollapsed={isCollapsed}
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<ExpenseGroupList
|
||||
groups={expenseGroups}
|
||||
showBudgetedColumn={showBudgetedColumn}
|
||||
month={month}
|
||||
onEditGroup={onEditGroup}
|
||||
onEditCategory={onEditCategory}
|
||||
onBudgetAction={onBudgetAction}
|
||||
show3Columns={show3Columns}
|
||||
showHiddenCategories={showHiddenCategories}
|
||||
isCollapsed={isCollapsed}
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
/>
|
||||
|
||||
{incomeGroup && (
|
||||
<IncomeGroup
|
||||
|
||||
@@ -6,13 +6,17 @@ import { theme } from '@actual-app/components/theme';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { moveCategory } from 'loot-core/client/queries/queriesSlice';
|
||||
import { type CategoryEntity } from 'loot-core/types/models';
|
||||
import {
|
||||
type CategoryGroupEntity,
|
||||
type CategoryEntity,
|
||||
} from 'loot-core/types/models';
|
||||
|
||||
import { useDispatch } from '../../../redux';
|
||||
|
||||
import { ExpenseCategoryListItem } from './ExpenseCategoryListItem';
|
||||
|
||||
type ExpenseCategoryListProps = {
|
||||
group: CategoryGroupEntity;
|
||||
categories: CategoryEntity[];
|
||||
shouldHideCategory: (category: CategoryEntity) => boolean;
|
||||
month: string;
|
||||
@@ -23,6 +27,7 @@ type ExpenseCategoryListProps = {
|
||||
};
|
||||
|
||||
export function ExpenseCategoryList({
|
||||
group,
|
||||
categories,
|
||||
month,
|
||||
onEditCategory,
|
||||
@@ -115,7 +120,9 @@ export function ExpenseCategoryList({
|
||||
|
||||
return (
|
||||
<GridList
|
||||
aria-label={t('Expense categories')}
|
||||
aria-label={t('{{groupName}} expense group categories', {
|
||||
groupName: group.name,
|
||||
})}
|
||||
items={categories}
|
||||
dragAndDropHooks={dragAndDropHooks}
|
||||
dependencies={[
|
||||
|
||||
@@ -551,6 +551,7 @@ export function ExpenseCategoryListItem({
|
||||
show3Columns={show3Columns}
|
||||
/>
|
||||
<ExpenseCategoryCells
|
||||
key={`${category.id}-${show3Columns}-${showBudgetedColumn}`}
|
||||
category={category}
|
||||
month={month}
|
||||
onBudgetAction={onBudgetAction}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { type DragItem } from 'react-aria';
|
||||
import { DropIndicator, GridList, useDragAndDrop } from 'react-aria-components';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { moveCategoryGroup } from 'loot-core/client/queries/queriesSlice';
|
||||
import {
|
||||
type CategoryEntity,
|
||||
type CategoryGroupEntity,
|
||||
} from 'loot-core/types/models';
|
||||
|
||||
import { useDispatch } from '../../../redux';
|
||||
|
||||
import {
|
||||
ExpenseGroupHeader,
|
||||
ExpenseGroupListItem,
|
||||
} from './ExpenseGroupListItem';
|
||||
|
||||
type ExpenseGroupListProps = {
|
||||
groups: CategoryGroupEntity[];
|
||||
show3Columns: boolean;
|
||||
showBudgetedColumn: boolean;
|
||||
month: string;
|
||||
onEditGroup: (id: CategoryGroupEntity['id']) => void;
|
||||
onEditCategory: (id: CategoryEntity['id']) => void;
|
||||
onBudgetAction: (month: string, action: string, args: unknown) => void;
|
||||
showHiddenCategories: boolean;
|
||||
isCollapsed: (id: CategoryGroupEntity['id']) => boolean;
|
||||
onToggleCollapse: (id: CategoryGroupEntity['id']) => void;
|
||||
};
|
||||
|
||||
export function ExpenseGroupList({
|
||||
groups,
|
||||
show3Columns,
|
||||
showBudgetedColumn,
|
||||
month,
|
||||
onEditGroup,
|
||||
onEditCategory,
|
||||
onBudgetAction,
|
||||
showHiddenCategories,
|
||||
isCollapsed,
|
||||
onToggleCollapse,
|
||||
}: ExpenseGroupListProps) {
|
||||
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,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
},
|
||||
renderDragPreview: items => {
|
||||
const draggedGroupId = items[0]['text/plain'];
|
||||
const group = groups.find(c => c.id === draggedGroupId);
|
||||
if (!group) {
|
||||
throw new Error(
|
||||
`Internal error: category group with ID ${draggedGroupId} not found.`,
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ExpenseGroupHeader
|
||||
group={group}
|
||||
month={month}
|
||||
showBudgetedColumn={showBudgetedColumn}
|
||||
show3Columns={show3Columns}
|
||||
onEdit={() => {}}
|
||||
isCollapsed={() => true}
|
||||
onToggleCollapse={() => {}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
onReorder: e => {
|
||||
const [key] = e.keys;
|
||||
const groupIdToMove = key as CategoryGroupEntity['id'];
|
||||
const groupToMove = groups.find(c => c.id === groupIdToMove);
|
||||
|
||||
if (!groupToMove) {
|
||||
throw new Error(
|
||||
`Internal error: category group with ID ${groupIdToMove} not found.`,
|
||||
);
|
||||
}
|
||||
|
||||
const targetGroupId = e.target.key as CategoryEntity['id'];
|
||||
|
||||
if (e.target.dropPosition === 'before') {
|
||||
dispatch(
|
||||
moveCategoryGroup({
|
||||
id: groupToMove.id,
|
||||
targetId: targetGroupId,
|
||||
}),
|
||||
);
|
||||
} else if (e.target.dropPosition === 'after') {
|
||||
const targetGroupIndex = groups.findIndex(c => c.id === targetGroupId);
|
||||
|
||||
if (targetGroupIndex === -1) {
|
||||
throw new Error(
|
||||
`Internal error: category group with ID ${targetGroupId} not found.`,
|
||||
);
|
||||
}
|
||||
|
||||
const nextToTargetCategory = groups[targetGroupIndex + 1];
|
||||
|
||||
dispatch(
|
||||
moveCategoryGroup({
|
||||
id: groupToMove.id,
|
||||
// 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 (
|
||||
<GridList
|
||||
aria-label={t('Expense category groups')}
|
||||
items={groups}
|
||||
dependencies={[
|
||||
month,
|
||||
onEditGroup,
|
||||
onEditCategory,
|
||||
onBudgetAction,
|
||||
show3Columns,
|
||||
showBudgetedColumn,
|
||||
showHiddenCategories,
|
||||
isCollapsed,
|
||||
onToggleCollapse,
|
||||
]}
|
||||
dragAndDropHooks={dragAndDropHooks}
|
||||
>
|
||||
{group => (
|
||||
<ExpenseGroupListItem
|
||||
key={group.id}
|
||||
value={group}
|
||||
month={month}
|
||||
onEditGroup={onEditGroup}
|
||||
onEditCategory={onEditCategory}
|
||||
onBudgetAction={onBudgetAction}
|
||||
showBudgetedColumn={showBudgetedColumn}
|
||||
show3Columns={show3Columns}
|
||||
showHiddenCategories={showHiddenCategories}
|
||||
isCollapsed={isCollapsed}
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
/>
|
||||
)}
|
||||
</GridList>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { type ComponentPropsWithoutRef, useCallback, useMemo } from 'react';
|
||||
import { GridListItem } from 'react-aria-components';
|
||||
|
||||
import { Button } from '@actual-app/components/button';
|
||||
import { Card } from '@actual-app/components/card';
|
||||
@@ -23,12 +24,12 @@ import { PrivacyFilter } from '../../PrivacyFilter';
|
||||
import { CellValue } from '../../spreadsheet/CellValue';
|
||||
import { useFormat } from '../../spreadsheet/useFormat';
|
||||
|
||||
import { getColumnWidth } from './BudgetTable';
|
||||
import { getColumnWidth, ROW_HEIGHT } from './BudgetTable';
|
||||
import { ExpenseCategoryList } from './ExpenseCategoryList';
|
||||
import { ListItem } from './ListItem';
|
||||
|
||||
type ExpenseGroupProps = {
|
||||
group: CategoryGroupEntity;
|
||||
type ExpenseGroupListItemProps = ComponentPropsWithoutRef<
|
||||
typeof GridListItem<CategoryGroupEntity>
|
||||
> & {
|
||||
month: string;
|
||||
showHiddenCategories: boolean;
|
||||
onEditGroup: (id: CategoryGroupEntity['id']) => void;
|
||||
@@ -40,8 +41,7 @@ type ExpenseGroupProps = {
|
||||
show3Columns: boolean;
|
||||
};
|
||||
|
||||
export function ExpenseGroup({
|
||||
group,
|
||||
export function ExpenseGroupListItem({
|
||||
onEditGroup,
|
||||
onEditCategory,
|
||||
month,
|
||||
@@ -51,51 +51,61 @@ export function ExpenseGroup({
|
||||
showHiddenCategories,
|
||||
isCollapsed,
|
||||
onToggleCollapse,
|
||||
}: ExpenseGroupProps) {
|
||||
...props
|
||||
}: ExpenseGroupListItemProps) {
|
||||
const { value: group } = props;
|
||||
|
||||
const categories = useMemo(
|
||||
() =>
|
||||
isCollapsed(group.id)
|
||||
!group || isCollapsed(group.id)
|
||||
? []
|
||||
: (group.categories?.filter(
|
||||
category => !category.hidden || showHiddenCategories,
|
||||
) ?? []),
|
||||
[group.categories, group.id, isCollapsed, showHiddenCategories],
|
||||
[group, isCollapsed, showHiddenCategories],
|
||||
);
|
||||
|
||||
const shouldHideCategory = useCallback(
|
||||
(category: CategoryEntity) => {
|
||||
return !!(category.hidden || group.hidden);
|
||||
return !!(category.hidden || group?.hidden);
|
||||
},
|
||||
[group.hidden],
|
||||
[group?.hidden],
|
||||
);
|
||||
|
||||
return (
|
||||
<Card
|
||||
style={{
|
||||
marginTop: 4,
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
<ExpenseGroupHeader
|
||||
group={group}
|
||||
month={month}
|
||||
showBudgetedColumn={showBudgetedColumn}
|
||||
show3Columns={show3Columns}
|
||||
onEdit={onEditGroup}
|
||||
isCollapsed={isCollapsed(group.id)}
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
/>
|
||||
if (!group) {
|
||||
return null;
|
||||
}
|
||||
|
||||
<ExpenseCategoryList
|
||||
categories={categories}
|
||||
month={month}
|
||||
onEditCategory={onEditCategory}
|
||||
onBudgetAction={onBudgetAction}
|
||||
shouldHideCategory={shouldHideCategory}
|
||||
show3Columns={show3Columns}
|
||||
showBudgetedColumn={showBudgetedColumn}
|
||||
/>
|
||||
</Card>
|
||||
return (
|
||||
<GridListItem textValue={group.name} {...props}>
|
||||
<Card
|
||||
style={{
|
||||
marginTop: 4,
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
<ExpenseGroupHeader
|
||||
group={group}
|
||||
month={month}
|
||||
showBudgetedColumn={showBudgetedColumn}
|
||||
show3Columns={show3Columns}
|
||||
onEdit={onEditGroup}
|
||||
isCollapsed={isCollapsed}
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
/>
|
||||
|
||||
<ExpenseCategoryList
|
||||
group={group}
|
||||
categories={categories}
|
||||
month={month}
|
||||
onEditCategory={onEditCategory}
|
||||
onBudgetAction={onBudgetAction}
|
||||
shouldHideCategory={shouldHideCategory}
|
||||
show3Columns={show3Columns}
|
||||
showBudgetedColumn={showBudgetedColumn}
|
||||
/>
|
||||
</Card>
|
||||
</GridListItem>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -103,13 +113,13 @@ type ExpenseGroupHeaderProps = {
|
||||
group: CategoryGroupEntity;
|
||||
month: string;
|
||||
onEdit: (id: CategoryGroupEntity['id']) => void;
|
||||
isCollapsed: boolean;
|
||||
isCollapsed: (id: CategoryGroupEntity['id']) => boolean;
|
||||
onToggleCollapse: (id: CategoryGroupEntity['id']) => void;
|
||||
show3Columns: boolean;
|
||||
showBudgetedColumn: boolean;
|
||||
};
|
||||
|
||||
function ExpenseGroupHeader({
|
||||
export function ExpenseGroupHeader({
|
||||
group,
|
||||
month,
|
||||
onEdit,
|
||||
@@ -119,13 +129,17 @@ function ExpenseGroupHeader({
|
||||
onToggleCollapse,
|
||||
}: ExpenseGroupHeaderProps) {
|
||||
return (
|
||||
<ListItem
|
||||
<View
|
||||
style={{
|
||||
height: ROW_HEIGHT,
|
||||
borderBottomWidth: 1,
|
||||
borderColor: theme.tableBorder,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
opacity: !!group.hidden ? 0.5 : undefined,
|
||||
paddingLeft: 0,
|
||||
backgroundColor: monthUtils.isCurrentMonth(month)
|
||||
? theme.budgetHeaderCurrentMonth
|
||||
: theme.budgetHeaderOtherMonth,
|
||||
@@ -144,14 +158,14 @@ function ExpenseGroupHeader({
|
||||
show3Columns={show3Columns}
|
||||
showBudgetedColumn={showBudgetedColumn}
|
||||
/>
|
||||
</ListItem>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
type ExpenseGroupNameProps = {
|
||||
group: CategoryGroupEntity;
|
||||
onEdit: (id: CategoryGroupEntity['id']) => void;
|
||||
isCollapsed: boolean;
|
||||
isCollapsed: (id: CategoryGroupEntity['id']) => boolean;
|
||||
onToggleCollapse: (id: CategoryGroupEntity['id']) => void;
|
||||
show3Columns: boolean;
|
||||
};
|
||||
@@ -177,6 +191,17 @@ function ExpenseGroupName({
|
||||
width: sidebarColumnWidth,
|
||||
}}
|
||||
>
|
||||
{/* Hidden drag button */}
|
||||
<Button
|
||||
slot="drag"
|
||||
style={{
|
||||
opacity: 0,
|
||||
width: 1,
|
||||
height: 1,
|
||||
position: 'absolute',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="bare"
|
||||
className={css({
|
||||
@@ -185,6 +210,7 @@ function ExpenseGroupName({
|
||||
'&[data-pressed]': {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
marginLeft: -5,
|
||||
})}
|
||||
onPress={() => onToggleCollapse(group.id)}
|
||||
>
|
||||
@@ -194,7 +220,7 @@ function ExpenseGroupName({
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
transition: 'transform .1s',
|
||||
transform: isCollapsed ? 'rotate(-90deg)' : '',
|
||||
transform: isCollapsed(group.id) ? 'rotate(-90deg)' : '',
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
@@ -22,9 +22,8 @@ import { PrivacyFilter } from '../../PrivacyFilter';
|
||||
import { CellValue } from '../../spreadsheet/CellValue';
|
||||
import { useFormat } from '../../spreadsheet/useFormat';
|
||||
|
||||
import { getColumnWidth } from './BudgetTable';
|
||||
import { getColumnWidth, ROW_HEIGHT } from './BudgetTable';
|
||||
import { IncomeCategoryList } from './IncomeCategoryList';
|
||||
import { ListItem } from './ListItem';
|
||||
|
||||
type IncomeGroupProps = {
|
||||
group: CategoryGroupEntity;
|
||||
@@ -84,7 +83,7 @@ export function IncomeGroup({
|
||||
group={group}
|
||||
month={month}
|
||||
onEdit={onEditGroup}
|
||||
isCollapsed={isCollapsed(group.id)}
|
||||
isCollapsed={isCollapsed}
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
/>
|
||||
<IncomeCategoryList
|
||||
@@ -102,7 +101,7 @@ type IncomeGroupHeaderProps = {
|
||||
group: CategoryGroupEntity;
|
||||
month: string;
|
||||
onEdit: (id: CategoryGroupEntity['id']) => void;
|
||||
isCollapsed: boolean;
|
||||
isCollapsed: (id: CategoryGroupEntity['id']) => boolean;
|
||||
onToggleCollapse: (id: CategoryGroupEntity['id']) => void;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
@@ -116,13 +115,17 @@ function IncomeGroupHeader({
|
||||
style,
|
||||
}: IncomeGroupHeaderProps) {
|
||||
return (
|
||||
<ListItem
|
||||
<View
|
||||
style={{
|
||||
height: ROW_HEIGHT,
|
||||
borderBottomWidth: 1,
|
||||
borderColor: theme.tableBorder,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
opacity: !!group.hidden ? 0.5 : undefined,
|
||||
paddingLeft: 0,
|
||||
backgroundColor: monthUtils.isCurrentMonth(month)
|
||||
? theme.budgetHeaderCurrentMonth
|
||||
: theme.budgetHeaderOtherMonth,
|
||||
@@ -137,14 +140,14 @@ function IncomeGroupHeader({
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
/>
|
||||
<IncomeGroupCells group={group} />
|
||||
</ListItem>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
type IncomeGroupNameProps = {
|
||||
group: CategoryGroupEntity;
|
||||
onEdit: (id: CategoryGroupEntity['id']) => void;
|
||||
isCollapsed: boolean;
|
||||
isCollapsed: (id: CategoryGroupEntity['id']) => boolean;
|
||||
onToggleCollapse: (id: CategoryGroupEntity['id']) => void;
|
||||
};
|
||||
|
||||
@@ -175,6 +178,7 @@ function IncomeGroupName({
|
||||
'&[data-pressed]': {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
marginLeft: -5,
|
||||
})}
|
||||
onPress={() => onToggleCollapse(group.id)}
|
||||
>
|
||||
@@ -184,7 +188,7 @@ function IncomeGroupName({
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
transition: 'transform .1s',
|
||||
transform: isCollapsed ? 'rotate(-90deg)' : '',
|
||||
transform: isCollapsed(group.id) ? 'rotate(-90deg)' : '',
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import React, {
|
||||
type ComponentProps,
|
||||
type ReactNode,
|
||||
type CSSProperties,
|
||||
} from 'react';
|
||||
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
import { View } from '@actual-app/components/view';
|
||||
|
||||
const ROW_HEIGHT = 50;
|
||||
|
||||
type ListItemProps = ComponentProps<typeof View> & {
|
||||
children?: ReactNode;
|
||||
style: CSSProperties;
|
||||
};
|
||||
|
||||
export const ListItem = ({ children, style, ...props }: ListItemProps) => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
height: ROW_HEIGHT,
|
||||
borderBottomWidth: 1,
|
||||
borderColor: theme.tableBorder,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
zIndex: 1,
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -358,7 +358,7 @@ export const moveCategory = createAppAsyncThunk(
|
||||
|
||||
type MoveCategoryGroupPayload = {
|
||||
id: CategoryGroupEntity['id'];
|
||||
targetId: CategoryGroupEntity['id'];
|
||||
targetId: CategoryGroupEntity['id'] | null;
|
||||
};
|
||||
|
||||
export const moveCategoryGroup = createAppAsyncThunk(
|
||||
|
||||
6
upcoming-release-notes/4599.md
Normal file
6
upcoming-release-notes/4599.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
[Mobile] Drag and drop to reorder expense category groups in budget page (only supports for Chromium-based browsers for now).
|
||||
Reference in New Issue
Block a user