refac
This commit is contained in:
1 parent
c783fd30f2
commit
45fcf272ef
11 files changed
+474
-66
No files matched your search
@@ -234,3 +234,89 @@ export const deleteFolderById = async (token: string, id: string, deleteContents
|
|||||||
|
|
||||||
return res;
|
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 chat = null;
|
||||||
let tags = [];
|
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 chatTasks = [];
|
||||||
|
|
||||||
let history = {
|
let history = {
|
||||||
@@ -3103,6 +3106,7 @@
|
|||||||
<Messages
|
<Messages
|
||||||
bind:this={messagesRef}
|
bind:this={messagesRef}
|
||||||
chatId={$chatId}
|
chatId={$chatId}
|
||||||
|
{readOnly}
|
||||||
bind:history
|
bind:history
|
||||||
bind:autoScroll
|
bind:autoScroll
|
||||||
bind:prompt
|
bind:prompt
|
||||||
@@ -3126,7 +3130,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</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
|
<MessageInput
|
||||||
bind:this={messageInput}
|
bind:this={messageInput}
|
||||||
{history}
|
{history}
|
||||||
@@ -3208,6 +3221,7 @@
|
|||||||
<!-- {$i18n.t('LLMs can make mistakes. Verify important information.')} -->
|
<!-- {$i18n.t('LLMs can make mistakes. Verify important information.')} -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex items-center h-full">
|
<div class="flex items-center h-full">
|
||||||
<Placeholder
|
<Placeholder
|
||||||
|
|||||||
@@ -4,12 +4,15 @@
|
|||||||
|
|
||||||
const i18n: Writable<any> = getContext('i18n');
|
const i18n: Writable<any> = getContext('i18n');
|
||||||
|
|
||||||
|
import { user } from '$lib/stores';
|
||||||
|
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
import ChatList from './ChatList.svelte';
|
import ChatList from './ChatList.svelte';
|
||||||
import FolderKnowledge from './FolderKnowledge.svelte';
|
import FolderKnowledge from './FolderKnowledge.svelte';
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import { getChatListByFolderId } from '$lib/apis/chats';
|
import { getChatListByFolderId } from '$lib/apis/chats';
|
||||||
|
import { getSharedFolderChats } from '$lib/apis/folders';
|
||||||
|
|
||||||
export let folder: any = null;
|
export let folder: any = null;
|
||||||
|
|
||||||
@@ -22,24 +25,8 @@
|
|||||||
let allChatsLoaded = false;
|
let allChatsLoaded = false;
|
||||||
|
|
||||||
const loadChats = async () => {
|
const loadChats = async () => {
|
||||||
chatListLoading = true;
|
// getSharedFolderChats returns all users' chats in one shot; no pagination
|
||||||
|
allChatsLoaded = 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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const setChatList = async () => {
|
const setChatList = async () => {
|
||||||
@@ -49,12 +36,21 @@
|
|||||||
chatListLoading = false;
|
chatListLoading = false;
|
||||||
|
|
||||||
if (folder && folder.id) {
|
if (folder && folder.id) {
|
||||||
const res = await getChatListByFolderId(localStorage.token, folder.id, page);
|
// Always use the shared folder endpoint so owners also see
|
||||||
|
// chats created by users who have write access to this folder.
|
||||||
if (res) {
|
const res = await getSharedFolderChats(localStorage.token, folder.id).catch(
|
||||||
chats = res;
|
(error) => {
|
||||||
|
console.error(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (res && res.chats) {
|
||||||
|
chats = res.chats;
|
||||||
|
allChatsLoaded = true;
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
chats = [];
|
chats = [];
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
import { getChatsByFolderId } from '$lib/apis/chats';
|
import { getChatsByFolderId } from '$lib/apis/chats';
|
||||||
|
|
||||||
import FolderModal from '$lib/components/layout/Sidebar/Folders/FolderModal.svelte';
|
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 Folder from '$lib/components/icons/Folder.svelte';
|
||||||
import XMark from '$lib/components/icons/XMark.svelte';
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
|
|
||||||
let showFolderModal = false;
|
let showFolderModal = false;
|
||||||
let showCreateSubFolderModal = false;
|
let showCreateSubFolderModal = false;
|
||||||
|
let showShareModal = false;
|
||||||
let showDeleteConfirm = false;
|
let showDeleteConfirm = false;
|
||||||
let deleteFolderContents = true;
|
let deleteFolderContents = true;
|
||||||
|
|
||||||
@@ -173,6 +175,8 @@
|
|||||||
onSubmit={createSubFolderHandler}
|
onSubmit={createSubFolderHandler}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FolderShareModal bind:show={showShareModal} {folder} />
|
||||||
|
|
||||||
<DeleteConfirmDialog
|
<DeleteConfirmDialog
|
||||||
bind:show={showDeleteConfirm}
|
bind:show={showDeleteConfirm}
|
||||||
title={$i18n.t('Delete folder?')}
|
title={$i18n.t('Delete folder?')}
|
||||||
@@ -232,6 +236,9 @@
|
|||||||
onEdit={() => {
|
onEdit={() => {
|
||||||
showFolderModal = true;
|
showFolderModal = true;
|
||||||
}}
|
}}
|
||||||
|
onShare={() => {
|
||||||
|
showShareModal = true;
|
||||||
|
}}
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
showDeleteConfirm = true;
|
showDeleteConfirm = true;
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
deleteAllChats,
|
deleteAllChats,
|
||||||
getChatListBySearchText
|
getChatListBySearchText
|
||||||
} from '$lib/apis/chats';
|
} 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 { createNewNote, getPinnedNoteList, toggleNotePinnedStatusById } from '$lib/apis/notes';
|
||||||
import { updateUserSettings } from '$lib/apis/users';
|
import { updateUserSettings } from '$lib/apis/users';
|
||||||
import { checkActiveChats } from '$lib/apis/tasks';
|
import { checkActiveChats } from '$lib/apis/tasks';
|
||||||
@@ -61,6 +61,7 @@
|
|||||||
import Folder from '../common/Folder.svelte';
|
import Folder from '../common/Folder.svelte';
|
||||||
import Tooltip from '../common/Tooltip.svelte';
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
import Folders from './Sidebar/Folders.svelte';
|
import Folders from './Sidebar/Folders.svelte';
|
||||||
|
import SharedFolderItem from './Sidebar/SharedFolderItem.svelte';
|
||||||
import { getChannels, createNewChannel } from '$lib/apis/channels';
|
import { getChannels, createNewChannel } from '$lib/apis/channels';
|
||||||
import ChannelModal from './Sidebar/ChannelModal.svelte';
|
import ChannelModal from './Sidebar/ChannelModal.svelte';
|
||||||
import ChannelItem from './Sidebar/ChannelItem.svelte';
|
import ChannelItem from './Sidebar/ChannelItem.svelte';
|
||||||
@@ -98,12 +99,15 @@
|
|||||||
let showPinnedNotes = false;
|
let showPinnedNotes = false;
|
||||||
let showChannels = false;
|
let showChannels = false;
|
||||||
let showFolders = false;
|
let showFolders = false;
|
||||||
|
let showSharedFolders = false;
|
||||||
|
|
||||||
let folders = {};
|
let folders = {};
|
||||||
let folderRegistry = {};
|
let folderRegistry = {};
|
||||||
|
|
||||||
let newFolderId = null;
|
let newFolderId = null;
|
||||||
|
|
||||||
|
let sharedFolders: any[] = [];
|
||||||
|
|
||||||
$: pinnedItems = $settings?.pinnedMenuItems ?? DEFAULT_PINNED_ITEMS;
|
$: pinnedItems = $settings?.pinnedMenuItems ?? DEFAULT_PINNED_ITEMS;
|
||||||
|
|
||||||
const isMenuItemVisible = (id) => {
|
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 }) => {
|
const createFolder = async ({ name, data, parent_id }) => {
|
||||||
@@ -291,6 +320,7 @@
|
|||||||
scrollPaginationEnabled.set(false);
|
scrollPaginationEnabled.set(false);
|
||||||
|
|
||||||
initFolders();
|
initFolders();
|
||||||
|
initSharedFolders();
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
await (async () => {
|
await (async () => {
|
||||||
console.log('Init tags');
|
console.log('Init tags');
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
|
|
||||||
export let selected = false;
|
export let selected = false;
|
||||||
export let shiftKey = false;
|
export let shiftKey = false;
|
||||||
|
export let readonly = false;
|
||||||
|
|
||||||
export let onDragEnd = () => {};
|
export let onDragEnd = () => {};
|
||||||
|
|
||||||
@@ -445,7 +446,7 @@
|
|||||||
id="sidebar-chat-group"
|
id="sidebar-chat-group"
|
||||||
bind:this={itemElement}
|
bind:this={itemElement}
|
||||||
class=" w-full {className} relative group"
|
class=" w-full {className} relative group"
|
||||||
draggable={!confirmEdit}
|
draggable={!confirmEdit && !readonly}
|
||||||
>
|
>
|
||||||
{#if confirmEdit}
|
{#if confirmEdit}
|
||||||
<div
|
<div
|
||||||
@@ -509,6 +510,7 @@
|
|||||||
lastReadAt = Date.now() / 1000;
|
lastReadAt = Date.now() / 1000;
|
||||||
}}
|
}}
|
||||||
on:dblclick={async (e) => {
|
on:dblclick={async (e) => {
|
||||||
|
if (readonly) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
@@ -557,6 +559,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
{#if !readonly}
|
||||||
<div
|
<div
|
||||||
id="sidebar-chat-item-menu"
|
id="sidebar-chat-item-menu"
|
||||||
class="
|
class="
|
||||||
@@ -691,4 +694,5 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { getContext, createEventDispatcher } from 'svelte';
|
||||||
|
const i18n = getContext('i18n');
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
import RecursiveFolder from './RecursiveFolder.svelte';
|
import RecursiveFolder from './RecursiveFolder.svelte';
|
||||||
@@ -12,16 +13,29 @@
|
|||||||
|
|
||||||
export let onDelete = (folderId) => {};
|
export let onDelete = (folderId) => {};
|
||||||
|
|
||||||
let folderList = [];
|
let ownedList = [];
|
||||||
// Get the list of folders that have no parent, sorted by name alphabetically
|
let sharedList = [];
|
||||||
$: folderList = Object.keys(folders)
|
|
||||||
.filter((key) => folders[key].parent_id === null)
|
$: {
|
||||||
.sort((a, b) =>
|
const rootKeys = Object.keys(folders)
|
||||||
folders[a].name.localeCompare(folders[b].name, undefined, {
|
.filter((key) => {
|
||||||
numeric: true,
|
const f = folders[key];
|
||||||
sensitivity: 'base'
|
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) => {
|
const onItemMove = (e) => {
|
||||||
if (e.originFolderId) {
|
if (e.originFolderId) {
|
||||||
@@ -40,7 +54,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each folderList as folderId (folderId)}
|
{#each ownedList as folderId (folderId)}
|
||||||
<RecursiveFolder
|
<RecursiveFolder
|
||||||
className=""
|
className=""
|
||||||
bind:folderRegistry
|
bind:folderRegistry
|
||||||
@@ -60,3 +74,29 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/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 align: 'start' | 'end' = 'start';
|
||||||
export let onEdit = () => {};
|
export let onEdit = () => {};
|
||||||
export let onExport = () => {};
|
export let onExport = () => {};
|
||||||
|
export let onShare = () => {};
|
||||||
export let onDelete = () => {};
|
export let onDelete = () => {};
|
||||||
export let onCreateSubFolder = () => {};
|
export let onCreateSubFolder = () => {};
|
||||||
|
|
||||||
@@ -76,6 +77,20 @@
|
|||||||
<div class="flex items-center">{$i18n.t('Export')}</div>
|
<div class="flex items-center">{$i18n.t('Export')}</div>
|
||||||
</button>
|
</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
|
<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"
|
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={() => {
|
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,
|
updateFolderById,
|
||||||
updateFolderParentIdById,
|
updateFolderParentIdById,
|
||||||
getFolderById,
|
getFolderById,
|
||||||
createNewFolder
|
createNewFolder,
|
||||||
|
getSharedFolderChats
|
||||||
} from '$lib/apis/folders';
|
} from '$lib/apis/folders';
|
||||||
import {
|
import {
|
||||||
getChatById,
|
getChatById,
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
|
|
||||||
import ChatItem from './ChatItem.svelte';
|
import ChatItem from './ChatItem.svelte';
|
||||||
import FolderMenu from './Folders/FolderMenu.svelte';
|
import FolderMenu from './Folders/FolderMenu.svelte';
|
||||||
|
import FolderShareModal from './Folders/FolderShareModal.svelte';
|
||||||
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||||
import FolderModal from './Folders/FolderModal.svelte';
|
import FolderModal from './Folders/FolderModal.svelte';
|
||||||
import Emoji from '$lib/components/common/Emoji.svelte';
|
import Emoji from '$lib/components/common/Emoji.svelte';
|
||||||
@@ -63,6 +65,7 @@
|
|||||||
let folderElement;
|
let folderElement;
|
||||||
|
|
||||||
let showFolderModal = false;
|
let showFolderModal = false;
|
||||||
|
let showShareModal = false;
|
||||||
let edit = false;
|
let edit = false;
|
||||||
|
|
||||||
let showCreateSubFolderModal = false;
|
let showCreateSubFolderModal = false;
|
||||||
@@ -78,7 +81,7 @@
|
|||||||
const onDragOver = (e) => {
|
const onDragOver = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (dragged || parentDragged) {
|
if (dragged || parentDragged || folders[folderId]?.shared) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
draggedOver = true;
|
draggedOver = true;
|
||||||
@@ -374,10 +377,18 @@
|
|||||||
export const setFolderItems = async () => {
|
export const setFolderItems = async () => {
|
||||||
await tick();
|
await tick();
|
||||||
if (open) {
|
if (open) {
|
||||||
chats = await getChatListByFolderId(localStorage.token, folderId).catch((error) => {
|
// Always use getSharedFolderChats so owners also see chats
|
||||||
toast.error(`${error}`);
|
// created by users who have write access to this folder.
|
||||||
return [];
|
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 {
|
} else {
|
||||||
chats = null;
|
chats = null;
|
||||||
}
|
}
|
||||||
@@ -478,6 +489,8 @@
|
|||||||
onSubmit={createSubFolderHandler}
|
onSubmit={createSubFolderHandler}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FolderShareModal bind:show={showShareModal} folder={folders[folderId]} />
|
||||||
|
|
||||||
{#if dragged && x && y}
|
{#if dragged && x && y}
|
||||||
<DragGhost {x} {y}>
|
<DragGhost {x} {y}>
|
||||||
<div class=" bg-black/80 backdrop-blur-2xl px-2 py-1 rounded-lg w-fit max-w-40">
|
<div class=" bg-black/80 backdrop-blur-2xl px-2 py-1 rounded-lg w-fit max-w-40">
|
||||||
@@ -491,7 +504,7 @@
|
|||||||
</DragGhost>
|
</DragGhost>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div bind:this={folderElement} class="relative {className}" draggable="true">
|
<div bind:this={folderElement} class="relative {className}" draggable="{!folders[folderId]?.shared}">
|
||||||
{#if draggedOver}
|
{#if draggedOver}
|
||||||
<div
|
<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"
|
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'
|
? 'bg-gray-100 dark:bg-gray-900 selected'
|
||||||
: ''}"
|
: ''}"
|
||||||
on:dblclick={(e) => {
|
on:dblclick={(e) => {
|
||||||
|
if (folders[folderId]?.shared) return;
|
||||||
if (clickTimer) {
|
if (clickTimer) {
|
||||||
clearTimeout(clickTimer); // cancel the single-click action
|
clearTimeout(clickTimer); // cancel the single-click action
|
||||||
clickTimer = null;
|
clickTimer = null;
|
||||||
@@ -614,29 +628,34 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
{#if !folders[folderId]?.shared}
|
||||||
class="absolute z-10 right-2 invisible group-hover:visible self-center flex items-center dark:text-gray-300"
|
<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;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div class="p-1 dark:hover:bg-gray-850 rounded-lg touch-auto">
|
<FolderMenu
|
||||||
<EllipsisHorizontal className="size-4" strokeWidth="2.5" />
|
onEdit={() => {
|
||||||
</div>
|
showFolderModal = true;
|
||||||
</FolderMenu>
|
}}
|
||||||
</button>
|
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>
|
||||||
</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