diff --git a/packages/desktop-client/src/components/tags/ManageTags.tsx b/packages/desktop-client/src/components/tags/ManageTags.tsx index 02e557719a..cb9dc8ae72 100644 --- a/packages/desktop-client/src/components/tags/ManageTags.tsx +++ b/packages/desktop-client/src/components/tags/ManageTags.tsx @@ -5,6 +5,7 @@ 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 { Text } from '@actual-app/components/text'; import { theme } from '@actual-app/components/theme'; import { View } from '@actual-app/components/view'; @@ -31,30 +32,15 @@ export function ManageTags() { const [create, setCreate] = useState(false); const tags = useTags(); - const defaultTag = useMemo( - () => ({ - id: '*', - tag: '*', - color: theme.noteTagDefault, - description: t('Default tag color'), - ...tags.find(tag => tag.tag === '*'), - }), - [t, tags], - ); - const filteredTags = useMemo(() => { return filter === '' - ? [defaultTag, ...tags.filter(tag => tag.tag !== '*')] + ? tags : tags.filter(tag => getNormalisedString(tag.tag).includes(getNormalisedString(filter)), ); - }, [defaultTag, filter, tags]); + }, [filter, tags]); - const selectedInst = useSelected( - 'manage-tags', - filteredTags.filter(tag => tag.tag !== '*'), - [], - ); + const selectedInst = useSelected('manage-tags', filteredTags, []); const onDeleteSelected = useCallback(async () => { dispatch(deleteAllTags([...selectedInst.items])); @@ -113,12 +99,25 @@ export function ManageTags() { {create && ( setCreate(false)} tags={tags} /> )} - setHoveredTag(id ?? undefined)} - /> + {tags.length ? ( + setHoveredTag(id ?? undefined)} + /> + ) : ( + + + No Tags + + + )} void; }; +const isTagValid = (tag: string) => tag.match(/^([^#\s]+)$/); + export const TagCreationRow = ({ onClose, tags }: TagCreationRowProps) => { const { t } = useTranslation(); const dispatch = useDispatch(); @@ -64,7 +66,7 @@ export const TagCreationRow = ({ onClose, tags }: TagCreationRowProps) => { }; const onAddTag = () => { - if (!tag.trim() || !color.trim() || tagNames.includes(tag)) { + if (!isTagValid(tag) || !color.trim() || tagNames.includes(tag)) { return; } @@ -187,7 +189,7 @@ export const TagCreationRow = ({ onClose, tags }: TagCreationRowProps) => { style={{ padding: '4px 10px' }} onPress={onAddTag} data-testid="add-button" - isDisabled={!tag || tagNames.includes(tag)} + isDisabled={!isTagValid(tag) || tagNames.includes(tag)} ref={addButtonRef} > Add diff --git a/packages/desktop-client/src/components/tags/TagEditor.tsx b/packages/desktop-client/src/components/tags/TagEditor.tsx index bfc5a8f171..1c5bb51b63 100644 --- a/packages/desktop-client/src/components/tags/TagEditor.tsx +++ b/packages/desktop-client/src/components/tags/TagEditor.tsx @@ -1,12 +1,11 @@ import { type RefObject } from 'react'; -import { useTranslation } from 'react-i18next'; import { Button } from '@actual-app/components/button'; import { ColorPicker } from '@actual-app/components/color-picker'; import { type Tag } from 'loot-core/types/models'; -import { createTag, updateTag } from '@desktop-client/queries/queriesSlice'; +import { updateTag } from '@desktop-client/queries/queriesSlice'; import { useDispatch } from '@desktop-client/redux'; import { useTagCSS } from '@desktop-client/style/tags'; @@ -16,25 +15,16 @@ type TagEditorProps = { }; export const TagEditor = ({ tag, ref }: TagEditorProps) => { - const { t } = useTranslation(); const dispatch = useDispatch(); const getTagCSS = useTagCSS(); - const formattedTag = <>#{tag.tag === '*' ? t('Default') : tag.tag}; + const formattedTag = <>#{tag.tag}; return ( { - dispatch( - tag.id !== '*' - ? updateTag({ ...tag, color: color.toString('hex') }) - : createTag({ - tag: tag.tag, - color: color.toString('hex'), - description: tag.description, - }), - ); + dispatch(updateTag({ ...tag, color: color.toString('hex') })); }} > - - )} + + { + dispatchSelected({ + type: 'select', + id: tag.id, + isRangeSelect: e.shiftKey, + }); + }} + selected={selected} + /> @@ -184,28 +153,27 @@ export const TagRow = memo( placeholder: t('No description'), }} /> - {tag.tag !== '*' && ( - - - - View Transactions - - - - - )} + + + + + View Transactions + + + + ); }, diff --git a/packages/desktop-client/src/style/tags.ts b/packages/desktop-client/src/style/tags.ts index 204aef853d..76dd9b870c 100644 --- a/packages/desktop-client/src/style/tags.ts +++ b/packages/desktop-client/src/style/tags.ts @@ -24,7 +24,7 @@ export function useTags() { return tags; } -function getTagCSSColors(theme: Theme, color?: string) { +function getTagCSSColors(theme: Theme, color?: string | null) { if (theme === 'light') { return [ color ? `${color} !important` : themeStyle.noteTagText, @@ -55,9 +55,7 @@ export function useTagCSS() { const [color, backgroundColor, backgroundColorHovered] = getTagCSSColors( 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), + options.color ?? tags.find(t => t.tag === tag)?.color, ); return css({ diff --git a/packages/loot-core/migrations/1749799110001_tags_tombstone.sql b/packages/loot-core/migrations/1749799110001_tags_tombstone.sql new file mode 100644 index 0000000000..f061e79f13 --- /dev/null +++ b/packages/loot-core/migrations/1749799110001_tags_tombstone.sql @@ -0,0 +1,5 @@ +BEGIN TRANSACTION; + +ALTER TABLE tags ADD COLUMN tombstone integer DEFAULT 0; + +COMMIT; diff --git a/packages/loot-core/src/server/db/index.ts b/packages/loot-core/src/server/db/index.ts index 781335b139..263825cd95 100644 --- a/packages/loot-core/src/server/db/index.ts +++ b/packages/loot-core/src/server/db/index.ts @@ -806,6 +806,15 @@ function toSqlQueryParameters(params: unknown[]) { } export function getTags() { + return all(` + SELECT id, tag, color, description + FROM tags + WHERE tombstone = 0 + ORDER BY tag + `); +} + +export function getAllTags() { return all(` SELECT id, tag, color, description FROM tags @@ -818,9 +827,7 @@ export function insertTag(tag): Promise { } export async function deleteTag(tag) { - return transaction(() => { - runQuery(`DELETE FROM tags WHERE id = ?`, [tag.id]); - }); + return delete_('tags', tag.id); } export function updateTag(tag) { diff --git a/packages/loot-core/src/server/db/types/index.ts b/packages/loot-core/src/server/db/types/index.ts index 9fc6b2bc8c..a8325a1ec3 100644 --- a/packages/loot-core/src/server/db/types/index.ts +++ b/packages/loot-core/src/server/db/types/index.ts @@ -328,6 +328,7 @@ export type DbViewSchedule = { export type DbTag = { id: string; tag: string; - color: string; + color?: string | null; description?: string | null; + tombstone: 1 | 0; }; diff --git a/packages/loot-core/src/server/tags/app.ts b/packages/loot-core/src/server/tags/app.ts index de136e8100..4c7a285517 100644 --- a/packages/loot-core/src/server/tags/app.ts +++ b/packages/loot-core/src/server/tags/app.ts @@ -28,12 +28,26 @@ async function getTags(): Promise { async function createTag({ tag, - color, + color = null, description = null, }: Omit): Promise { + const allTags = await db.getAllTags(); + + const { id: tagId = null } = allTags.find(t => t.tag === tag) || {}; + if (tagId) { + await db.updateTag({ + id: tagId, + tag, + color, + description, + tombstone: 0, + }); + return { id: tagId, tag, color, description }; + } + const id = await db.insertTag({ tag: tag.trim(), - color: color.trim(), + color: color ? color.trim() : null, description, }); @@ -66,7 +80,7 @@ async function findTags(): Promise { for (const { notes } of taggedNotes) { for (const [_, tag] of notes.matchAll(/(? t.tag === tag)) { - tags.push(await createTag({ tag, color: '' })); + tags.push(await createTag({ tag })); } } } diff --git a/packages/loot-core/src/types/models/tags.ts b/packages/loot-core/src/types/models/tags.ts index e4f2da9b63..c5699e7036 100644 --- a/packages/loot-core/src/types/models/tags.ts +++ b/packages/loot-core/src/types/models/tags.ts @@ -1,6 +1,6 @@ export interface Tag { id: string; tag: string; - color: string; + color?: string | null; description?: string | null; } diff --git a/upcoming-release-notes/5387.md b/upcoming-release-notes/5387.md new file mode 100644 index 0000000000..304157ef8c --- /dev/null +++ b/upcoming-release-notes/5387.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [youngcw, pogman-code] +--- + +Fix tags not syncing properly