Add tree organization system with categories, tags, and folders
Features: - Categories: Global and team-specific tree categorization (admin-managed) - Tags: Flexible tree tagging with autocomplete (author + admin) - User folders: Personal tree collections with subfolder support - Hierarchical structure (max 3 levels deep) - Right-click context menu for folder management - Cascade delete for subfolders - Filter trees by category, tags, and folder in library view Backend: - New models: Category, Tag, UserFolder with relationships - New API endpoints for categories, tags, and folders - Tree organization migrations (005, 006) Frontend: - FolderSidebar with hierarchical folder tree - FolderEditModal for create/edit with color picker - AddToFolderMenu for quick tree organization - TagInput with autocomplete and TagBadges display - Updated TreeMetadataForm and TreeLibraryPage Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
32
frontend/src/api/categories.ts
Normal file
32
frontend/src/api/categories.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import apiClient from './client'
|
||||
import type { Category, CategoryListItem, CategoryCreate, CategoryUpdate } from '@/types'
|
||||
|
||||
export const categoriesApi = {
|
||||
async list(includeInactive = false, teamOnly = false): Promise<CategoryListItem[]> {
|
||||
const response = await apiClient.get<CategoryListItem[]>('/categories', {
|
||||
params: { include_inactive: includeInactive, team_only: teamOnly },
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
async get(id: string): Promise<Category> {
|
||||
const response = await apiClient.get<Category>(`/categories/${id}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async create(data: CategoryCreate): Promise<Category> {
|
||||
const response = await apiClient.post<Category>('/categories', data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async update(id: string, data: CategoryUpdate): Promise<Category> {
|
||||
const response = await apiClient.put<Category>(`/categories/${id}`, data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await apiClient.delete(`/categories/${id}`)
|
||||
},
|
||||
}
|
||||
|
||||
export default categoriesApi
|
||||
50
frontend/src/api/folders.ts
Normal file
50
frontend/src/api/folders.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import apiClient from './client'
|
||||
import type { Folder, FolderListItem, FolderCreate, FolderUpdate, FolderReorderRequest } from '@/types'
|
||||
|
||||
export const foldersApi = {
|
||||
async list(): Promise<FolderListItem[]> {
|
||||
const response = await apiClient.get<FolderListItem[]>('/folders')
|
||||
return response.data
|
||||
},
|
||||
|
||||
async get(id: string): Promise<Folder> {
|
||||
const response = await apiClient.get<Folder>(`/folders/${id}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async create(data: FolderCreate): Promise<Folder> {
|
||||
const response = await apiClient.post<Folder>('/folders', data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async update(id: string, data: FolderUpdate): Promise<Folder> {
|
||||
const response = await apiClient.put<Folder>(`/folders/${id}`, data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await apiClient.delete(`/folders/${id}`)
|
||||
},
|
||||
|
||||
async reorder(folderIds: string[]): Promise<void> {
|
||||
await apiClient.post('/folders/reorder', {
|
||||
folder_ids: folderIds,
|
||||
} as FolderReorderRequest)
|
||||
},
|
||||
|
||||
// Folder tree management
|
||||
async getTreeIds(folderId: string): Promise<string[]> {
|
||||
const response = await apiClient.get<string[]>(`/folders/${folderId}/trees`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async addTree(folderId: string, treeId: string): Promise<void> {
|
||||
await apiClient.post(`/folders/${folderId}/trees`, { tree_id: treeId })
|
||||
},
|
||||
|
||||
async removeTree(folderId: string, treeId: string): Promise<void> {
|
||||
await apiClient.delete(`/folders/${folderId}/trees/${treeId}`)
|
||||
},
|
||||
}
|
||||
|
||||
export default foldersApi
|
||||
@@ -3,3 +3,6 @@ export { default as authApi } from './auth'
|
||||
export { default as treesApi } from './trees'
|
||||
export { default as sessionsApi } from './sessions'
|
||||
export { default as inviteApi } from './invite'
|
||||
export { default as tagsApi } from './tags'
|
||||
export { default as categoriesApi } from './categories'
|
||||
export { default as foldersApi } from './folders'
|
||||
|
||||
54
frontend/src/api/tags.ts
Normal file
54
frontend/src/api/tags.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import apiClient from './client'
|
||||
import type { Tag, TagListItem, TagCreate, TagAssignment } from '@/types'
|
||||
|
||||
export const tagsApi = {
|
||||
async list(includeTeam = true): Promise<TagListItem[]> {
|
||||
const response = await apiClient.get<TagListItem[]>('/tags', {
|
||||
params: { include_team: includeTeam },
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
async search(query: string, limit = 10, includeTeam = true): Promise<TagListItem[]> {
|
||||
const response = await apiClient.get<TagListItem[]>('/tags/search', {
|
||||
params: { q: query, limit, include_team: includeTeam },
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
async get(id: string): Promise<Tag> {
|
||||
const response = await apiClient.get<Tag>(`/tags/${id}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async create(data: TagCreate): Promise<Tag> {
|
||||
const response = await apiClient.post<Tag>('/tags', data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Tree tag management
|
||||
async getTreeTags(treeId: string): Promise<TagListItem[]> {
|
||||
const response = await apiClient.get<TagListItem[]>(`/tags/trees/${treeId}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async addTagsToTree(treeId: string, tags: string[]): Promise<TagListItem[]> {
|
||||
const response = await apiClient.post<TagListItem[]>(`/tags/trees/${treeId}`, {
|
||||
tags,
|
||||
} as TagAssignment)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async replaceTreeTags(treeId: string, tags: string[]): Promise<TagListItem[]> {
|
||||
const response = await apiClient.put<TagListItem[]>(`/tags/trees/${treeId}`, {
|
||||
tags,
|
||||
} as TagAssignment)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async removeTagFromTree(treeId: string, tagSlug: string): Promise<void> {
|
||||
await apiClient.delete(`/tags/trees/${treeId}/${tagSlug}`)
|
||||
},
|
||||
}
|
||||
|
||||
export default tagsApi
|
||||
@@ -1,23 +1,8 @@
|
||||
import apiClient from './client'
|
||||
import type { Tree, TreeListItem, TreeCreate, TreeUpdate } from '@/types'
|
||||
|
||||
export interface TreeListParams {
|
||||
page?: number
|
||||
size?: number
|
||||
category?: string
|
||||
include_inactive?: boolean
|
||||
}
|
||||
|
||||
export interface TreeListResponse {
|
||||
items: TreeListItem[]
|
||||
total: number
|
||||
page: number
|
||||
size: number
|
||||
pages: number
|
||||
}
|
||||
import type { Tree, TreeListItem, TreeCreate, TreeUpdate, TreeFilters } from '@/types'
|
||||
|
||||
export const treesApi = {
|
||||
async list(params?: TreeListParams): Promise<TreeListItem[]> {
|
||||
async list(params?: TreeFilters): Promise<TreeListItem[]> {
|
||||
const response = await apiClient.get<TreeListItem[]>('/trees', { params })
|
||||
return response.data
|
||||
},
|
||||
@@ -41,14 +26,15 @@ export const treesApi = {
|
||||
await apiClient.delete(`/trees/${id}`)
|
||||
},
|
||||
|
||||
async categories(): Promise<string[]> {
|
||||
// Legacy categories endpoint (returns string categories)
|
||||
async legacyCategories(): Promise<string[]> {
|
||||
const response = await apiClient.get<string[]>('/trees/categories')
|
||||
return response.data
|
||||
},
|
||||
|
||||
async search(query: string, category?: string): Promise<TreeListItem[]> {
|
||||
async search(query: string, limit?: number): Promise<TreeListItem[]> {
|
||||
const response = await apiClient.get<TreeListItem[]>('/trees/search', {
|
||||
params: { q: query, category },
|
||||
params: { q: query, limit },
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user