mirror of
https://git.mirrors.martin98.com/https://github.com/open-webui/open-webui
synced 2025-08-13 23:35:53 +08:00
feat: folder menu
This commit is contained in:
parent
b402061546
commit
3578c85e39
@ -56,7 +56,7 @@ li p {
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
--tw-border-opacity: 1;
|
||||
background-color: rgba(217, 217, 227, 0.8);
|
||||
background-color: rgba(236, 236, 236, 0.8);
|
||||
border-color: rgba(255, 255, 255, var(--tw-border-opacity));
|
||||
border-radius: 9999px;
|
||||
border-width: 1px;
|
||||
@ -64,7 +64,7 @@ li p {
|
||||
|
||||
/* Dark theme scrollbar styles */
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(69, 69, 74, 0.8); /* Darker color for dark theme */
|
||||
background-color: rgba(33, 33, 33, 0.8); /* Darker color for dark theme */
|
||||
border-color: rgba(0, 0, 0, var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,8 @@
|
||||
export let className = '';
|
||||
export let buttonClassName = 'w-fit';
|
||||
export let title = null;
|
||||
|
||||
export let disabled = false;
|
||||
</script>
|
||||
|
||||
<div class={className}>
|
||||
@ -47,7 +49,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if open}
|
||||
{#if open && !disabled}
|
||||
<div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
|
||||
<slot name="content" />
|
||||
</div>
|
||||
|
@ -22,7 +22,7 @@
|
||||
|
||||
<slot name="content">
|
||||
<DropdownMenu.Content
|
||||
class="w-full max-w-[130px] rounded-lg px-1 py-1.5 border border-gray-700 z-50 bg-gray-850 text-white"
|
||||
class="w-full max-w-[130px] rounded-lg px-1 py-1.5 border border-gray-900 z-50 bg-gray-850 text-white"
|
||||
sideOffset={8}
|
||||
side="bottom"
|
||||
align="start"
|
||||
|
@ -622,7 +622,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (chat.pinned) {
|
||||
if (!chat.pinned) {
|
||||
const res = await toggleChatPinnedStatusById(localStorage.token, id);
|
||||
|
||||
if (res) {
|
||||
|
@ -37,7 +37,7 @@
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import Document from '$lib/components/icons/Document.svelte';
|
||||
|
||||
export let className = 'pr-2';
|
||||
export let className = '';
|
||||
|
||||
export let id;
|
||||
export let title;
|
||||
|
@ -60,14 +60,14 @@
|
||||
|
||||
<div slot="content">
|
||||
<DropdownMenu.Content
|
||||
class="w-full max-w-[180px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
|
||||
class="w-full max-w-[160px] rounded-xl px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-xl"
|
||||
sideOffset={-2}
|
||||
side="bottom"
|
||||
align="start"
|
||||
transition={flyAndScale}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
pinHandler();
|
||||
}}
|
||||
@ -82,7 +82,7 @@
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
renameHandler();
|
||||
}}
|
||||
@ -92,7 +92,7 @@
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
cloneChatHandler();
|
||||
}}
|
||||
@ -102,7 +102,7 @@
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
archiveChatHandler();
|
||||
}}
|
||||
@ -112,7 +112,7 @@
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
shareHandler();
|
||||
}}
|
||||
@ -122,7 +122,7 @@
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
deleteHandler();
|
||||
}}
|
||||
@ -131,7 +131,7 @@
|
||||
<div class="flex items-center">{$i18n.t('Delete')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
|
||||
<hr class="border-gray-100 dark:border-gray-800 mt-1 mb-1" />
|
||||
|
||||
<div class="flex p-1">
|
||||
<Tags
|
||||
|
@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu } from 'bits-ui';
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
import { getContext, createEventDispatcher } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
|
||||
import Pencil from '$lib/components/icons/Pencil.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
let show = false;
|
||||
</script>
|
||||
|
||||
<Dropdown
|
||||
bind:show
|
||||
on:change={(e) => {
|
||||
if (e.detail === false) {
|
||||
dispatch('close');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tooltip content={$i18n.t('More')}>
|
||||
<slot />
|
||||
</Tooltip>
|
||||
|
||||
<div slot="content">
|
||||
<DropdownMenu.Content
|
||||
class="w-full max-w-[160px] rounded-lg px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-xl"
|
||||
sideOffset={-2}
|
||||
side="bottom"
|
||||
align="start"
|
||||
transition={flyAndScale}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
dispatch('rename');
|
||||
}}
|
||||
>
|
||||
<Pencil strokeWidth="2" />
|
||||
<div class="flex items-center">{$i18n.t('Rename')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
dispatch('delete');
|
||||
}}
|
||||
>
|
||||
<GarbageBin strokeWidth="2" />
|
||||
<div class="flex items-center">{$i18n.t('Delete')}</div>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</div>
|
||||
</Dropdown>
|
@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { getContext, createEventDispatcher, onMount, onDestroy } from 'svelte';
|
||||
import { getContext, createEventDispatcher, onMount, onDestroy, tick } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
@ -12,6 +12,7 @@
|
||||
import FolderOpen from '$lib/components/icons/FolderOpen.svelte';
|
||||
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
|
||||
import {
|
||||
deleteFolderById,
|
||||
updateFolderIsExpandedById,
|
||||
updateFolderNameById,
|
||||
updateFolderParentIdById
|
||||
@ -19,6 +20,7 @@
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { updateChatFolderIdById } from '$lib/apis/chats';
|
||||
import ChatItem from './ChatItem.svelte';
|
||||
import FolderMenu from './Folders/FolderMenu.svelte';
|
||||
|
||||
export let open = false;
|
||||
|
||||
@ -179,6 +181,18 @@
|
||||
}
|
||||
});
|
||||
|
||||
const deleteHandler = async () => {
|
||||
const res = await deleteFolderById(localStorage.token, folderId).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success('Folder deleted successfully');
|
||||
dispatch('update');
|
||||
}
|
||||
};
|
||||
|
||||
const nameUpdateHandler = async () => {
|
||||
if (name === '') {
|
||||
toast.error("Folder name can't be empty");
|
||||
@ -226,6 +240,21 @@
|
||||
};
|
||||
|
||||
$: isExpandedUpdateDebounceHandler(open);
|
||||
|
||||
const editHandler = async () => {
|
||||
console.log('Edit');
|
||||
await tick();
|
||||
name = folders[folderId].name;
|
||||
edit = true;
|
||||
|
||||
await tick();
|
||||
|
||||
// focus on the input
|
||||
setTimeout(() => {
|
||||
const input = document.getElementById(`folder-${folderId}-input`);
|
||||
input.focus();
|
||||
}, 100);
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if dragged && x && y}
|
||||
@ -252,6 +281,8 @@
|
||||
bind:open
|
||||
className="w-full"
|
||||
buttonClassName="w-full"
|
||||
disabled={(folders[folderId]?.childrenIds ?? []).length === 0 &&
|
||||
(folders[folderId].items?.chats ?? []).length === 0}
|
||||
on:change={(e) => {
|
||||
dispatch('open', e.detail);
|
||||
}}
|
||||
@ -259,16 +290,10 @@
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="w-full group">
|
||||
<button
|
||||
class="w-full py-1.5 px-2 rounded-md flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-500 font-medium hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
id="folder-{folderId}-button"
|
||||
class="relative w-full py-1.5 px-2 rounded-md flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-500 font-medium hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
on:dblclick={() => {
|
||||
name = folders[folderId].name;
|
||||
edit = true;
|
||||
|
||||
// focus on the input
|
||||
setTimeout(() => {
|
||||
const input = document.getElementById(`folder-${folderId}-input`);
|
||||
input.focus();
|
||||
}, 0);
|
||||
editHandler();
|
||||
}}
|
||||
>
|
||||
<div class="text-gray-300 dark:text-gray-600">
|
||||
@ -309,21 +334,27 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class=" hidden group-hover:flex dark:text-gray-300">
|
||||
<button
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
console.log('clicked');
|
||||
<div
|
||||
class="absolute z-10 right-2 invisible group-hover:visible self-center flex items-center dark:text-gray-300 touch-auto pointer-events-auto"
|
||||
>
|
||||
<FolderMenu
|
||||
on:rename={() => {
|
||||
editHandler();
|
||||
}}
|
||||
on:delete={() => {
|
||||
deleteHandler();
|
||||
}}
|
||||
>
|
||||
<EllipsisHorizontal className="size-4" strokeWidth="2.5" />
|
||||
</button>
|
||||
<button class="p-0.5 dark:hover:bg-gray-850 rounded-lg" on:click={(e) => {}}>
|
||||
<EllipsisHorizontal className="size-4" strokeWidth="2.5" />
|
||||
</button>
|
||||
</FolderMenu>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div slot="content" class="w-full">
|
||||
{#if folders[folderId].childrenIds || folders[folderId].items?.chats}
|
||||
{#if (folders[folderId]?.childrenIds ?? []).length > 0 || (folders[folderId].items?.chats ?? []).length > 0}
|
||||
<div
|
||||
class="ml-3 pl-1 mt-[1px] flex flex-col overflow-y-auto scrollbar-hidden border-s border-gray-100 dark:border-gray-900"
|
||||
>
|
||||
|
Loading…
x
Reference in New Issue
Block a user