diff --git a/src/renderer/components/Editor/Editor.css b/src/renderer/components/Editor/Editor.css index 797583a..39ae1a6 100644 --- a/src/renderer/components/Editor/Editor.css +++ b/src/renderer/components/Editor/Editor.css @@ -1021,128 +1021,3 @@ font-size: 11px; opacity: 0.7; } - -/* Multi-select dropdown for categories */ -.multi-select-dropdown { - position: relative; - width: 100%; -} - -.multi-select-trigger { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - padding: 8px 10px; - border: 1px solid var(--vscode-input-border, #3c3c3c); - border-radius: 4px; - background: var(--vscode-input-background); - color: var(--vscode-input-foreground); - font-size: 13px; - cursor: pointer; - text-align: left; -} - -.multi-select-trigger:hover { - border-color: var(--vscode-focusBorder, #007fd4); -} - -.multi-select-trigger:focus { - outline: none; - border-color: var(--vscode-focusBorder, #007fd4); -} - -.multi-select-value { - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.multi-select-arrow { - font-size: 10px; - margin-left: 8px; - opacity: 0.7; -} - -.multi-select-menu { - position: absolute; - top: 100%; - left: 0; - right: 0; - margin-top: 2px; - padding: 4px 0; - background: var(--vscode-dropdown-background, #3c3c3c); - border: 1px solid var(--vscode-dropdown-border, #3c3c3c); - border-radius: 4px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - z-index: 1000; - max-height: 200px; - overflow-y: auto; -} - -.multi-select-option { - display: flex; - align-items: center; - gap: 8px; - padding: 6px 10px; - cursor: pointer; - font-size: 13px; - color: var(--vscode-dropdown-foreground); -} - -.multi-select-option:hover { - background: var(--vscode-list-hoverBackground, #2a2d2e); -} - -.multi-select-option input[type="checkbox"] { - margin: 0; - cursor: pointer; - accent-color: var(--vscode-focusBorder, #007fd4); -} - -.multi-select-option span { - flex: 1; -} - -/* Pills showing selected categories */ -.multi-select-pills { - display: flex; - flex-wrap: wrap; - gap: 4px; - margin-top: 6px; -} - -.multi-select-pill { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 2px 6px 2px 8px; - background: var(--vscode-badge-background, #4d4d4d); - color: var(--vscode-badge-foreground, #ffffff); - border-radius: 12px; - font-size: 11px; -} - -.multi-select-pill-remove { - display: flex; - align-items: center; - justify-content: center; - width: 14px; - height: 14px; - padding: 0; - margin: 0; - border: none; - background: transparent; - color: var(--vscode-badge-foreground, #ffffff); - font-size: 12px; - line-height: 1; - cursor: pointer; - border-radius: 50%; - opacity: 0.7; -} - -.multi-select-pill-remove:hover { - opacity: 1; - background: rgba(255, 255, 255, 0.1); -} diff --git a/src/renderer/components/Editor/Editor.tsx b/src/renderer/components/Editor/Editor.tsx index b888f63..c980ad9 100644 --- a/src/renderer/components/Editor/Editor.tsx +++ b/src/renderer/components/Editor/Editor.tsx @@ -162,9 +162,6 @@ export const PostEditor: React.FC = ({ postId }) => { const [author, setAuthor] = useState(''); const [tags, setTags] = useState([]); const [selectedCategories, setSelectedCategories] = useState(['article']); - const [availableCategories, setAvailableCategories] = useState(['article', 'picture', 'aside', 'page']); - const [categoriesDropdownOpen, setCategoriesDropdownOpen] = useState(false); - const categoriesDropdownRef = useRef(null); const [isSaving, setIsSaving] = useState(false); const [hasPublishedVersion, setHasPublishedVersion] = useState(false); const [editorMode, setEditorMode] = useState(preferredEditorMode); @@ -194,34 +191,6 @@ export const PostEditor: React.FC = ({ postId }) => { window.electronAPI?.posts.hasPublishedVersion(postId).then(setHasPublishedVersion); }, [postId]); - // Load available categories from backend (project-scoped) - useEffect(() => { - const loadCategories = async () => { - try { - const categories = await window.electronAPI?.meta.getCategories(); - if (categories && categories.length > 0) { - setAvailableCategories(categories); - } - } catch { - // Keep defaults - } - }; - loadCategories(); - }, []); - - // Close categories dropdown when clicking outside - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (categoriesDropdownRef.current && !categoriesDropdownRef.current.contains(event.target as Node)) { - setCategoriesDropdownOpen(false); - } - }; - if (categoriesDropdownOpen) { - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - } - }, [categoriesDropdownOpen]); - // Resolve media URLs in content for display const resolvedContent = useMemo(() => resolveMediaUrls(content, media), [content, media]); @@ -770,66 +739,14 @@ export const PostEditor: React.FC = ({ postId }) => {
-
- - {categoriesDropdownOpen && ( -
- {availableCategories.map((cat) => ( - - ))} -
- )} -
- {selectedCategories.length > 1 && ( -
- {selectedCategories.map((cat) => ( - - {cat} - - - ))} -
- )} + { + setSelectedCategories(categories.length > 0 ? categories : ['article']); + }} + placeholder="Add categories..." + mode="category" + />
diff --git a/src/renderer/components/TagInput/TagInput.tsx b/src/renderer/components/TagInput/TagInput.tsx index c1d54af..82260b2 100644 --- a/src/renderer/components/TagInput/TagInput.tsx +++ b/src/renderer/components/TagInput/TagInput.tsx @@ -19,6 +19,8 @@ interface TagInputProps { placeholder?: string; /** Whether the input is disabled */ disabled?: boolean; + /** Input mode (tags or categories) */ + mode?: 'tag' | 'category'; } export const TagInput: React.FC = ({ @@ -26,6 +28,7 @@ export const TagInput: React.FC = ({ onChange, placeholder = 'Add tags...', disabled = false, + mode = 'tag', }) => { const [inputValue, setInputValue] = useState(''); const [suggestions, setSuggestions] = useState([]); @@ -40,14 +43,21 @@ export const TagInput: React.FC = ({ // Load all available tags const loadTags = useCallback(async () => { try { - const tags = await window.electronAPI?.tags.getAll(); - if (tags) { - setAllTags(tags as TagData[]); + if (mode === 'category') { + const categories = await window.electronAPI?.meta.getCategories(); + if (categories) { + setAllTags(categories.map((name) => ({ id: name, name }))); + } + } else { + const tags = await window.electronAPI?.tags.getAll(); + if (tags) { + setAllTags(tags as TagData[]); + } } } catch (error) { - console.error('Failed to load tags:', error); + console.error(`Failed to load ${mode}s:`, error); } - }, []); + }, [mode]); useEffect(() => { loadTags(); @@ -55,8 +65,11 @@ export const TagInput: React.FC = ({ // Listen for tag changes useEffect(() => { + if (mode !== 'tag') { + return; + } return subscribeToTagEvents(window.electronAPI?.on, loadTags); - }, [loadTags]); + }, [loadTags, mode]); // Filter suggestions based on input useEffect(() => { @@ -124,16 +137,20 @@ export const TagInput: React.FC = ({ setIsCreating(true); try { - await window.electronAPI?.tags.create({ name: normalized }); + if (mode === 'category') { + await window.electronAPI?.meta.addCategory(normalized); + } else { + await window.electronAPI?.tags.create({ name: normalized }); + } addTag(normalized); - showToast.success(`Tag "${normalized}" created`); + showToast.success(`${mode === 'category' ? 'Category' : 'Tag'} "${normalized}" created`); } catch (error) { const err = error as Error; showToast.error(err.message); } finally { setIsCreating(false); } - }, [allTags, addTag]); + }, [allTags, addTag, mode]); // Handle keyboard navigation const handleKeyDown = (e: React.KeyboardEvent) => { @@ -285,7 +302,7 @@ export const TagInput: React.FC = ({ onClick={() => createAndAddTag(inputValue.trim())} > + - Create "{inputValue.trim()}" + Create {mode === 'category' ? 'category' : 'tag'} "{inputValue.trim()}" )} diff --git a/tests/renderer/components/TagInput.test.tsx b/tests/renderer/components/TagInput.test.tsx index 2526366..bc3e48e 100644 --- a/tests/renderer/components/TagInput.test.tsx +++ b/tests/renderer/components/TagInput.test.tsx @@ -49,3 +49,40 @@ describe('TagInput subscriptions', () => { }); }); }); + +describe('TagInput category mode', () => { + beforeEach(() => { + const onMock = vi.fn((_channel: string, _callback: (...args: unknown[]) => void) => vi.fn()); + + (window as any).electronAPI = { + ...(window as any).electronAPI, + tags: { + getAll: vi.fn().mockResolvedValue([]), + create: vi.fn(), + }, + meta: { + ...(window as any).electronAPI?.meta, + getCategories: vi.fn().mockResolvedValue(['article']), + }, + on: onMock, + }; + }); + + it('loads categories from meta API in category mode', async () => { + render( + + ); + + await act(async () => { + await Promise.resolve(); + }); + + expect(window.electronAPI.meta.getCategories).toHaveBeenCalledTimes(1); + expect(window.electronAPI.on).not.toHaveBeenCalled(); + }); +});