diff --git a/src/lib/apis/folders/index.ts b/src/lib/apis/folders/index.ts index 5ac37a8251..cd63d4d2e0 100644 --- a/src/lib/apis/folders/index.ts +++ b/src/lib/apis/folders/index.ts @@ -234,3 +234,89 @@ export const deleteFolderById = async (token: string, id: string, deleteContents return res; }; + +export const updateFolderAccessById = async ( + token: string, + id: string, + accessGrants: any[] +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}/access/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ access_grants: accessGrants }) + }) + .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 getSharedFolders = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/folders/shared`, { + 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(); + }) + .catch((err) => { + error = err.detail; + return []; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getSharedFolderChats = async (token: string, folderId: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${folderId}/shared/chats`, { + 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(); + }) + .catch((err) => { + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 69652dbe41..639a71ecfd 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -169,6 +169,9 @@ let chat = null; let tags = []; + // Read-only when viewing someone else's chat (e.g. via shared folder access) + $: readOnly = chat != null && chat.user_id !== $user?.id; + let chatTasks = []; let history = { @@ -3103,6 +3106,7 @@ -
+ {#if readOnly} +
+
+ {$i18n.t('Read only')} +
+
+ {:else} +
+ {/if} {:else}
= getContext('i18n'); + import { user } from '$lib/stores'; + import { fade } from 'svelte/transition'; import ChatList from './ChatList.svelte'; import FolderKnowledge from './FolderKnowledge.svelte'; import Spinner from '$lib/components/common/Spinner.svelte'; import { getChatListByFolderId } from '$lib/apis/chats'; + import { getSharedFolderChats } from '$lib/apis/folders'; export let folder: any = null; @@ -22,24 +25,8 @@ let allChatsLoaded = false; const loadChats = async () => { - chatListLoading = true; - - page += 1; - - let newChatList: any[] = []; - - newChatList = await getChatListByFolderId(localStorage.token, folder.id, page).catch( - (error) => { - console.error(error); - return []; - } - ); - - // once the bottom of the list has been reached (no results) there is no need to continue querying - allChatsLoaded = newChatList.length === 0; - chats = [...(chats || []), ...(newChatList || [])]; - - chatListLoading = false; + // getSharedFolderChats returns all users' chats in one shot; no pagination + allChatsLoaded = true; }; const setChatList = async () => { @@ -49,12 +36,21 @@ chatListLoading = false; if (folder && folder.id) { - const res = await getChatListByFolderId(localStorage.token, folder.id, page); - - if (res) { - chats = res; + // Always use the shared folder endpoint so owners also see + // chats created by users who have write access to this folder. + const res = await getSharedFolderChats(localStorage.token, folder.id).catch( + (error) => { + console.error(error); + return null; + } + ); + if (res && res.chats) { + chats = res.chats; + allChatsLoaded = true; } else { - chats = []; + // Fallback to regular API (e.g. if user has no shared access) + const fallback = await getChatListByFolderId(localStorage.token, folder.id, page).catch(() => []); + chats = fallback || []; } } else { chats = []; diff --git a/src/lib/components/chat/Placeholder/FolderTitle.svelte b/src/lib/components/chat/Placeholder/FolderTitle.svelte index c48654accd..7495f5bc09 100644 --- a/src/lib/components/chat/Placeholder/FolderTitle.svelte +++ b/src/lib/components/chat/Placeholder/FolderTitle.svelte @@ -20,6 +20,7 @@ import { getChatsByFolderId } from '$lib/apis/chats'; import FolderModal from '$lib/components/layout/Sidebar/Folders/FolderModal.svelte'; + import FolderShareModal from '$lib/components/layout/Sidebar/Folders/FolderShareModal.svelte'; import Folder from '$lib/components/icons/Folder.svelte'; import XMark from '$lib/components/icons/XMark.svelte'; @@ -36,6 +37,7 @@ let showFolderModal = false; let showCreateSubFolderModal = false; + let showShareModal = false; let showDeleteConfirm = false; let deleteFolderContents = true; @@ -173,6 +175,8 @@ onSubmit={createSubFolderHandler} /> + + { showFolderModal = true; }} + onShare={() => { + showShareModal = true; + }} onDelete={() => { showDeleteConfirm = true; }} diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index f2329273aa..2f91772191 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -46,7 +46,7 @@ deleteAllChats, getChatListBySearchText } from '$lib/apis/chats'; - import { createNewFolder, getFolders, updateFolderParentIdById } from '$lib/apis/folders'; + import { createNewFolder, getFolders, getSharedFolders, updateFolderParentIdById } from '$lib/apis/folders'; import { createNewNote, getPinnedNoteList, toggleNotePinnedStatusById } from '$lib/apis/notes'; import { updateUserSettings } from '$lib/apis/users'; import { checkActiveChats } from '$lib/apis/tasks'; @@ -61,6 +61,7 @@ import Folder from '../common/Folder.svelte'; import Tooltip from '../common/Tooltip.svelte'; import Folders from './Sidebar/Folders.svelte'; + import SharedFolderItem from './Sidebar/SharedFolderItem.svelte'; import { getChannels, createNewChannel } from '$lib/apis/channels'; import ChannelModal from './Sidebar/ChannelModal.svelte'; import ChannelItem from './Sidebar/ChannelItem.svelte'; @@ -98,12 +99,15 @@ let showPinnedNotes = false; let showChannels = false; let showFolders = false; + let showSharedFolders = false; let folders = {}; let folderRegistry = {}; let newFolderId = null; + let sharedFolders: any[] = []; + $: pinnedItems = $settings?.pinnedMenuItems ?? DEFAULT_PINNED_ITEMS; const isMenuItemVisible = (id) => { @@ -215,6 +219,31 @@ }); } } + + // Merge shared folders into the same structure + try { + sharedFolders = await getSharedFolders(localStorage.token); + } catch (e) { + sharedFolders = []; + } + + for (const sf of sharedFolders) { + if (folders[sf.id]) continue; // Already owned by user + folders[sf.id] = { ...sf, shared: true }; + } + + // Build parent-child relationships for shared folders + for (const sf of sharedFolders) { + if (folders[sf.id]?.shared && sf.parent_id && folders[sf.parent_id]) { + folders[sf.parent_id].childrenIds = folders[sf.parent_id].childrenIds + ? [...new Set([...folders[sf.parent_id].childrenIds, sf.id])] + : [sf.id]; + } + } + }; + + const initSharedFolders = async () => { + await initFolders(); }; const createFolder = async ({ name, data, parent_id }) => { @@ -291,6 +320,7 @@ scrollPaginationEnabled.set(false); initFolders(); + initSharedFolders(); await Promise.all([ await (async () => { console.log('Init tags'); diff --git a/src/lib/components/layout/Sidebar/ChatItem.svelte b/src/lib/components/layout/Sidebar/ChatItem.svelte index a0ebfb484c..d89d24a8ac 100644 --- a/src/lib/components/layout/Sidebar/ChatItem.svelte +++ b/src/lib/components/layout/Sidebar/ChatItem.svelte @@ -63,6 +63,7 @@ export let selected = false; export let shiftKey = false; + export let readonly = false; export let onDragEnd = () => {}; @@ -445,7 +446,7 @@ id="sidebar-chat-group" bind:this={itemElement} class=" w-full {className} relative group" - draggable={!confirmEdit} + draggable={!confirmEdit && !readonly} > {#if confirmEdit}
{ + if (readonly) return; e.preventDefault(); e.stopPropagation(); @@ -557,6 +559,7 @@ {/if} + {#if !readonly} + +
+ +
+
+ diff --git a/src/lib/components/layout/Sidebar/RecursiveFolder.svelte b/src/lib/components/layout/Sidebar/RecursiveFolder.svelte index fa80719d5a..459175e5a9 100644 --- a/src/lib/components/layout/Sidebar/RecursiveFolder.svelte +++ b/src/lib/components/layout/Sidebar/RecursiveFolder.svelte @@ -19,7 +19,8 @@ updateFolderById, updateFolderParentIdById, getFolderById, - createNewFolder + createNewFolder, + getSharedFolderChats } from '$lib/apis/folders'; import { getChatById, @@ -39,6 +40,7 @@ import ChatItem from './ChatItem.svelte'; import FolderMenu from './Folders/FolderMenu.svelte'; + import FolderShareModal from './Folders/FolderShareModal.svelte'; import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; import FolderModal from './Folders/FolderModal.svelte'; import Emoji from '$lib/components/common/Emoji.svelte'; @@ -63,6 +65,7 @@ let folderElement; let showFolderModal = false; + let showShareModal = false; let edit = false; let showCreateSubFolderModal = false; @@ -78,7 +81,7 @@ const onDragOver = (e) => { e.preventDefault(); e.stopPropagation(); - if (dragged || parentDragged) { + if (dragged || parentDragged || folders[folderId]?.shared) { return; } draggedOver = true; @@ -374,10 +377,18 @@ export const setFolderItems = async () => { await tick(); if (open) { - chats = await getChatListByFolderId(localStorage.token, folderId).catch((error) => { - toast.error(`${error}`); - return []; - }); + // Always use getSharedFolderChats so owners also see chats + // created by users who have write access to this folder. + try { + const res = await getSharedFolderChats(localStorage.token, folderId); + chats = res?.chats ?? []; + } catch (error) { + // Fallback to regular API + chats = await getChatListByFolderId(localStorage.token, folderId).catch((error) => { + toast.error(`${error}`); + return []; + }); + } } else { chats = null; } @@ -478,6 +489,8 @@ onSubmit={createSubFolderHandler} /> + + {#if dragged && x && y}
@@ -491,7 +504,7 @@ {/if} -
+
{#if draggedOver}
{ + if (folders[folderId]?.shared) return; if (clickTimer) { clearTimeout(clickTimer); // cancel the single-click action clickTimer = null; @@ -614,29 +628,34 @@ {/if}
- + { + showFolderModal = true; + }} + onShare={() => { + showShareModal = true; + }} + onDelete={() => { + showDeleteConfirm = true; + }} + onExport={() => { + exportHandler(); + }} + onCreateSubFolder={() => { + createSubFolderParentId = folderId; + showCreateSubFolderModal = true; + }} + > +
+ +
+
+ + {/if}
diff --git a/src/lib/components/layout/Sidebar/SharedFolderItem.svelte b/src/lib/components/layout/Sidebar/SharedFolderItem.svelte new file mode 100644 index 0000000000..b7000695cb --- /dev/null +++ b/src/lib/components/layout/Sidebar/SharedFolderItem.svelte @@ -0,0 +1,130 @@ + + +
+ +
+ +
+ + {#if expanded} +
+ {#if loading} +
+ + {$i18n.t('Loading...')} +
+ {:else} + {#each chats as chat (chat.id)} + + {/each} + + {#each children as child (child.id)} + + {/each} + + {#if chats.length === 0 && children.length === 0} +
+ {$i18n.t('Empty')} +
+ {/if} + {/if} +
+ {/if} +