From 45610bae81abe9f79783e1ca5ac444bba2caa6da Mon Sep 17 00:00:00 2001 From: POGMAN Date: Wed, 23 Jul 2025 18:57:43 +0200 Subject: [PATCH] Add button to import existing tag from transactions notes (#5368) * Add button to import existing tag from transactions notes * fix ##non-tag matching * 'find' tags instead of 'import' to avoid confusion * add link to show transaction that have given tag * use same style as PayeeTableRow's button --- .../src/components/payees/PayeeTableRow.tsx | 2 + .../src/components/tags/ManageTags.tsx | 11 ++++- .../src/components/tags/TagRow.tsx | 46 ++++++++++++++++++- .../src/queries/queriesSlice.ts | 12 +++++ packages/desktop-client/src/style/tags.ts | 4 +- packages/loot-core/src/server/db/index.ts | 11 +++++ packages/loot-core/src/server/tags/app.ts | 25 ++++++++++ upcoming-release-notes/5368.md | 6 +++ 8 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 upcoming-release-notes/5368.md diff --git a/packages/desktop-client/src/components/payees/PayeeTableRow.tsx b/packages/desktop-client/src/components/payees/PayeeTableRow.tsx index 54d1295d76..60073bb99b 100644 --- a/packages/desktop-client/src/components/payees/PayeeTableRow.tsx +++ b/packages/desktop-client/src/components/payees/PayeeTableRow.tsx @@ -56,6 +56,8 @@ function RuleButton({ ruleCount, focused, onEdit, onClick }: RuleButtonProps) { border: '1px solid ' + theme.noticeBackground, color: theme.noticeTextDark, fontSize: 12, + cursor: 'pointer', + ':hover': { backgroundColor: theme.noticeBackgroundLight }, }} onEdit={onEdit} onSelect={onClick} diff --git a/packages/desktop-client/src/components/tags/ManageTags.tsx b/packages/desktop-client/src/components/tags/ManageTags.tsx index 84e868b9c2..02e557719a 100644 --- a/packages/desktop-client/src/components/tags/ManageTags.tsx +++ b/packages/desktop-client/src/components/tags/ManageTags.tsx @@ -3,6 +3,7 @@ import { Trans, useTranslation } from 'react-i18next'; import { Button } from '@actual-app/components/button'; import { SvgAdd } from '@actual-app/components/icons/v1'; +import { SvgSearchAlternate } from '@actual-app/components/icons/v2'; import { Stack } from '@actual-app/components/stack'; import { theme } from '@actual-app/components/theme'; import { View } from '@actual-app/components/view'; @@ -18,7 +19,7 @@ import { SelectedProvider, useSelected, } from '@desktop-client/hooks/useSelected'; -import { deleteAllTags } from '@desktop-client/queries/queriesSlice'; +import { deleteAllTags, findTags } from '@desktop-client/queries/queriesSlice'; import { useDispatch } from '@desktop-client/redux'; import { useTags } from '@desktop-client/style/tags'; @@ -92,6 +93,14 @@ export function ManageTags() { Add New + { dispatch( @@ -64,6 +69,23 @@ export const TagRow = memo( ); }; + const onShowActivity = () => { + const filterConditions = [ + { + field: 'notes', + op: 'hasTags', + value: `#${tag.tag}`, + type: 'string', + }, + ]; + navigate('/accounts', { + state: { + goBack: true, + filterConditions, + }, + }); + }; + return ( + {tag.tag !== '*' && ( + + + + View Transactions + + + + + )} ); }, diff --git a/packages/desktop-client/src/queries/queriesSlice.ts b/packages/desktop-client/src/queries/queriesSlice.ts index 119df06dce..b552bedc29 100644 --- a/packages/desktop-client/src/queries/queriesSlice.ts +++ b/packages/desktop-client/src/queries/queriesSlice.ts @@ -189,6 +189,10 @@ const queriesSlice = createSlice({ const tagIdx = state.tags.findIndex(tag => tag.id === action.payload.id); state.tags[tagIdx] = action.payload; }); + + builder.addCase(findTags.fulfilled, (state, action) => { + state.tags = action.payload; + }); }, }); @@ -485,6 +489,14 @@ export const updateTag = createAppAsyncThunk( }, ); +export const findTags = createAppAsyncThunk( + `${sliceName}/findTags`, + async () => { + const id = await send('tags-find'); + return id; + }, +); + // Budget actions type ApplyBudgetActionPayload = diff --git a/packages/desktop-client/src/style/tags.ts b/packages/desktop-client/src/style/tags.ts index fc1ecb41c1..204aef853d 100644 --- a/packages/desktop-client/src/style/tags.ts +++ b/packages/desktop-client/src/style/tags.ts @@ -56,8 +56,8 @@ export function useTagCSS() { theme, // fallback strategy: options color > tag color > default color > theme color (undefined) options.color ?? - tags.find(t => t.tag === tag)?.color ?? - tags.find(t => t.tag === '*')?.color, + (tags.find(t => t.tag === tag)?.color || + tags.find(t => t.tag === '*')?.color), ); return css({ diff --git a/packages/loot-core/src/server/db/index.ts b/packages/loot-core/src/server/db/index.ts index c0158c7a14..781335b139 100644 --- a/packages/loot-core/src/server/db/index.ts +++ b/packages/loot-core/src/server/db/index.ts @@ -826,3 +826,14 @@ export async function deleteTag(tag) { export function updateTag(tag) { return update('tags', tag); } + +export function findTags() { + return all<{ notes: string }>( + ` + SELECT notes + FROM transactions + WHERE notes LIKE ? + `, + ['%#%'], + ); +} diff --git a/packages/loot-core/src/server/tags/app.ts b/packages/loot-core/src/server/tags/app.ts index 36d5c3f117..de136e8100 100644 --- a/packages/loot-core/src/server/tags/app.ts +++ b/packages/loot-core/src/server/tags/app.ts @@ -11,6 +11,7 @@ export type TagsHandlers = { 'tags-delete': typeof deleteTag; 'tags-delete-all': typeof deleteAllTags; 'tags-update': typeof updateTag; + 'tags-find': typeof findTags; }; export const app = createApp(); @@ -19,6 +20,7 @@ app.method('tags-create', mutator(undoable(createTag))); app.method('tags-delete', mutator(undoable(deleteTag))); app.method('tags-delete-all', mutator(deleteAllTags)); app.method('tags-update', mutator(undoable(updateTag))); +app.method('tags-find', mutator(findTags)); async function getTags(): Promise { return await db.getTags(); @@ -56,3 +58,26 @@ async function updateTag(tag: Tag): Promise { await db.updateTag(tag); return tag; } + +async function findTags(): Promise { + const taggedNotes = await db.findTags(); + + const tags = await getTags(); + for (const { notes } of taggedNotes) { + for (const [_, tag] of notes.matchAll(/(? t.tag === tag)) { + tags.push(await createTag({ tag, color: '' })); + } + } + } + + return tags.sort(function (a, b) { + if (a.tag < b.tag) { + return -1; + } + if (a.tag > b.tag) { + return 1; + } + return 0; + }); +} diff --git a/upcoming-release-notes/5368.md b/upcoming-release-notes/5368.md new file mode 100644 index 0000000000..f0f52b2afb --- /dev/null +++ b/upcoming-release-notes/5368.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [pogman-code] +--- + +Add button to find existing tag from transactions notes