From 8913f37c3d8fde7dea6d54a550357f1d495b3941 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 7 Mar 2026 19:45:43 -0600 Subject: [PATCH] enh: create subfolder Co-Authored-By: Colin Chen <1207878+silenceroom@users.noreply.github.com> --- backend/open_webui/models/folders.py | 1 + backend/open_webui/routers/folders.py | 6 +-- src/lib/apis/folders/index.ts | 1 + src/lib/components/layout/Sidebar.svelte | 19 ++++++--- .../layout/Sidebar/Folders/FolderMenu.svelte | 14 +++++++ .../layout/Sidebar/Folders/FolderModal.svelte | 4 +- .../layout/Sidebar/RecursiveFolder.svelte | 40 ++++++++++++++++++- src/lib/i18n/locales/en-US/translation.json | 3 +- 8 files changed, 76 insertions(+), 12 deletions(-) diff --git a/backend/open_webui/models/folders.py b/backend/open_webui/models/folders.py index 24a872bcc..b491b831f 100644 --- a/backend/open_webui/models/folders.py +++ b/backend/open_webui/models/folders.py @@ -71,6 +71,7 @@ class FolderForm(BaseModel): name: str data: Optional[dict] = None meta: Optional[dict] = None + parent_id: Optional[str] = None model_config = ConfigDict(extra="allow") diff --git a/backend/open_webui/routers/folders.py b/backend/open_webui/routers/folders.py index ab0326c88..602173e4c 100644 --- a/backend/open_webui/routers/folders.py +++ b/backend/open_webui/routers/folders.py @@ -119,7 +119,7 @@ def create_folder( db: Session = Depends(get_session), ): folder = Folders.get_folder_by_parent_id_and_user_id_and_name( - None, user.id, form_data.name, db=db + form_data.parent_id, user.id, form_data.name, db=db ) if folder: @@ -129,7 +129,7 @@ def create_folder( ) try: - folder = Folders.insert_new_folder(user.id, form_data, db=db) + folder = Folders.insert_new_folder(user.id, form_data, form_data.parent_id, db=db) return folder except Exception as e: log.exception(e) @@ -317,7 +317,7 @@ async def delete_folder_by_id( folder = folders.pop() if folder: try: - folder_ids = Folders.delete_folder_by_id_and_user_id(id, user.id, db=db) + folder_ids = Folders.delete_folder_by_id_and_user_id(folder.id, user.id, db=db) for folder_id in folder_ids: if delete_contents: diff --git a/src/lib/apis/folders/index.ts b/src/lib/apis/folders/index.ts index 535adbd5f..af5405de3 100644 --- a/src/lib/apis/folders/index.ts +++ b/src/lib/apis/folders/index.ts @@ -4,6 +4,7 @@ type FolderForm = { name?: string; data?: Record; meta?: Record; + parent_id?: string | null; }; export const createNewFolder = async (token: string, folderForm: FolderForm) => { diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index 27f9af905..27e03a786 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -142,19 +142,24 @@ } }; - const createFolder = async ({ name, data }) => { + const createFolder = async ({ name, data, parent_id }) => { name = name?.trim(); if (!name) { toast.error($i18n.t('Folder name cannot be empty.')); return; } - const rootFolders = Object.values(folders).filter((folder) => folder.parent_id === null); - if (rootFolders.find((folder) => folder.name.toLowerCase() === name.toLowerCase())) { + // Check for duplicate names in the same parent + const siblings = Object.values(folders).filter( + (folder) => folder.parent_id === parent_id + ); + if (siblings.find((folder) => folder.name.toLowerCase() === name.toLowerCase())) { // If a folder with the same name already exists, append a number to the name let i = 1; while ( - rootFolders.find((folder) => folder.name.toLowerCase() === `${name} ${i}`.toLowerCase()) + siblings.find( + (folder) => folder.name.toLowerCase() === `${name} ${i}`.toLowerCase() + ) ) { i++; } @@ -166,9 +171,10 @@ const tempId = uuidv4(); folders = { ...folders, - tempId: { + [tempId]: { id: tempId, name: name, + parent_id: parent_id, created_at: Date.now(), updated_at: Date.now() } @@ -176,7 +182,8 @@ const res = await createNewFolder(localStorage.token, { name, - data + data, + parent_id }).catch((error) => { toast.error(`${error}`); return null; diff --git a/src/lib/components/layout/Sidebar/Folders/FolderMenu.svelte b/src/lib/components/layout/Sidebar/Folders/FolderMenu.svelte index c9cac29c7..7d0c2faf8 100644 --- a/src/lib/components/layout/Sidebar/Folders/FolderMenu.svelte +++ b/src/lib/components/layout/Sidebar/Folders/FolderMenu.svelte @@ -11,11 +11,13 @@ import Pencil from '$lib/components/icons/Pencil.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte'; import Download from '$lib/components/icons/Download.svelte'; + import Folder from '$lib/components/icons/Folder.svelte'; export let align: 'start' | 'end' = 'start'; export let onEdit = () => {}; export let onExport = () => {}; export let onDelete = () => {}; + export let onCreateSub = () => {}; let show = false; @@ -47,6 +49,18 @@ {align} transition={flyAndScale} > + { + onCreateSub(); + }} + > + +
{$i18n.t('Create Folder')}
+
+ +
+ { diff --git a/src/lib/components/layout/Sidebar/Folders/FolderModal.svelte b/src/lib/components/layout/Sidebar/Folders/FolderModal.svelte index f1d097528..73248a59c 100644 --- a/src/lib/components/layout/Sidebar/Folders/FolderModal.svelte +++ b/src/lib/components/layout/Sidebar/Folders/FolderModal.svelte @@ -19,6 +19,7 @@ export let onSubmit: Function = (e) => {}; export let folderId = null; + export let parentId = null; export let edit = false; let folder = null; @@ -55,7 +56,8 @@ await onSubmit({ name, meta, - data + data, + parent_id: edit ? undefined : parentId }); show = false; loading = false; diff --git a/src/lib/components/layout/Sidebar/RecursiveFolder.svelte b/src/lib/components/layout/Sidebar/RecursiveFolder.svelte index f800cfe9b..3557a4fd5 100644 --- a/src/lib/components/layout/Sidebar/RecursiveFolder.svelte +++ b/src/lib/components/layout/Sidebar/RecursiveFolder.svelte @@ -18,7 +18,8 @@ updateFolderIsExpandedById, updateFolderById, updateFolderParentIdById, - getFolderById + getFolderById, + createNewFolder } from '$lib/apis/folders'; import { getChatById, @@ -64,6 +65,9 @@ let showFolderModal = false; let edit = false; + let showCreateSubFolderModal = false; + let createSubFolderParentId = null; + let draggedOver = false; let dragged = false; @@ -414,6 +418,30 @@ saveAs(blob, `folder-${folders[folderId].name}-export-${Date.now()}.json`); }; + + const createSubFolderHandler = async ({ name, meta, data, parent_id }) => { + if (name === '') { + toast.error($i18n.t('Folder name cannot be empty.')); + return; + } + + name = name.trim(); + + const res = await createNewFolder(localStorage.token, { + name, + data, + meta, + parent_id + }).catch((error) => { + toast.error(`${error}`); + return null; + }); + + if (res) { + toast.success($i18n.t('Folder created successfully')); + dispatch('update'); + } + }; + + {#if dragged && x && y}
@@ -593,6 +627,10 @@ onExport={() => { exportHandler(); }} + onCreateSub={() => { + createSubFolderParentId = folderId; + showCreateSubFolderModal = true; + }} >
diff --git a/src/lib/i18n/locales/en-US/translation.json b/src/lib/i18n/locales/en-US/translation.json index dced0f6c9..ae23cad54 100644 --- a/src/lib/i18n/locales/en-US/translation.json +++ b/src/lib/i18n/locales/en-US/translation.json @@ -897,6 +897,7 @@ "Focus Chat Input": "", "Folder": "", "Folder Background Image": "", + "Folder created successfully": "", "Folder deleted successfully": "", "Folder Max File Count": "", "Folder name": "", @@ -2135,4 +2136,4 @@ "YouTube": "", "Youtube Language": "", "Youtube Proxy URL": "" -} +} \ No newline at end of file