diff --git a/packages/loot-core/src/server/accounts/transactions.ts b/packages/loot-core/src/server/accounts/transactions.ts index 30f4ea94ae9..4831d624770 100644 --- a/packages/loot-core/src/server/accounts/transactions.ts +++ b/packages/loot-core/src/server/accounts/transactions.ts @@ -34,6 +34,17 @@ async function getTransactionsByIds( ); } +type BatchedTransaction = { + id: string; + account?: string; + category?: string; + payee?: string; + schedule?: string; +} & Omit< + TransactionEntity, + 'id' | 'account' | 'category' | 'payee' | 'schedule' +>; + export async function batchUpdateTransactions({ added, deleted, @@ -42,14 +53,9 @@ export async function batchUpdateTransactions({ detectOrphanPayees = true, runTransfers = true, }: { - added?: Array<{ id: string; payee: unknown; category: unknown }>; - deleted?: Array<{ id: string; payee: unknown }>; - updated?: Array<{ - id: string; - payee?: unknown; - account?: unknown; - category?: unknown; - }>; + added?: BatchedTransaction[]; + deleted?: BatchedTransaction[]; + updated?: BatchedTransaction[]; learnCategories?: boolean; detectOrphanPayees?: boolean; runTransfers?: boolean; @@ -80,6 +86,7 @@ export async function batchUpdateTransactions({ // and makes bulk updates much faster await batchMessages(async () => { if (added) { + added = assignChildSortOrders(added); addedIds = await Promise.all( added.map(async t => db.insertTransaction(t)), ); @@ -184,3 +191,19 @@ export async function batchUpdateTransactions({ updated: runTransfers ? transfersUpdated : resultUpdated, }; } + +function assignChildSortOrders(transactions: BatchedTransaction[]) { + const childSortOrderPerParent = Object.fromEntries( + transactions.map(t => [t.parent_id, 0]), + ); + + function nextChildSortOrder(parentId) { + return ++childSortOrderPerParent[parentId]; + } + + // Set relative sort order of children + return transactions.map(t => ({ + ...t, + sort_order: t.parent_id ? -nextChildSortOrder(t.parent_id) : undefined, + })); +} diff --git a/packages/loot-core/src/server/db/index.ts b/packages/loot-core/src/server/db/index.ts index f5dac6914db..d5dc8d4c192 100644 --- a/packages/loot-core/src/server/db/index.ts +++ b/packages/loot-core/src/server/db/index.ts @@ -28,7 +28,11 @@ import { } from '../models'; import { sendMessages, batchMessages } from '../sync'; -import { shoveSortOrders, SORT_INCREMENT } from './sort'; +import { + shoveSortOrders, + SORT_INCREMENT, + TRANSACTION_SORT_INCREMENT, +} from './sort'; export { toDateRepr, fromDateRepr } from '../models'; @@ -628,15 +632,22 @@ export async function getTransactions(accountId) { export async function insertTransaction(transaction) { const lastTransaction = await first( - `SELECT sort_order FROM v_transactions WHERE date = ? ORDER BY sort_order DESC LIMIT 1`, + 'SELECT sort_order FROM v_transactions ' + + 'WHERE date = ? AND is_child <> 1 AND parent_id IS NULL ' + + 'ORDER BY sort_order DESC LIMIT 1', [transaction.date.replace(/-/g, '')], // Remove hyphens ); - const sort_order = - (lastTransaction ? lastTransaction.sort_order : 0) + SORT_INCREMENT; + + // Child transactions have relative sort orders. + const isChild = transaction.is_child || transaction.parent_id; + const sortIncrement = + TRANSACTION_SORT_INCREMENT + + (isChild && transaction.sort_order ? transaction.sort_order : 0); transaction = { ...transaction, - sort_order: sort_order, + sort_order: + (lastTransaction ? lastTransaction.sort_order : 0) + sortIncrement, }; return insertWithSchema('transactions', transaction); @@ -653,11 +664,33 @@ export async function moveTransaction(id, accountId, targetId) { [accountId, id], ); - const { updates, sort_order } = shoveSortOrders(transactions, targetId); + const { updates, sort_order } = shoveSortOrders( + transactions, + targetId, + TRANSACTION_SORT_INCREMENT, + ); + for (let info of updates) { await update('transactions', info); + moveSubtransactions(info.id, info.sort_order); } await update('transactions', { id, sort_order }); + moveSubtransactions(id, sort_order); +} + +async function moveSubtransactions(parentId, parentSortOrder) { + const subtransactions = await all( + 'SELECT id FROM v_transactions WHERE parent_id = ? ORDER BY sort_order DESC', + [parentId], + ); + + for (let [index, sub] of subtransactions.entries()) { + const newIndex = index + 1; + await update('transactions', { + id: sub.id, + sort_order: parentSortOrder - newIndex, + }); + } } export async function deleteTransaction(transaction) { diff --git a/packages/loot-core/src/server/db/sort.ts b/packages/loot-core/src/server/db/sort.ts index c033c1f17c2..dc1e764ff12 100644 --- a/packages/loot-core/src/server/db/sort.ts +++ b/packages/loot-core/src/server/db/sort.ts @@ -1,4 +1,5 @@ export const SORT_INCREMENT = 16384; +export const TRANSACTION_SORT_INCREMENT = 1024; function midpoint(items, to) { const below = items[to - 1]; @@ -13,7 +14,11 @@ function midpoint(items, to) { } } -export function shoveSortOrders(items, targetId?: string) { +export function shoveSortOrders( + items, + targetId?: string, + sortIncrement = SORT_INCREMENT, +) { const to = items.findIndex(item => item.id === targetId); const target = items[to]; const before = items[to - 1]; @@ -24,17 +29,17 @@ export function shoveSortOrders(items, targetId?: string) { let order; if (items.length > 0) { // Add a new increment to whatever is the latest sort order - order = items[items.length - 1].sort_order + SORT_INCREMENT; + order = items[items.length - 1].sort_order + sortIncrement; } else { // If no items exist, the default is to use the first increment - order = SORT_INCREMENT; + order = sortIncrement; } return { updates, sort_order: order }; } else { if (target.sort_order - (before ? before.sort_order : 0) <= 2) { let next = to; - let order = Math.floor(items[next].sort_order) + SORT_INCREMENT; + let order = Math.floor(items[next].sort_order) + sortIncrement; while (next < items.length) { // No need to update it if it's already greater than the current // order. This can happen because there may already be large @@ -46,7 +51,7 @@ export function shoveSortOrders(items, targetId?: string) { updates.push({ id: items[next].id, sort_order: order }); next++; - order += SORT_INCREMENT; + order += sortIncrement; } }