From a942c30ca86c6d4fa6a577abc6b469f89afd6d18 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Wed, 16 Oct 2024 21:05:03 -0700 Subject: [PATCH] feat: folder ui --- backend/open_webui/apps/webui/main.py | 2 + backend/open_webui/apps/webui/models/chats.py | 25 ++ .../open_webui/apps/webui/models/folders.py | 70 +++++- .../open_webui/apps/webui/routers/folders.py | 197 ++++++++++++++++ .../versions/c69f45358db4_add_folder_table.py | 9 +- src/lib/apis/folders/index.ts | 198 ++++++++++++++++ src/lib/components/common/DragGhost.svelte | 2 +- src/lib/components/common/Folder.svelte | 34 +-- src/lib/components/icons/Document.svelte | 19 ++ src/lib/components/layout/Sidebar.svelte | 119 ++++++++-- .../components/layout/Sidebar/ChatItem.svelte | 95 ++++---- .../components/layout/Sidebar/Folders.svelte | 14 ++ .../layout/Sidebar/RecursiveFolder.svelte | 220 ++++++++++++++++++ 13 files changed, 917 insertions(+), 87 deletions(-) create mode 100644 backend/open_webui/apps/webui/routers/folders.py create mode 100644 src/lib/apis/folders/index.ts create mode 100644 src/lib/components/icons/Document.svelte create mode 100644 src/lib/components/layout/Sidebar/Folders.svelte create mode 100644 src/lib/components/layout/Sidebar/RecursiveFolder.svelte diff --git a/backend/open_webui/apps/webui/main.py b/backend/open_webui/apps/webui/main.py index 1d12d708e..94e42f4a8 100644 --- a/backend/open_webui/apps/webui/main.py +++ b/backend/open_webui/apps/webui/main.py @@ -9,6 +9,7 @@ from open_webui.apps.webui.models.models import Models from open_webui.apps.webui.routers import ( auths, chats, + folders, configs, files, functions, @@ -110,6 +111,7 @@ app.include_router(configs.router, prefix="/configs", tags=["configs"]) app.include_router(auths.router, prefix="/auths", tags=["auths"]) app.include_router(users.router, prefix="/users", tags=["users"]) app.include_router(chats.router, prefix="/chats", tags=["chats"]) +app.include_router(folders.router, prefix="/folders", tags=["folders"]) app.include_router(models.router, prefix="/models", tags=["models"]) app.include_router(knowledge.router, prefix="/knowledge", tags=["knowledge"]) diff --git a/backend/open_webui/apps/webui/models/chats.py b/backend/open_webui/apps/webui/models/chats.py index 12bdd1c38..2b0d29795 100644 --- a/backend/open_webui/apps/webui/models/chats.py +++ b/backend/open_webui/apps/webui/models/chats.py @@ -33,6 +33,7 @@ class Chat(Base): pinned = Column(Boolean, default=False, nullable=True) meta = Column(JSON, server_default="{}") + folder_id = Column(Text, nullable=True) class ChatModel(BaseModel): @@ -51,6 +52,7 @@ class ChatModel(BaseModel): pinned: Optional[bool] = False meta: dict = {} + folder_id: Optional[str] = None #################### @@ -512,6 +514,29 @@ class ChatTable: # Validate and return chats return [ChatModel.model_validate(chat) for chat in all_chats] + def get_chats_by_folder_id_and_user_id( + self, folder_id: str, user_id: str + ) -> list[ChatModel]: + with get_db() as db: + all_chats = ( + db.query(Chat).filter_by(folder_id=folder_id, user_id=user_id).all() + ) + return [ChatModel.model_validate(chat) for chat in all_chats] + + def update_chat_folder_id_by_id_and_user_id( + self, id: str, user_id: str, folder_id: str + ) -> Optional[ChatModel]: + try: + with get_db() as db: + chat = db.get(Chat, id) + chat.folder_id = folder_id + chat.updated_at = int(time.time()) + db.commit() + db.refresh(chat) + return ChatModel.model_validate(chat) + except Exception: + return None + def get_chat_tags_by_id_and_user_id(self, id: str, user_id: str) -> list[TagModel]: with get_db() as db: chat = db.get(Chat, id) diff --git a/backend/open_webui/apps/webui/models/folders.py b/backend/open_webui/apps/webui/models/folders.py index e8e6ffef1..93a53a590 100644 --- a/backend/open_webui/apps/webui/models/folders.py +++ b/backend/open_webui/apps/webui/models/folders.py @@ -22,7 +22,6 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"]) class FolderItems(BaseModel): chat_ids: Optional[list[str]] = None file_ids: Optional[list[str]] = None - folder_ids: Optional[list[str]] = None model_config = ConfigDict(extra="allow") @@ -52,6 +51,21 @@ class FolderModel(BaseModel): model_config = ConfigDict(from_attributes=True) +#################### +# Forms +#################### + + +class FolderForm(BaseModel): + name: str + model_config = ConfigDict(extra="allow") + + +class FolderItemsUpdateForm(BaseModel): + items: FolderItems + model_config = ConfigDict(extra="allow") + + class FolderTable: def insert_new_folder(self, name: str, user_id: str) -> Optional[FolderModel]: with get_db() as db: @@ -96,7 +110,59 @@ class FolderTable: for folder in db.query(Folder).filter_by(user_id=user_id).all() ] - def update_folder_by_name_and_user_id( + def get_folders_by_parent_id_and_user_id(self, parent_id: str, user_id: str): + with get_db() as db: + return [ + FolderModel.model_validate(folder) + for folder in db.query(Folder) + .filter_by(parent_id=parent_id, user_id=user_id) + .all() + ] + + def update_folder_parent_id_by_id_and_user_id( + self, + id: str, + user_id: str, + parent_id: str, + ) -> Optional[FolderModel]: + try: + with get_db() as db: + folder = db.query(Folder).filter_by(id=id, user_id=user_id).first() + folder.parent_id = parent_id + folder.updated_at = int(time.time()) + + db.commit() + + return FolderModel.model_validate(folder) + except Exception as e: + 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 + ) -> 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: + return None + + folder = db.query(Folder).filter_by(id=id, user_id=user_id).first() + folder.id = new_id + folder.name = new_name + folder.updated_at = int(time.time()) + + db.commit() + + return FolderModel.model_validate(folder) + except Exception as e: + log.error(f"update_folder: {e}") + return + + def update_folder_items_by_name_and_user_id( self, name: str, user_id: str, items: FolderItems ) -> Optional[FolderModel]: try: diff --git a/backend/open_webui/apps/webui/routers/folders.py b/backend/open_webui/apps/webui/routers/folders.py new file mode 100644 index 000000000..b0c26b595 --- /dev/null +++ b/backend/open_webui/apps/webui/routers/folders.py @@ -0,0 +1,197 @@ +import logging +import os +import shutil +import uuid +from pathlib import Path +from typing import Optional +from pydantic import BaseModel +import mimetypes + + +from open_webui.apps.webui.models.folders import ( + FolderForm, + FolderItemsUpdateForm, + FolderModel, + Folders, +) +from open_webui.apps.webui.models.chats import Chats + +from open_webui.config import UPLOAD_DIR +from open_webui.env import SRC_LOG_LEVELS +from open_webui.constants import ERROR_MESSAGES + + +from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status +from fastapi.responses import FileResponse, StreamingResponse + + +from open_webui.utils.utils import get_admin_user, get_verified_user + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + + +router = APIRouter() + + +############################ +# Get Folders +############################ + + +@router.get("/", response_model=list[FolderModel]) +async def get_folders(user=Depends(get_verified_user)): + folders = Folders.get_folders_by_user_id(user.id) + return folders + + +############################ +# Create Folder +############################ + + +@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) + if folder: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Folder already exists"), + ) + + try: + folder = Folders.insert_new_folder(form_data.name, user.id) + return folder + except Exception as e: + log.exception(e) + log.error(f"Error creating folder: {form_data.name}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Error creating folder"), + ) + + +############################ +# Get Folders By Id +############################ + + +@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) + if folder: + return folder + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# Update Folder Name By Id +############################ + + +@router.post("/{id}/update") +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) + if folder: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Folder already exists"), + ) + + 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 + ) + + # 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 + ) + + # 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 + ) + + 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 Folder Items By Id +############################ + + +@router.post("/{id}/update/items") +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) + if folder: + try: + folder = Folders.update_folder_by_name_and_user_id( + id, user.id, form_data.items + ) + 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, + ) + + +############################ +# Delete Folder 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) + if folder: + try: + result = Folders.delete_folder_by_name_and_user_id(id, user.id) + return result + except Exception as e: + log.exception(e) + log.error(f"Error deleting folder: {id}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Error deleting folder"), + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) diff --git a/backend/open_webui/migrations/versions/c69f45358db4_add_folder_table.py b/backend/open_webui/migrations/versions/c69f45358db4_add_folder_table.py index 9bbec3a59..0341cad07 100644 --- a/backend/open_webui/migrations/versions/c69f45358db4_add_folder_table.py +++ b/backend/open_webui/migrations/versions/c69f45358db4_add_folder_table.py @@ -34,9 +34,16 @@ def upgrade(): server_default=sa.func.now(), onupdate=sa.func.now(), ), - sa.PrimaryKeyConstraint("id", "user_id") + sa.PrimaryKeyConstraint("id", "user_id"), + ) + + op.add_column( + "chat", + sa.Column("folder_id", sa.Text(), nullable=True), ) def downgrade(): + op.drop_column("chat", "folder_id") + op.drop_table("folder") diff --git a/src/lib/apis/folders/index.ts b/src/lib/apis/folders/index.ts new file mode 100644 index 000000000..2098a7810 --- /dev/null +++ b/src/lib/apis/folders/index.ts @@ -0,0 +1,198 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const createNewFolder = async (token: string, name: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/folders/`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + name: name + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFolders = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/folders/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFolderById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateFolderNameById = async (token: string, id: string, name: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + name: name + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type FolderItems = { + chat_ids: string[]; + file_ids: string[]; + folder_ids: string[]; +}; + +export const updateFolderItemsById = async (token: string, id: string, items: FolderItems) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}/update/items`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + items: items + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteFolderById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/components/common/DragGhost.svelte b/src/lib/components/common/DragGhost.svelte index 6d97a4517..7169d72f0 100644 --- a/src/lib/components/common/DragGhost.svelte +++ b/src/lib/components/common/DragGhost.svelte @@ -24,7 +24,7 @@ bind:this={popupElement} class="fixed top-0 left-0 w-screen h-[100dvh] z-50 touch-none pointer-events-none" > -
+
diff --git a/src/lib/components/common/Folder.svelte b/src/lib/components/common/Folder.svelte index 2002b7e0b..42ca584fc 100644 --- a/src/lib/components/common/Folder.svelte +++ b/src/lib/components/common/Folder.svelte @@ -14,13 +14,15 @@ export let name = ''; export let collapsible = true; + export let className = ''; + let folderElement; - let dragged = false; + let draggedOver = false; const onDragOver = (e) => { e.preventDefault(); - dragged = true; + draggedOver = true; }; const onDrop = (e) => { @@ -29,19 +31,23 @@ if (folderElement.contains(e.target)) { console.log('Dropped on the Button'); - // get data from the drag event - const dataTransfer = e.dataTransfer.getData('text/plain'); - const data = JSON.parse(dataTransfer); - console.log(data); - dispatch('drop', data); + try { + // get data from the drag event + const dataTransfer = e.dataTransfer.getData('text/plain'); + const data = JSON.parse(dataTransfer); + console.log(data); + dispatch('drop', data); + } catch (error) { + console.error(error); + } - dragged = false; + draggedOver = false; } }; const onDragLeave = (e) => { e.preventDefault(); - dragged = false; + draggedOver = false; }; onMount(() => { @@ -57,10 +63,10 @@ }); -
- {#if dragged} +
+ {#if draggedOver}
{/if} @@ -74,7 +80,7 @@ }} > -
+
-
+
diff --git a/src/lib/components/icons/Document.svelte b/src/lib/components/icons/Document.svelte new file mode 100644 index 000000000..9ae719725 --- /dev/null +++ b/src/lib/components/icons/Document.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index ec54e0bc1..79f32851e 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -43,6 +43,8 @@ import Folder from '../common/Folder.svelte'; import Plus from '../icons/Plus.svelte'; import Tooltip from '../common/Tooltip.svelte'; + import { createNewFolder, getFolders } from '$lib/apis/folders'; + import Folders from './Sidebar/Folders.svelte'; const BREAKPOINT = 768; @@ -65,6 +67,55 @@ let chatListLoading = false; let allChatsLoaded = false; + let folders = {}; + + const initFolders = async () => { + const folderList = await getFolders(localStorage.token).catch((error) => { + toast.error(error); + return []; + }); + + for (const folder of folderList) { + folders[folder.id] = { ...(folders[folder.id] ? folders[folder.id] : {}), ...folder }; + + if (folders[folder.id].parent_id) { + folders[folders[folder.id].parent_id].childrenIds = folders[folders[folder.id].parent_id] + .childrenIds + ? [...folders[folders[folder.id].parent_id].childrenIds, folder.id] + : [folder.id]; + + folders[folders[folder.id].parent_id].childrenIds.sort((a, b) => { + return folders[b].updated_at - folders[a].updated_at; + }); + } + } + }; + + const createFolder = async (name = 'Untitled') => { + if (name === '') { + toast.error($i18n.t('Folder name cannot be empty.')); + return; + } + + if (name.toLowerCase() in folders) { + // If a folder with the same name already exists, append a number to the name + let i = 1; + while (name.toLowerCase() + ` ${i}` in folders) { + i++; + } + name = name + ` ${i}`; + } + + const res = await createNewFolder(localStorage.token, name).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + await initFolders(); + } + }; + const initChatList = async () => { // Reset pagination variables tags.set(await getAllTags(localStorage.token)); @@ -280,6 +331,7 @@ localStorage.sidebar = value; }); + await initFolders(); await pinnedChats.set(await getPinnedChatList(localStorage.token)); await initChatList(); @@ -491,7 +543,12 @@
- @@ -514,33 +571,40 @@ {/if} {#if !search && $pinnedChats.length > 0} -
+
{ localStorage.setItem('showPinnedChat', e.detail); console.log(e.detail); }} on:drop={async (e) => { - const { id } = e.detail; + const { type, id } = e.detail; - const status = await getChatPinnedStatusById(localStorage.token, id); + if (type === 'chat') { + const status = await getChatPinnedStatusById(localStorage.token, id); - if (!status) { - const res = await toggleChatPinnedStatusById(localStorage.token, id); + if (!status) { + const res = await toggleChatPinnedStatusById(localStorage.token, id); - if (res) { - await pinnedChats.set(await getPinnedChatList(localStorage.token)); - initChatList(); + if (res) { + await pinnedChats.set(await getPinnedChatList(localStorage.token)); + initChatList(); + } } } }} name={$i18n.t('Pinned')} > -
+
{#each $pinnedChats as chat, idx} { @@ -557,6 +621,10 @@ showDeleteConfirm = true; } }} + on:change={async () => { + await pinnedChats.set(await getPinnedChatList(localStorage.token)); + initChatList(); + }} on:tag={(e) => { const { type, name } = e.detail; tagEventHandler(type, name, chat.id); @@ -568,20 +636,28 @@
{/if} + {#if folders} +
+ +
+ {/if} +
{ - const { id } = e.detail; + const { type, id } = e.detail; - const status = await getChatPinnedStatusById(localStorage.token, id); + if (type === 'chat') { + const status = await getChatPinnedStatusById(localStorage.token, id); - if (status) { - const res = await toggleChatPinnedStatusById(localStorage.token, id); + if (status) { + const res = await toggleChatPinnedStatusById(localStorage.token, id); - if (res) { - await pinnedChats.set(await getPinnedChatList(localStorage.token)); - initChatList(); + if (res) { + await pinnedChats.set(await getPinnedChatList(localStorage.token)); + initChatList(); + } } } }} @@ -619,7 +695,8 @@ {/if} { @@ -636,6 +713,10 @@ showDeleteConfirm = true; } }} + on:change={async () => { + await pinnedChats.set(await getPinnedChatList(localStorage.token)); + initChatList(); + }} on:tag={(e) => { const { type, name } = e.detail; tagEventHandler(type, name, chat.id); diff --git a/src/lib/components/layout/Sidebar/ChatItem.svelte b/src/lib/components/layout/Sidebar/ChatItem.svelte index 4dbcb9331..03e7f10e1 100644 --- a/src/lib/components/layout/Sidebar/ChatItem.svelte +++ b/src/lib/components/layout/Sidebar/ChatItem.svelte @@ -33,8 +33,15 @@ import Tooltip from '$lib/components/common/Tooltip.svelte'; import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte'; import DragGhost from '$lib/components/common/DragGhost.svelte'; + import Check from '$lib/components/icons/Check.svelte'; + import XMark from '$lib/components/icons/XMark.svelte'; + import Document from '$lib/components/icons/Document.svelte'; + + export let className = 'pr-2'; + + export let id; + export let title; - export let chat; export let selected = false; export let shiftKey = false; @@ -43,7 +50,7 @@ let showShareChatModal = false; let confirmEdit = false; - let chatTitle = chat.title; + let chatTitle = title; const editChatTitle = async (id, title) => { if (title === '') { @@ -93,7 +100,7 @@ let itemElement; - let drag = false; + let dragged = false; let x = 0; let y = 0; @@ -108,11 +115,12 @@ event.dataTransfer.setData( 'text/plain', JSON.stringify({ - id: chat.id + type: 'chat', + id: id }) ); - drag = true; + dragged = true; itemElement.style.opacity = '0.5'; // Optional: Visual cue to show it's being dragged }; @@ -123,7 +131,7 @@ const onDragEnd = (event) => { itemElement.style.opacity = '1'; // Reset visual cue after drag - drag = false; + dragged = false; }; onMount(() => { @@ -146,24 +154,26 @@ }); - + -{#if drag && x && y} +{#if dragged && x && y} -
-
+
+
+
- {chat.title} + {title}
{/if} -
+
{#if confirmEdit}
{:else} { dispatch('select'); @@ -191,7 +202,7 @@ } }} on:dblclick={() => { - chatTitle = chat.title; + chatTitle = title; confirmEdit = true; }} on:mouseenter={(e) => { @@ -205,7 +216,7 @@ >
- {chat.title} + {title}
@@ -214,12 +225,14 @@
{ @@ -230,28 +243,19 @@ }} > {#if confirmEdit} -
+
@@ -263,16 +267,7 @@ chatTitle = ''; }} > - - - +
@@ -282,7 +277,7 @@ - {#if chat.id === $chatId} + {#if id === $chatId} +
+ +
+ {#if folders[folderId].childrenIds || folders[folderId].items?.chat_ids} +
+ {#if folders[folderId]?.childrenIds} + {#each folders[folderId]?.childrenIds as folderId (folderId)} + + {/each} + {/if} + + {#if folders[folderId].items?.chat_ids} + {#each folder.items.chat_ids as chatId (chatId)} + {chatId} + {/each} + {/if} +
+ {/if} +
+ +