diff --git a/backend/open_webui/apps/webui/models/folders.py b/backend/open_webui/apps/webui/models/folders.py index 93a53a590..6e28e3797 100644 --- a/backend/open_webui/apps/webui/models/folders.py +++ b/backend/open_webui/apps/webui/models/folders.py @@ -67,14 +67,17 @@ class FolderItemsUpdateForm(BaseModel): class FolderTable: - def insert_new_folder(self, name: str, user_id: str) -> Optional[FolderModel]: + def insert_new_folder( + self, user_id: str, name: str, parent_id: Optional[str] = None + ) -> Optional[FolderModel]: with get_db() as db: - id = name.lower() + id = str(uuid.uuid4()) folder = FolderModel( **{ "id": id, "user_id": user_id, "name": name, + "parent_id": parent_id, "created_at": int(time.time()), "updated_at": int(time.time()), } @@ -92,11 +95,10 @@ class FolderTable: print(e) return None - def get_folder_by_name_and_user_id( - self, name: str, user_id: str + def get_folder_by_id_and_user_id( + self, id: str, user_id: str ) -> Optional[FolderModel]: try: - id = name.lower() with get_db() as db: folder = db.query(Folder).filter_by(id=id, user_id=user_id).first() return FolderModel.model_validate(folder) @@ -110,7 +112,24 @@ class FolderTable: for folder in db.query(Folder).filter_by(user_id=user_id).all() ] - def get_folders_by_parent_id_and_user_id(self, parent_id: str, user_id: str): + def get_folder_by_parent_id_and_user_id_and_name( + self, parent_id: Optional[str], user_id: str, name: str + ) -> Optional[FolderModel]: + try: + with get_db() as db: + folder = ( + db.query(Folder) + .filter_by(parent_id=parent_id, user_id=user_id, name=name) + .first() + ) + return FolderModel.model_validate(folder) + except Exception as e: + log.error(f"get_folder_by_name_and_user_id: {e}") + return None + + def get_folders_by_parent_id_and_user_id( + self, parent_id: Optional[str], user_id: str + ) -> list[FolderModel]: with get_db() as db: return [ FolderModel.model_validate(folder) @@ -138,21 +157,23 @@ class FolderTable: log.error(f"update_folder: {e}") return - def update_folder_name_by_name_and_user_id( - self, name: str, user_id: str, new_name: str + def update_folder_name_by_id_and_user_id( + self, id: str, user_id: str, name: str ) -> Optional[FolderModel]: try: - id = name.lower() - new_id = new_name.lower() with get_db() as db: - # Check if new folder name already exists - folder = db.query(Folder).filter_by(id=new_id, user_id=user_id).first() - if folder: + folder = db.query(Folder).filter_by(id=id, user_id=user_id).first() + + existing_folder = ( + db.query(Folder) + .filter_by(name=name, parent_id=folder.parent_id, user_id=user_id) + .first() + ) + + if existing_folder: return None - folder = db.query(Folder).filter_by(id=id, user_id=user_id).first() - folder.id = new_id - folder.name = new_name + folder.name = name folder.updated_at = int(time.time()) db.commit() @@ -162,11 +183,10 @@ class FolderTable: log.error(f"update_folder: {e}") return - def update_folder_items_by_name_and_user_id( - self, name: str, user_id: str, items: FolderItems + def update_folder_items_by_id_and_user_id( + self, id: str, user_id: str, items: FolderItems ) -> Optional[FolderModel]: try: - id = name.lower() with get_db() as db: folder = db.query(Folder).filter_by(id=id, user_id=user_id).first() @@ -180,14 +200,11 @@ class FolderTable: log.error(f"update_folder: {e}") return - def delete_folder_by_name_and_user_id(self, name: str, user_id: str) -> bool: + def delete_folder_by_id_and_user_id(self, id: str, user_id: str) -> bool: try: with get_db() as db: - id = name.lower() - folder = db.query(Folder).filter_by(id=id, user_id=user_id).first() db.delete(folder) - db.commit() return True except Exception as e: diff --git a/backend/open_webui/apps/webui/routers/folders.py b/backend/open_webui/apps/webui/routers/folders.py index b0c26b595..df38b2190 100644 --- a/backend/open_webui/apps/webui/routers/folders.py +++ b/backend/open_webui/apps/webui/routers/folders.py @@ -52,7 +52,10 @@ async def get_folders(user=Depends(get_verified_user)): @router.post("/") def create_folder(form_data: FolderForm, user=Depends(get_verified_user)): - folder = Folders.get_folder_by_name_and_user_id(form_data.name, user.id) + folder = Folders.get_folder_by_parent_id_and_user_id_and_name( + None, user.id, form_data.name + ) + if folder: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -60,11 +63,11 @@ def create_folder(form_data: FolderForm, user=Depends(get_verified_user)): ) try: - folder = Folders.insert_new_folder(form_data.name, user.id) + folder = Folders.insert_new_folder(user.id, form_data.name) return folder except Exception as e: log.exception(e) - log.error(f"Error creating folder: {form_data.name}") + log.error("Error creating folder") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT("Error creating folder"), @@ -78,7 +81,7 @@ def create_folder(form_data: FolderForm, user=Depends(get_verified_user)): @router.get("/{id}", response_model=Optional[FolderModel]) async def get_folder_by_id(id: str, user=Depends(get_verified_user)): - folder = Folders.get_folder_by_name_and_user_id(id, user.id) + folder = Folders.get_folder_by_id_and_user_id(id, user.id) if folder: return folder else: @@ -97,35 +100,65 @@ async def get_folder_by_id(id: str, user=Depends(get_verified_user)): async def update_folder_name_by_id( id: str, form_data: FolderForm, user=Depends(get_verified_user) ): - new_id = form_data.name.lower() - folder = Folders.get_folder_by_name_and_user_id(new_id, user.id) + folder = Folders.get_folder_by_id_and_user_id(id, user.id) if folder: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Folder already exists"), + existing_folder = Folders.get_folder_by_parent_id_and_user_id_and_name( + folder.parent_id, user.id, form_data.name ) - - folder = Folders.get_folder_by_name_and_user_id(id, user.id) - if folder: - try: - folder = Folders.update_folder_name_by_name_and_user_id( - id, user.id, form_data.name + if existing_folder: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Folder already exists"), ) - # Update children folders parent_id - children_folders = Folders.get_folders_by_parent_id_and_user_id(id, user.id) - for child in children_folders: - Folders.update_folder_parent_id_by_id_and_user_id( - child.id, user.id, folder.id - ) + try: + folder = Folders.update_folder_name_by_id_and_user_id( + id, user.id, form_data.name + ) + return folder + except Exception as e: + log.exception(e) + log.error(f"Error updating folder: {id}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Error updating folder"), + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) - # Update children items parent_id - chats = Chats.get_chats_by_folder_id_and_user_id(id, user.id) - for chat in chats: - Chats.update_chat_folder_id_by_id_and_user_id( - chat.id, user.id, folder.id - ) +############################ +# Update Folder Name By Id +############################ + + +class FolderParentIdForm(BaseModel): + parent_id: str + + +@router.post("/{id}/update/parent") +async def update_folder_parent_id_by_id( + id: str, form_data: FolderParentIdForm, user=Depends(get_verified_user) +): + folder = Folders.get_folder_by_id_and_user_id(id, user.id) + if folder: + existing_folder = Folders.get_folder_by_parent_id_and_user_id_and_name( + form_data.parent_id, user.id, folder.name + ) + + if existing_folder: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Folder already exists"), + ) + + try: + folder = Folders.update_folder_parent_id_by_id_and_user_id( + id, user.id, form_data.parent_id + ) return folder except Exception as e: log.exception(e) @@ -150,10 +183,10 @@ async def update_folder_name_by_id( async def update_folder_items_by_id( id: str, form_data: FolderItemsUpdateForm, user=Depends(get_verified_user) ): - folder = Folders.get_folder_by_name_and_user_id(id, user.id) + folder = Folders.get_folder_by_id_and_user_id(id, user.id) if folder: try: - folder = Folders.update_folder_by_name_and_user_id( + folder = Folders.update_folder_items_by_id_and_user_id( id, user.id, form_data.items ) return folder @@ -178,10 +211,10 @@ async def update_folder_items_by_id( @router.delete("/{id}") async def delete_folder_by_id(id: str, user=Depends(get_verified_user)): - folder = Folders.get_folder_by_name_and_user_id(id, user.id) + folder = Folders.get_folder_by_id_and_user_id(id, user.id) if folder: try: - result = Folders.delete_folder_by_name_and_user_id(id, user.id) + result = Folders.delete_folder_by_id_and_user_id(id, user.id) return result except Exception as e: log.exception(e) diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index 79f32851e..5043d39d9 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -75,6 +75,8 @@ return []; }); + folders = {}; + for (const folder of folderList) { folders[folder.id] = { ...(folders[folder.id] ? folders[folder.id] : {}), ...folder }; @@ -636,15 +638,15 @@ {/if} - {#if folders} -
+
+ {#if !search && folders} -
- {/if} + {/if} -
{ const { type, id } = e.detail; @@ -662,7 +664,7 @@ } }} > -
+
{#if $chats} {#each $chats as chat, idx} {#if idx === 0 || (idx > 0 && chat.time_range !== $chats[idx - 1].time_range)} @@ -695,6 +697,7 @@ {/if} -
+
- +
{title}
diff --git a/src/lib/components/layout/Sidebar/Folders.svelte b/src/lib/components/layout/Sidebar/Folders.svelte index 102c256d4..e675c609e 100644 --- a/src/lib/components/layout/Sidebar/Folders.svelte +++ b/src/lib/components/layout/Sidebar/Folders.svelte @@ -6,7 +6,12 @@ // Get the list of folders that have no parent, sorted by name alphabetically $: folderList = Object.keys(folders) .filter((key) => folders[key].parent_id === null) - .sort((a, b) => folders[a].name.localeCompare(folders[b].name)); + .sort((a, b) => + folders[a].name.localeCompare(folders[b].name, undefined, { + numeric: true, + sensitivity: 'base' + }) + ); {#each folderList as folderId (folderId)} diff --git a/src/lib/components/layout/Sidebar/Folders/FolderMenu.svelte b/src/lib/components/layout/Sidebar/Folders/FolderMenu.svelte new file mode 100644 index 000000000..e69de29bb diff --git a/src/lib/components/layout/Sidebar/RecursiveFolder.svelte b/src/lib/components/layout/Sidebar/RecursiveFolder.svelte index 6c653769a..68f4117a2 100644 --- a/src/lib/components/layout/Sidebar/RecursiveFolder.svelte +++ b/src/lib/components/layout/Sidebar/RecursiveFolder.svelte @@ -10,13 +10,14 @@ import DragGhost from '$lib/components/common/DragGhost.svelte'; import FolderOpen from '$lib/components/icons/FolderOpen.svelte'; + import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte'; export let open = true; export let folders; export let folderId; - export let className; + export let className = ''; let folderElement; @@ -67,6 +68,7 @@ let y; const onDragStart = (event) => { + event.stopPropagation(); event.dataTransfer.setDragImage(dragImage, 0, 0); // Set the data to be transferred @@ -83,11 +85,15 @@ }; const onDrag = (event) => { + event.stopPropagation(); + x = event.clientX; y = event.clientY; }; const onDragEnd = (event) => { + event.stopPropagation(); + folderElement.style.opacity = '1'; // Reset visual cue after drag dragged = false; }; @@ -122,7 +128,7 @@ {#if dragged && x && y} -
+
@@ -149,7 +155,7 @@ }} > -
+
+ +
@@ -203,8 +220,8 @@ class="ml-3 pl-1 mt-[1px] flex flex-col overflow-y-auto scrollbar-hidden border-s dark:border-gray-850" > {#if folders[folderId]?.childrenIds} - {#each folders[folderId]?.childrenIds as folderId (folderId)} - + {#each folders[folderId]?.childrenIds as childId (`${folderId}-${childId}`)} + {/each} {/if}