From 7779044f56b041c538af385ca651fe7691ee9893 Mon Sep 17 00:00:00 2001 From: Gleb <390857+GlebYavorski@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:53:05 +0300 Subject: [PATCH] fix(transaction): don't write "mixed" tag placeholder on bulk edit When bulk editing transactions with different categories, the modal puts a `mixed` placeholder in the tag list, and `modifyTags` is supposed to expand it into each transaction's own tags. For a transaction without any category `prevTags` is `null`, so the `id === 'mixed' && prevTags` check fell through to the `else` branch and stored the literal `mixed` string as a tag id. After that `usePopulatedTags()['mixed']` is `undefined` and rendering the transaction list crashes in `Symbol` (`tag.symbol` of undefined), taking the whole app down. Fixes #147 Co-Authored-By: Claude Opus 5 (1M context) --- src/5-entities/transaction/thunks.test.ts | 51 +++++++++++++++++++++++ src/5-entities/transaction/thunks.ts | 3 +- 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 src/5-entities/transaction/thunks.test.ts diff --git a/src/5-entities/transaction/thunks.test.ts b/src/5-entities/transaction/thunks.test.ts new file mode 100644 index 000000000..8605243af --- /dev/null +++ b/src/5-entities/transaction/thunks.test.ts @@ -0,0 +1,51 @@ +import type { TTagId } from '6-shared/types' +import { describe, expect, test } from 'vitest' +import { store } from 'store' +import { applyClientPatch } from 'store/data' +import { makeTransaction } from './makeTransaction' +import { getTransactionsById } from './model' +import { bulkEditTransactions } from './thunks' + +function makeTr(id: string, tag: TTagId[] | null) { + return makeTransaction({ + id, + user: 0, + date: '2024-01-01', + incomeInstrument: 2, + incomeAccount: 'acc', + outcomeInstrument: 2, + outcomeAccount: 'acc', + outcome: 100, + tag, + }) +} + +describe('bulkEditTransactions', () => { + test('adds a tag keeping own tags of every transaction', () => { + store.dispatch( + applyClientPatch({ + transaction: [makeTr('tr1', ['tag1']), makeTr('tr2', ['tag2'])], + }) + ) + store.dispatch( + bulkEditTransactions(['tr1', 'tr2'], { tags: ['mixed', 'tag3'] }) + ) + const transactions = getTransactionsById(store.getState()) + expect(transactions.tr1.tag).toEqual(['tag1', 'tag3']) + expect(transactions.tr2.tag).toEqual(['tag2', 'tag3']) + }) + + test('never writes the "mixed" placeholder to a transaction without tags', () => { + store.dispatch( + applyClientPatch({ + transaction: [makeTr('tr3', ['tag1']), makeTr('tr4', null)], + }) + ) + store.dispatch( + bulkEditTransactions(['tr3', 'tr4'], { tags: ['mixed', 'tag3'] }) + ) + const transactions = getTransactionsById(store.getState()) + expect(transactions.tr3.tag).toEqual(['tag1', 'tag3']) + expect(transactions.tr4.tag).toEqual(['tag3']) + }) +}) diff --git a/src/5-entities/transaction/thunks.ts b/src/5-entities/transaction/thunks.ts index a7d34ef1a..fedecd8a0 100644 --- a/src/5-entities/transaction/thunks.ts +++ b/src/5-entities/transaction/thunks.ts @@ -138,7 +138,8 @@ const modifyTags = (prevTags: string[] | null, newTags?: string[]) => { const addId = (id: string) => result.includes(id) || id === 'null' ? '' : result.push(id) newTags?.forEach(id => { - if (id === 'mixed' && prevTags) prevTags.forEach(addId) + // 'mixed' is a placeholder for the transaction's own tags, never a real id + if (id === 'mixed') prevTags?.forEach(addId) else addId(id) }) return result