diff --git a/src/renderer/components/Editor/Editor.tsx b/src/renderer/components/Editor/Editor.tsx index 16496f2..6b2e3d8 100644 --- a/src/renderer/components/Editor/Editor.tsx +++ b/src/renderer/components/Editor/Editor.tsx @@ -20,7 +20,7 @@ import { GitDiffView } from '../GitDiffView/GitDiffView'; import { DocumentationView } from '../DocumentationView/DocumentationView'; import { SiteValidationView } from '../SiteValidationView'; import { ScriptsView } from '../ScriptsView/ScriptsView'; -import { AutoSaveManager, getContrastColor } from '../../utils'; +import { AutoSaveManager, getContrastColor, loadTagColorMap } from '../../utils'; import { InsertModal } from '../InsertModal'; import { AISuggestionsModal, AISuggestions } from '../AISuggestionsModal/AISuggestionsModal'; import { openEntityTab } from '../../navigation/tabPolicy'; @@ -1476,12 +1476,6 @@ interface CategoryCount { count: number; } -interface TagDataWithColor { - id: string; - name: string; - color?: string; -} - const Dashboard: React.FC = () => { const { t: tr, language } = useI18n(); const { posts, media } = useAppStore(); @@ -1500,26 +1494,18 @@ const Dashboard: React.FC = () => { useEffect(() => { const loadStats = async () => { try { - const [ds, ym, tc, cc, allTagsData] = await Promise.all([ + const [ds, ym, tc, cc, colorMap] = await Promise.all([ window.electronAPI?.posts.getDashboardStats(), window.electronAPI?.posts.getByYearMonth(), window.electronAPI?.posts.getTagsWithCounts(), window.electronAPI?.posts.getCategoriesWithCounts(), - window.electronAPI?.tags.getAll(), + loadTagColorMap(), ]); if (ds) setStats(ds); if (ym) setYearMonthData(ym); if (tc) setTagCounts(tc); if (cc) setCategoryCounts(cc); - if (allTagsData) { - const colorMap = new Map(); - for (const tag of allTagsData as TagDataWithColor[]) { - if (tag.color) { - colorMap.set(tag.name, tag.color); - } - } - setTagColors(colorMap); - } + setTagColors(colorMap); } catch (e) { console.error('Failed to load dashboard stats:', e); } diff --git a/src/renderer/components/Sidebar/Sidebar.tsx b/src/renderer/components/Sidebar/Sidebar.tsx index 4f5dd25..e9336fd 100644 --- a/src/renderer/components/Sidebar/Sidebar.tsx +++ b/src/renderer/components/Sidebar/Sidebar.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { useAppStore, PostData, MediaData } from '../../store'; import { showToast } from '../Toast'; -import { getContrastColor, groupPostsByStatus } from '../../utils'; +import { getContrastColor, groupPostsByStatus, loadTagColorMap } from '../../utils'; import type { ChatConversation, ImportDefinitionData } from '../../types/electron'; import { GitSidebar } from '../GitSidebar/GitSidebar'; import { scrollToSettingsSection, SettingsCategory } from '../SettingsView/SettingsView'; @@ -27,13 +27,6 @@ function getMediaDisplayName(media: MediaData): string { return media.originalName; } -// Tag data with color information -interface TagData { - id: string; - name: string; - color?: string; -} - const UI_DATE_LOCALE: Record = { en: 'en-US', de: 'de-DE', @@ -537,10 +530,10 @@ const PostsList: React.FC = ({ mode, isActive }) => { // Load available tags with colors and categories useEffect(() => { const loadFilters = async () => { - const [tags, categories, allTagsData] = await Promise.all([ + const [tags, categories, colorMap] = await Promise.all([ window.electronAPI?.posts.getTags(), window.electronAPI?.posts.getCategories(), - window.electronAPI?.tags?.getAll?.(), + loadTagColorMap(), ]); if (tags) setAvailableTags(tags as string[]); if (categories) { @@ -551,15 +544,7 @@ const PostsList: React.FC = ({ mode, isActive }) => { : allCategories ); } - if (allTagsData) { - const colorMap = new Map(); - for (const tag of allTagsData as TagData[]) { - if (tag.color) { - colorMap.set(tag.name, tag.color); - } - } - setTagColors(colorMap); - } + setTagColors(colorMap); }; loadFilters(); }, [posts]); @@ -1005,20 +990,12 @@ const MediaList: React.FC = () => { // Load available tags with colors useEffect(() => { const loadTags = async () => { - const [tags, allTagsData] = await Promise.all([ + const [tags, colorMap] = await Promise.all([ window.electronAPI?.media.getTags(), - window.electronAPI?.tags?.getAll?.(), + loadTagColorMap(), ]); if (tags) setAvailableTags(tags as string[]); - if (allTagsData) { - const colorMap = new Map(); - for (const tag of allTagsData as TagData[]) { - if (tag.color) { - colorMap.set(tag.name, tag.color); - } - } - setTagColors(colorMap); - } + setTagColors(colorMap); }; loadTags(); }, [media]); diff --git a/src/renderer/utils/index.ts b/src/renderer/utils/index.ts index f45b463..4c9915f 100644 --- a/src/renderer/utils/index.ts +++ b/src/renderer/utils/index.ts @@ -3,3 +3,4 @@ export { getContrastColor } from './color'; export { unescapeMacroSyntax } from './markdownEscape'; export { groupPostsByStatus, type GroupedPosts, type PostStatus } from './postGrouping'; export { loadTabsForProject, saveTabsForProject } from './tabPersistence'; +export { buildTagColorMap, loadTagColorMap } from './tagColors'; diff --git a/src/renderer/utils/tagColors.ts b/src/renderer/utils/tagColors.ts new file mode 100644 index 0000000..6d9b5d6 --- /dev/null +++ b/src/renderer/utils/tagColors.ts @@ -0,0 +1,21 @@ +interface TagColorSource { + name: string; + color?: string | null; +} + +export function buildTagColorMap(tags: TagColorSource[] | null | undefined): Map { + const colorMap = new Map(); + + for (const tag of tags ?? []) { + if (tag.color) { + colorMap.set(tag.name, tag.color); + } + } + + return colorMap; +} + +export async function loadTagColorMap(): Promise> { + const allTagsData = await window.electronAPI?.tags?.getAll?.(); + return buildTagColorMap(allTagsData as TagColorSource[] | null | undefined); +} \ No newline at end of file diff --git a/tests/renderer/utils/tagColors.test.ts b/tests/renderer/utils/tagColors.test.ts new file mode 100644 index 0000000..84f5c13 --- /dev/null +++ b/tests/renderer/utils/tagColors.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { buildTagColorMap, loadTagColorMap } from '../../../src/renderer/utils/tagColors'; + +describe('tagColors utils', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('buildTagColorMap includes only tags with colors', () => { + const map = buildTagColorMap([ + { id: '1', name: 'alpha', color: '#111111' }, + { id: '2', name: 'beta' }, + { id: '3', name: 'gamma', color: '#333333' }, + { id: '4', name: 'delta', color: null }, + ]); + + expect(map.get('alpha')).toBe('#111111'); + expect(map.get('gamma')).toBe('#333333'); + expect(map.has('beta')).toBe(false); + expect(map.has('delta')).toBe(false); + }); + + it('loadTagColorMap resolves colors from electron tags api', async () => { + (window as any).electronAPI = { + ...(window as any).electronAPI, + tags: { + getAll: vi.fn().mockResolvedValue([ + { id: '1', name: 'blue', color: '#0000ff' }, + { id: '2', name: 'red', color: '#ff0000' }, + ]), + }, + }; + + const map = await loadTagColorMap(); + + expect(map.get('blue')).toBe('#0000ff'); + expect(map.get('red')).toBe('#ff0000'); + }); + + it('loadTagColorMap returns empty map when api is unavailable', async () => { + (window as any).electronAPI = { + ...(window as any).electronAPI, + tags: undefined, + }; + + const map = await loadTagColorMap(); + expect(map.size).toBe(0); + }); +});