mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-09 03:32:54 -05:00
Compare commits
4 Commits
v26.2.1
...
mobile-fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efaee98d2a | ||
|
|
0b156d1815 | ||
|
|
5e9f38ea45 | ||
|
|
b901e7a6bd |
@@ -1,4 +1,4 @@
|
||||
import { type DragItem } from 'react-aria';
|
||||
import { isTextDropItem, type DragItem } from 'react-aria';
|
||||
import { DropIndicator, GridList, useDragAndDrop } from 'react-aria-components';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -13,8 +13,11 @@ import {
|
||||
import { ExpenseCategoryListItem } from './ExpenseCategoryListItem';
|
||||
|
||||
import { moveCategory } from '@desktop-client/budget/budgetSlice';
|
||||
import { useCategories } from '@desktop-client/hooks/useCategories';
|
||||
import { useDispatch } from '@desktop-client/redux';
|
||||
|
||||
const DRAG_TYPE = 'mobile-expense-category-list/category-id';
|
||||
|
||||
type ExpenseCategoryListProps = {
|
||||
categoryGroup: CategoryGroupEntity;
|
||||
categories: CategoryEntity[];
|
||||
@@ -37,14 +40,14 @@ export function ExpenseCategoryList({
|
||||
shouldHideCategory,
|
||||
}: ExpenseCategoryListProps) {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const { reorderCategory } = useReorderCategory();
|
||||
|
||||
const { dragAndDropHooks } = useDragAndDrop({
|
||||
getItems: keys =>
|
||||
[...keys].map(
|
||||
key =>
|
||||
({
|
||||
'text/plain': key as CategoryEntity['id'],
|
||||
[DRAG_TYPE]: key as CategoryEntity['id'],
|
||||
}) as DragItem,
|
||||
),
|
||||
renderDropIndicator: target => {
|
||||
@@ -54,7 +57,7 @@ export function ExpenseCategoryList({
|
||||
className={css({
|
||||
'&[data-drop-target]': {
|
||||
height: 4,
|
||||
backgroundColor: theme.tableBorderSeparator,
|
||||
backgroundColor: theme.tableBorderHover,
|
||||
opacity: 1,
|
||||
borderRadius: 4,
|
||||
},
|
||||
@@ -62,59 +65,25 @@ export function ExpenseCategoryList({
|
||||
/>
|
||||
);
|
||||
},
|
||||
acceptedDragTypes: [DRAG_TYPE],
|
||||
getDropOperation: () => 'move',
|
||||
onInsert: async e => {
|
||||
const [id] = await Promise.all(
|
||||
e.items.filter(isTextDropItem).map(item => item.getText(DRAG_TYPE)),
|
||||
);
|
||||
reorderCategory({
|
||||
id: id as CategoryEntity['id'],
|
||||
targetId: e.target.key as CategoryEntity['id'],
|
||||
dropPosition: e.target.dropPosition,
|
||||
});
|
||||
},
|
||||
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.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.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.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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
reorderCategory({
|
||||
id: key as CategoryEntity['id'],
|
||||
targetId: e.target.key as CategoryEntity['id'],
|
||||
dropPosition: e.target.dropPosition,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -149,3 +118,76 @@ export function ExpenseCategoryList({
|
||||
</GridList>
|
||||
);
|
||||
}
|
||||
|
||||
function useReorderCategory() {
|
||||
const dispatch = useDispatch();
|
||||
const { list: categories } = useCategories();
|
||||
const reorderCategory = ({
|
||||
id,
|
||||
targetId,
|
||||
dropPosition,
|
||||
}: {
|
||||
id: CategoryEntity['id'];
|
||||
targetId: CategoryEntity['id'];
|
||||
dropPosition: 'on' | 'before' | 'after';
|
||||
}) => {
|
||||
const categoryToMove = categories.find(c => c.id === id);
|
||||
|
||||
if (!categoryToMove) {
|
||||
throw new Error(`Internal error: category with ID ${id} not found.`);
|
||||
}
|
||||
|
||||
if (!categoryToMove.group) {
|
||||
throw new Error(
|
||||
`Internal error: Failed to move category ${id} because it is not in a group.`,
|
||||
);
|
||||
}
|
||||
|
||||
const targetCategoryGroupId = categories.find(
|
||||
c => c.id === targetId,
|
||||
)?.group;
|
||||
|
||||
if (!targetCategoryGroupId) {
|
||||
throw new Error(
|
||||
`Internal error: Failed to move category ${id} because target category ${targetId} is not in a group.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (dropPosition === 'before') {
|
||||
dispatch(
|
||||
moveCategory({
|
||||
id: categoryToMove.id,
|
||||
groupId: targetCategoryGroupId,
|
||||
targetId,
|
||||
}),
|
||||
);
|
||||
} else if (dropPosition === 'after') {
|
||||
const targetCategoryIndex = categories.findIndex(c => c.id === targetId);
|
||||
|
||||
if (targetCategoryIndex === -1) {
|
||||
throw new Error(
|
||||
`Internal error: category with ID ${targetId} not found.`,
|
||||
);
|
||||
}
|
||||
|
||||
const nextToTargetCategory = categories[targetCategoryIndex + 1];
|
||||
|
||||
dispatch(
|
||||
moveCategory({
|
||||
id: categoryToMove.id,
|
||||
groupId: targetCategoryGroupId,
|
||||
// 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 {
|
||||
reorderCategory,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,8 +16,11 @@ import {
|
||||
} from './ExpenseGroupListItem';
|
||||
|
||||
import { moveCategoryGroup } from '@desktop-client/budget/budgetSlice';
|
||||
import { useCategories } from '@desktop-client/hooks/useCategories';
|
||||
import { useDispatch } from '@desktop-client/redux';
|
||||
|
||||
const DRAG_TYPE = 'mobile-expense-group-list/category-group-id';
|
||||
|
||||
type ExpenseGroupListProps = {
|
||||
categoryGroups: CategoryGroupEntity[];
|
||||
show3Columns: boolean;
|
||||
@@ -44,14 +47,14 @@ export function ExpenseGroupList({
|
||||
onToggleCollapse,
|
||||
}: ExpenseGroupListProps) {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { reorderCategoryGroup } = useReorderCategoryGroup();
|
||||
const { dragAndDropHooks } = useDragAndDrop({
|
||||
getItems: keys =>
|
||||
[...keys].map(
|
||||
key =>
|
||||
({
|
||||
'text/plain': key as CategoryEntity['id'],
|
||||
[DRAG_TYPE]: key as CategoryGroupEntity['id'],
|
||||
}) as DragItem,
|
||||
),
|
||||
renderDropIndicator: target => {
|
||||
@@ -61,7 +64,7 @@ export function ExpenseGroupList({
|
||||
className={css({
|
||||
'&[data-drop-target]': {
|
||||
height: 4,
|
||||
backgroundColor: theme.tableBorderSeparator,
|
||||
backgroundColor: theme.tableBorderHover,
|
||||
opacity: 1,
|
||||
borderRadius: 4,
|
||||
},
|
||||
@@ -70,7 +73,7 @@ export function ExpenseGroupList({
|
||||
);
|
||||
},
|
||||
renderDragPreview: items => {
|
||||
const draggedGroupId = items[0]['text/plain'];
|
||||
const draggedGroupId = items[0][DRAG_TYPE];
|
||||
const group = categoryGroups.find(c => c.id === draggedGroupId);
|
||||
if (!group) {
|
||||
throw new Error(
|
||||
@@ -92,49 +95,11 @@ export function ExpenseGroupList({
|
||||
},
|
||||
onReorder: e => {
|
||||
const [key] = e.keys;
|
||||
const groupIdToMove = key as CategoryGroupEntity['id'];
|
||||
const groupToMove = categoryGroups.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 = categoryGroups.findIndex(
|
||||
c => c.id === targetGroupId,
|
||||
);
|
||||
|
||||
if (targetGroupIndex === -1) {
|
||||
throw new Error(
|
||||
`Internal error: category group with ID ${targetGroupId} not found.`,
|
||||
);
|
||||
}
|
||||
|
||||
const nextToTargetCategory = categoryGroups[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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
reorderCategoryGroup({
|
||||
id: key as CategoryGroupEntity['id'],
|
||||
targetId: e.target.key as CategoryGroupEntity['id'],
|
||||
dropPosition: e.target.dropPosition,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -174,3 +139,60 @@ export function ExpenseGroupList({
|
||||
</GridList>
|
||||
);
|
||||
}
|
||||
|
||||
function useReorderCategoryGroup() {
|
||||
const dispatch = useDispatch();
|
||||
const { list: categoryGroups } = useCategories();
|
||||
const reorderCategoryGroup = ({
|
||||
id,
|
||||
targetId,
|
||||
dropPosition,
|
||||
}: {
|
||||
id: CategoryGroupEntity['id'];
|
||||
targetId: CategoryGroupEntity['id'];
|
||||
dropPosition: 'on' | 'before' | 'after';
|
||||
}) => {
|
||||
const groupToMove = categoryGroups.find(c => c.id === id);
|
||||
|
||||
if (!groupToMove) {
|
||||
throw new Error(
|
||||
`Internal error: category group with ID ${id} not found.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (dropPosition === 'before') {
|
||||
dispatch(
|
||||
moveCategoryGroup({
|
||||
id: groupToMove.id,
|
||||
targetId,
|
||||
}),
|
||||
);
|
||||
} else if (dropPosition === 'after') {
|
||||
const targetGroupIndex = categoryGroups.findIndex(c => c.id === targetId);
|
||||
|
||||
if (targetGroupIndex === -1) {
|
||||
throw new Error(
|
||||
`Internal error: category group with ID ${targetId} not found.`,
|
||||
);
|
||||
}
|
||||
|
||||
const nextToTargetCategory = categoryGroups[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 {
|
||||
reorderCategoryGroup,
|
||||
};
|
||||
}
|
||||
|
||||
6
upcoming-release-notes/6634.md
Normal file
6
upcoming-release-notes/6634.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
[Mobile] Fix drag and drop across category groups
|
||||
Reference in New Issue
Block a user