mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-22 00:13:45 -05:00
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
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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() {
|
||||
<SvgAdd width={10} height={10} style={{ marginRight: 3 }} />
|
||||
<Trans>Add New</Trans>
|
||||
</Button>
|
||||
<Button variant="bare" onPress={() => dispatch(findTags())}>
|
||||
<SvgSearchAlternate
|
||||
width={10}
|
||||
height={10}
|
||||
style={{ marginRight: 3 }}
|
||||
/>
|
||||
<Trans>Find Existing Tags</Trans>
|
||||
</Button>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Search
|
||||
placeholder={t('Filter tags...')}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React, { memo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { Button } from '@actual-app/components/button';
|
||||
import { SvgArrowThinRight } from '@actual-app/components/icons/v1';
|
||||
import { SvgRefreshArrow } from '@actual-app/components/icons/v2';
|
||||
import { Menu } from '@actual-app/components/menu';
|
||||
import { Popover } from '@actual-app/components/popover';
|
||||
import { Text } from '@actual-app/components/text';
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
|
||||
import { type Tag } from 'loot-core/types/models';
|
||||
@@ -16,8 +18,10 @@ import {
|
||||
Row,
|
||||
Cell,
|
||||
InputCell,
|
||||
CellButton,
|
||||
} from '@desktop-client/components/table';
|
||||
import { useContextMenu } from '@desktop-client/hooks/useContextMenu';
|
||||
import { useNavigate } from '@desktop-client/hooks/useNavigate';
|
||||
import { useProperFocus } from '@desktop-client/hooks/useProperFocus';
|
||||
import { useSelectedDispatch } from '@desktop-client/hooks/useSelected';
|
||||
import {
|
||||
@@ -51,6 +55,7 @@ export const TagRow = memo(
|
||||
const triggerRef = useRef(null);
|
||||
const { setMenuOpen, menuOpen, handleContextMenu, position } =
|
||||
useContextMenu();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onUpdate = (description: string) => {
|
||||
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 (
|
||||
<Row
|
||||
ref={triggerRef}
|
||||
@@ -162,6 +184,28 @@ export const TagRow = memo(
|
||||
placeholder: t('No description'),
|
||||
}}
|
||||
/>
|
||||
{tag.tag !== '*' && (
|
||||
<Cell width="auto" style={{ padding: '0 10px' }} plain>
|
||||
<CellButton
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
padding: '3px 6px',
|
||||
backgroundColor: theme.noticeBackground,
|
||||
border: '1px solid ' + theme.noticeBackground,
|
||||
color: theme.noticeTextDark,
|
||||
fontSize: 12,
|
||||
cursor: 'pointer',
|
||||
':hover': { backgroundColor: theme.noticeBackgroundLight },
|
||||
}}
|
||||
onSelect={onShowActivity}
|
||||
>
|
||||
<Text style={{ paddingRight: 5 }}>
|
||||
<Trans>View Transactions</Trans>
|
||||
</Text>
|
||||
<SvgArrowThinRight style={{ width: 8, height: 8 }} />
|
||||
</CellButton>
|
||||
</Cell>
|
||||
)}
|
||||
</Row>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 ?
|
||||
`,
|
||||
['%#%'],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<TagsHandlers>();
|
||||
@@ -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<Tag[]> {
|
||||
return await db.getTags();
|
||||
@@ -56,3 +58,26 @@ async function updateTag(tag: Tag): Promise<Tag> {
|
||||
await db.updateTag(tag);
|
||||
return tag;
|
||||
}
|
||||
|
||||
async function findTags(): Promise<Tag[]> {
|
||||
const taggedNotes = await db.findTags();
|
||||
|
||||
const tags = await getTags();
|
||||
for (const { notes } of taggedNotes) {
|
||||
for (const [_, tag] of notes.matchAll(/(?<!#)#([^#\s]+)/g)) {
|
||||
if (!tags.find(t => 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;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user