mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-23 22:14:58 -06:00
refac
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 @@
|
||||
<Messages
|
||||
bind:this={messagesRef}
|
||||
chatId={$chatId}
|
||||
{readOnly}
|
||||
bind:history
|
||||
bind:autoScroll
|
||||
bind:prompt
|
||||
@@ -3126,7 +3130,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" pb-2 {dragged ? 'z-0' : 'z-10'}">
|
||||
{#if readOnly}
|
||||
<div class="pb-6 z-10">
|
||||
<div
|
||||
class="text-xs text-gray-400 dark:text-gray-500 text-center"
|
||||
>
|
||||
{$i18n.t('Read only')}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class=" pb-2 {dragged ? 'z-0' : 'z-10'}">
|
||||
<MessageInput
|
||||
bind:this={messageInput}
|
||||
{history}
|
||||
@@ -3208,6 +3221,7 @@
|
||||
<!-- {$i18n.t('LLMs can make mistakes. Verify important information.')} -->
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex items-center h-full">
|
||||
<Placeholder
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
|
||||
const i18n: Writable<any> = 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 = [];
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
<FolderShareModal bind:show={showShareModal} {folder} />
|
||||
|
||||
<DeleteConfirmDialog
|
||||
bind:show={showDeleteConfirm}
|
||||
title={$i18n.t('Delete folder?')}
|
||||
@@ -232,6 +236,9 @@
|
||||
onEdit={() => {
|
||||
showFolderModal = true;
|
||||
}}
|
||||
onShare={() => {
|
||||
showShareModal = true;
|
||||
}}
|
||||
onDelete={() => {
|
||||
showDeleteConfirm = true;
|
||||
}}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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}
|
||||
<div
|
||||
@@ -509,6 +510,7 @@
|
||||
lastReadAt = Date.now() / 1000;
|
||||
}}
|
||||
on:dblclick={async (e) => {
|
||||
if (readonly) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -557,6 +559,7 @@
|
||||
{/if}
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
{#if !readonly}
|
||||
<div
|
||||
id="sidebar-chat-item-menu"
|
||||
class="
|
||||
@@ -691,4 +694,5 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { getContext, createEventDispatcher } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import RecursiveFolder from './RecursiveFolder.svelte';
|
||||
@@ -12,16 +13,29 @@
|
||||
|
||||
export let onDelete = (folderId) => {};
|
||||
|
||||
let folderList = [];
|
||||
// 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, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'base'
|
||||
let ownedList = [];
|
||||
let sharedList = [];
|
||||
|
||||
$: {
|
||||
const rootKeys = Object.keys(folders)
|
||||
.filter((key) => {
|
||||
const f = folders[key];
|
||||
if (!f.name) return false;
|
||||
// Root folder: no parent, or shared folder whose parent isn't in our folders
|
||||
if (f.shared) {
|
||||
return !f.parent_id || !folders[f.parent_id];
|
||||
}
|
||||
return f.parent_id === null;
|
||||
})
|
||||
);
|
||||
.sort((a, b) =>
|
||||
(folders[a].name ?? '').localeCompare(folders[b].name ?? '', undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'base'
|
||||
})
|
||||
);
|
||||
ownedList = rootKeys.filter((key) => !folders[key].shared);
|
||||
sharedList = rootKeys.filter((key) => folders[key].shared);
|
||||
}
|
||||
|
||||
const onItemMove = (e) => {
|
||||
if (e.originFolderId) {
|
||||
@@ -40,7 +54,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each folderList as folderId (folderId)}
|
||||
{#each ownedList as folderId (folderId)}
|
||||
<RecursiveFolder
|
||||
className=""
|
||||
bind:folderRegistry
|
||||
@@ -60,3 +74,29 @@
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
{#if sharedList.length > 0}
|
||||
<div class="w-full pl-2.5 text-[11px] text-gray-400 dark:text-gray-600 pt-2 pb-0.5">
|
||||
{$i18n.t('Shared')}
|
||||
</div>
|
||||
{#each sharedList as folderId (folderId)}
|
||||
<RecursiveFolder
|
||||
className=""
|
||||
bind:folderRegistry
|
||||
{folders}
|
||||
{folderId}
|
||||
{shiftKey}
|
||||
{onDelete}
|
||||
{onItemMove}
|
||||
on:import={(e) => {
|
||||
dispatch('import', e.detail);
|
||||
}}
|
||||
on:update={(e) => {
|
||||
dispatch('update', e.detail);
|
||||
}}
|
||||
on:change={(e) => {
|
||||
dispatch('change', e.detail);
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
export let align: 'start' | 'end' = 'start';
|
||||
export let onEdit = () => {};
|
||||
export let onExport = () => {};
|
||||
export let onShare = () => {};
|
||||
export let onDelete = () => {};
|
||||
export let onCreateSubFolder = () => {};
|
||||
|
||||
@@ -76,6 +77,20 @@
|
||||
<div class="flex items-center">{$i18n.t('Export')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm select-none cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl w-full"
|
||||
on:click={() => {
|
||||
onShare();
|
||||
}}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7.217 10.907a2.25 2.25 0 1 0 0 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186 9.566-5.314m-9.566 7.5 9.566 5.314m0 0a2.25 2.25 0 1 0 3.935 2.186 2.25 2.25 0 0 0-3.935-2.186Zm0-12.814a2.25 2.25 0 1 0 3.935-2.186 2.25 2.25 0 0 0-3.935 2.186Z" />
|
||||
</svg>
|
||||
<div class="flex items-center">{$i18n.t('Share')}</div>
|
||||
</button>
|
||||
|
||||
<hr class="border-gray-50/30 dark:border-gray-800/30 my-1" />
|
||||
|
||||
<button
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm select-none cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl w-full"
|
||||
on:click={() => {
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import AccessControl from '$lib/components/workspace/common/AccessControl.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import { updateFolderAccessById } from '$lib/apis/folders';
|
||||
import { user } from '$lib/stores';
|
||||
|
||||
type AccessGrant = {
|
||||
id?: string;
|
||||
principal_type: 'user' | 'group';
|
||||
principal_id: string;
|
||||
permission: 'read' | 'write';
|
||||
};
|
||||
|
||||
export let show = false;
|
||||
export let folder: any = null;
|
||||
|
||||
let accessGrants: AccessGrant[] = [];
|
||||
|
||||
$: if (folder) {
|
||||
accessGrants = folder.access_grants ?? [];
|
||||
}
|
||||
|
||||
const handleAccessChange = async () => {
|
||||
if (!folder) return;
|
||||
try {
|
||||
const res = await updateFolderAccessById(localStorage.token, folder.id, accessGrants);
|
||||
if (res) {
|
||||
accessGrants = res.access_grants ?? accessGrants;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to update folder access', e);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal size="sm" bind:show>
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-100 px-5 pt-3 pb-1">
|
||||
<div class=" text-lg font-medium self-center font-primary">
|
||||
{$i18n.t('Share')}: {folder?.name ?? ''}
|
||||
</div>
|
||||
<button
|
||||
class="self-center"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="w-full px-5 pb-4 dark:text-white">
|
||||
<AccessControl
|
||||
bind:accessGrants
|
||||
onChange={handleAccessChange}
|
||||
accessRoles={['read', 'write']}
|
||||
share={$user?.role === 'admin' || $user?.permissions?.sharing?.folders}
|
||||
sharePublic={false}
|
||||
shareUsers={$user?.role === 'admin' || $user?.permissions?.access_grants?.allow_users}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
<FolderShareModal bind:show={showShareModal} folder={folders[folderId]} />
|
||||
|
||||
{#if dragged && x && y}
|
||||
<DragGhost {x} {y}>
|
||||
<div class=" bg-black/80 backdrop-blur-2xl px-2 py-1 rounded-lg w-fit max-w-40">
|
||||
@@ -491,7 +504,7 @@
|
||||
</DragGhost>
|
||||
{/if}
|
||||
|
||||
<div bind:this={folderElement} class="relative {className}" draggable="true">
|
||||
<div bind:this={folderElement} class="relative {className}" draggable="{!folders[folderId]?.shared}">
|
||||
{#if draggedOver}
|
||||
<div
|
||||
class="absolute top-0 left-0 w-full h-full rounded-xs bg-gray-100/50 dark:bg-gray-700/20 bg-opacity-50 dark:bg-opacity-10 z-50 pointer-events-none touch-none"
|
||||
@@ -515,6 +528,7 @@
|
||||
? 'bg-gray-100 dark:bg-gray-900 selected'
|
||||
: ''}"
|
||||
on:dblclick={(e) => {
|
||||
if (folders[folderId]?.shared) return;
|
||||
if (clickTimer) {
|
||||
clearTimeout(clickTimer); // cancel the single-click action
|
||||
clickTimer = null;
|
||||
@@ -614,29 +628,34 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="absolute z-10 right-2 invisible group-hover:visible self-center flex items-center dark:text-gray-300"
|
||||
>
|
||||
<FolderMenu
|
||||
onEdit={() => {
|
||||
showFolderModal = true;
|
||||
}}
|
||||
onDelete={() => {
|
||||
showDeleteConfirm = true;
|
||||
}}
|
||||
onExport={() => {
|
||||
exportHandler();
|
||||
}}
|
||||
onCreateSubFolder={() => {
|
||||
createSubFolderParentId = folderId;
|
||||
showCreateSubFolderModal = true;
|
||||
}}
|
||||
{#if !folders[folderId]?.shared}
|
||||
<button
|
||||
class="absolute z-10 right-2 invisible group-hover:visible self-center flex items-center dark:text-gray-300"
|
||||
>
|
||||
<div class="p-1 dark:hover:bg-gray-850 rounded-lg touch-auto">
|
||||
<EllipsisHorizontal className="size-4" strokeWidth="2.5" />
|
||||
</div>
|
||||
</FolderMenu>
|
||||
</button>
|
||||
<FolderMenu
|
||||
onEdit={() => {
|
||||
showFolderModal = true;
|
||||
}}
|
||||
onShare={() => {
|
||||
showShareModal = true;
|
||||
}}
|
||||
onDelete={() => {
|
||||
showDeleteConfirm = true;
|
||||
}}
|
||||
onExport={() => {
|
||||
exportHandler();
|
||||
}}
|
||||
onCreateSubFolder={() => {
|
||||
createSubFolderParentId = folderId;
|
||||
showCreateSubFolderModal = true;
|
||||
}}
|
||||
>
|
||||
<div class="p-1 dark:hover:bg-gray-850 rounded-lg touch-auto">
|
||||
<EllipsisHorizontal className="size-4" strokeWidth="2.5" />
|
||||
</div>
|
||||
</FolderMenu>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<script lang="ts">
|
||||
import { getContext, createEventDispatcher } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import { chatId, mobile, showSidebar } from '$lib/stores';
|
||||
import { getSharedFolderChats } from '$lib/apis/folders';
|
||||
|
||||
import ChatItem from './ChatItem.svelte';
|
||||
import Collapsible from '../../common/Collapsible.svelte';
|
||||
import ChevronDown from '../../icons/ChevronDown.svelte';
|
||||
import ChevronRight from '../../icons/ChevronRight.svelte';
|
||||
import FolderOpen from '$lib/components/icons/FolderOpen.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
|
||||
export let folder: any;
|
||||
export let allSharedFolders: any[] = [];
|
||||
export let className = '';
|
||||
|
||||
let expanded = false;
|
||||
let chats: any[] = [];
|
||||
let loading = false;
|
||||
let loaded = false;
|
||||
|
||||
$: children = allSharedFolders.filter((f) => f.parent_id === folder.id);
|
||||
$: permission = folder.permission || 'read';
|
||||
$: isWritable = permission === 'write';
|
||||
|
||||
const toggleExpand = async () => {
|
||||
expanded = !expanded;
|
||||
if (expanded && !loaded) {
|
||||
await loadChats();
|
||||
}
|
||||
};
|
||||
|
||||
const loadChats = async () => {
|
||||
loading = true;
|
||||
try {
|
||||
const res = await getSharedFolderChats(localStorage.token, folder.id);
|
||||
if (res) {
|
||||
chats = res.chats || [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load shared folder chats', e);
|
||||
chats = [];
|
||||
} finally {
|
||||
loading = false;
|
||||
loaded = true;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class={className}>
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="w-full group"
|
||||
>
|
||||
<button
|
||||
class="w-full py-1 flex items-center gap-1 text-xs text-left font-medium
|
||||
text-gray-600 dark:text-gray-400
|
||||
hover:text-gray-900 dark:hover:text-gray-200
|
||||
transition-colors duration-100 cursor-pointer"
|
||||
on:click={toggleExpand}
|
||||
>
|
||||
<div class="shrink-0">
|
||||
{#if expanded}
|
||||
<ChevronDown className="size-3" strokeWidth="2.5" />
|
||||
{:else}
|
||||
<ChevronRight className="size-3" strokeWidth="2.5" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<FolderOpen className="size-3.5 shrink-0" strokeWidth="2" />
|
||||
|
||||
<div class="flex-1 truncate">
|
||||
{folder.name}
|
||||
</div>
|
||||
|
||||
{#if folder.owner_name}
|
||||
<div class="shrink-0 text-[10px] text-gray-400 dark:text-gray-600 pr-1">
|
||||
{folder.owner_name}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !isWritable}
|
||||
<div class="shrink-0 text-[10px] text-gray-400 dark:text-gray-600" title="Read only">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-3">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if expanded}
|
||||
<div class="pl-3">
|
||||
{#if loading}
|
||||
<div class="flex items-center gap-1.5 py-1 px-2 text-xs text-gray-400">
|
||||
<Spinner className="size-3" />
|
||||
<span>{$i18n.t('Loading...')}</span>
|
||||
</div>
|
||||
{:else}
|
||||
{#each chats as chat (chat.id)}
|
||||
<ChatItem
|
||||
id={chat.id}
|
||||
title={chat.title}
|
||||
createdAt={chat.created_at}
|
||||
updatedAt={chat.updated_at}
|
||||
readonly={chat.readonly ?? !isWritable}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
{#each children as child (child.id)}
|
||||
<svelte:self
|
||||
folder={child}
|
||||
{allSharedFolders}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
{#if chats.length === 0 && children.length === 0}
|
||||
<div class="text-[11px] text-gray-400 dark:text-gray-600 py-1 px-2">
|
||||
{$i18n.t('Empty')}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user