mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-31 06:45:17 +01:00
🪄 refactor: UI Polish and Admin Dialog Unification (#11108)
* refactor(OpenSidebar): removed useless classNames
* style(Header): update hover styles across various components for improved UI consistency
* style(Nav): update hover styles in AccountSettings and SearchBar for improved UI consistency
* style: update button classes for consistent hover effects and improved UI responsiveness
* style(Nav, OpenSidebar, Header, Convo): improve UI responsiveness and animation transitions
* style(PresetsMenu, NewChat): update icon sizes and improve component styling for better UI consistency
* style(Nav, Root): enhance sidebar mobile animations and responsiveness for better UI experience
* style(ExportAndShareMenu, BookmarkMenu): update icon sizes for improved UI consistency
* style: remove transition duration from button classes for improved UI responsiveness
* style(CustomMenu, ModelSelector): update background colors for improved UI consistency and responsiveness
* style(ExportAndShareMenu): update icon color for improved UI consistency
* style(TemporaryChat): refine button styles for improved UI consistency and responsiveness
* style(BookmarkNav): refactor to use DropdownPopup and remove BookmarkNavItems for improved UI consistency and functionality
* style(CustomMenu, EndpointItem): enhance UI elements for improved consistency and accessibility
* style(EndpointItem): adjust gap in icon container for improved layout consistency
* style(CustomMenu, EndpointItem): update focus ring color for improved UI consistency
* style(EndpointItem): update icon color for improved UI consistency in dark theme
* style: update focus styles for improved accessibility and consistency across components
* refactor(Nav): extract sidebar width to NAV_WIDTH constant
Centralize mobile (320px) and desktop (260px) sidebar widths in a single
exported constant to avoid magic numbers and ensure consistency.
* fix(BookmarkNav): memoize handlers used in useMemo
Wrap handleTagClick and handleClear in useCallback and add them to the
dropdownItems useMemo dependency array to prevent stale closures.
* feat: introduce FilterInput component and replace existing inputs with it across multiple components
* feat(DataTable): replace custom input with FilterInput component for improved filtering
* fix: Nested dialog overlay stacking issue
Fixes overlay appearing behind content when opening nested dialogs.
Introduced dynamic z-index calculation based on dialog depth using React context.
- First dialog: overlay z-50, content z-100
- Nested dialogs increment by 60: overlay z-110/content z-160, etc.
Preserves a11y escape key handling from #10975 and #10851.
Regression from #11008 (afb67fcf1) which increased content z-index
without adjusting overlay z-index for nested dialog scenarios.
* Refactor admin settings components to use a unified AdminSettingsDialog
- Removed redundant code from AdminSettings, MCPAdminSettings, and Memories AdminSettings components.
- Introduced AdminSettingsDialog component to handle permission management for different sections.
- Updated permission handling logic to use a consistent structure across components.
- Enhanced role selection and permission confirmation features in the new dialog.
- Improved UI consistency and maintainability by centralizing dialog functionality.
* refactor(Memory): memory management UI components and replace MemoryViewer with MemoryPanel
* refactor(Memory): enhance UI components for Memory dialogs and improve input styling
* refactor(Bookmarks): improve bookmark management UI with enhanced styling
* refactor(translations): remove redundant filter input and bookmark count entries
* refactor(Convo): integrate useShiftKey hook for enhanced keyboard interaction and improve UI responsiveness
This commit is contained in:
parent
c21733930c
commit
5181356bef
71 changed files with 2115 additions and 2191 deletions
111
client/src/components/SidePanel/Bookmarks/BookmarkCard.tsx
Normal file
111
client/src/components/SidePanel/Bookmarks/BookmarkCard.tsx
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import React, { useRef } from 'react';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { GripVertical } from 'lucide-react';
|
||||
import type { TConversationTag } from 'librechat-data-provider';
|
||||
import { TooltipAnchor, useToastContext } from '@librechat/client';
|
||||
import { useConversationTagMutation } from '~/data-provider';
|
||||
import { NotificationSeverity } from '~/common';
|
||||
import BookmarkCardActions from './BookmarkCardActions';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
interface BookmarkCardProps {
|
||||
bookmark: TConversationTag;
|
||||
position: number;
|
||||
moveRow: (dragIndex: number, hoverIndex: number) => void;
|
||||
}
|
||||
|
||||
interface DragItem {
|
||||
index: number;
|
||||
id: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export default function BookmarkCard({ bookmark, position, moveRow }: BookmarkCardProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
|
||||
const mutation = useConversationTagMutation({
|
||||
context: 'BookmarkCard',
|
||||
tag: bookmark.tag,
|
||||
});
|
||||
|
||||
const handleDrop = (item: DragItem) => {
|
||||
mutation.mutate(
|
||||
{ ...bookmark, position: item.index },
|
||||
{
|
||||
onSuccess: () => {
|
||||
showToast({
|
||||
message: localize('com_ui_bookmarks_update_success'),
|
||||
severity: NotificationSeverity.SUCCESS,
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
showToast({
|
||||
message: localize('com_ui_bookmarks_update_error'),
|
||||
severity: NotificationSeverity.ERROR,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const [, drop] = useDrop({
|
||||
accept: 'bookmark',
|
||||
drop: handleDrop,
|
||||
hover(item: DragItem) {
|
||||
if (!ref.current || item.index === position) {
|
||||
return;
|
||||
}
|
||||
moveRow(item.index, position);
|
||||
item.index = position;
|
||||
},
|
||||
});
|
||||
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: 'bookmark',
|
||||
item: { index: position },
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
});
|
||||
|
||||
drag(drop(ref));
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex cursor-move items-center gap-2 rounded-lg px-3 py-2.5',
|
||||
'border border-border-light bg-transparent',
|
||||
'hover:bg-surface-secondary',
|
||||
isDragging && 'opacity-50',
|
||||
)}
|
||||
>
|
||||
{/* Drag handle */}
|
||||
<GripVertical className="size-4 shrink-0 text-text-tertiary" aria-hidden="true" />
|
||||
|
||||
{/* Tag name */}
|
||||
<span className="min-w-0 flex-1 truncate text-sm font-semibold text-text-primary">
|
||||
{bookmark.tag}
|
||||
</span>
|
||||
|
||||
{/* Count badge */}
|
||||
<TooltipAnchor
|
||||
description={`${bookmark.count} ${localize(bookmark.count === 1 ? 'com_ui_conversation' : 'com_ui_conversations')}`}
|
||||
side="top"
|
||||
render={
|
||||
<span className="shrink-0 rounded-full bg-surface-tertiary px-2 py-0.5 text-xs text-text-secondary">
|
||||
{bookmark.count}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="shrink-0">
|
||||
<BookmarkCardActions bookmark={bookmark} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
import { useState, useRef, useCallback } from 'react';
|
||||
import { Pencil, Trash2 } from 'lucide-react';
|
||||
import type { TConversationTag } from 'librechat-data-provider';
|
||||
import {
|
||||
Button,
|
||||
OGDialog,
|
||||
OGDialogTrigger,
|
||||
OGDialogTemplate,
|
||||
TooltipAnchor,
|
||||
useToastContext,
|
||||
} from '@librechat/client';
|
||||
import { useDeleteConversationTagMutation } from '~/data-provider';
|
||||
import { BookmarkEditDialog } from '~/components/Bookmarks';
|
||||
import { NotificationSeverity } from '~/common';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
interface BookmarkCardActionsProps {
|
||||
bookmark: TConversationTag;
|
||||
}
|
||||
|
||||
export default function BookmarkCardActions({ bookmark }: BookmarkCardActionsProps) {
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const editTriggerRef = useRef<HTMLButtonElement>(null);
|
||||
const deleteTriggerRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const deleteBookmarkMutation = useDeleteConversationTagMutation({
|
||||
onSuccess: () => {
|
||||
showToast({
|
||||
message: localize('com_ui_bookmarks_delete_success'),
|
||||
});
|
||||
setDeleteOpen(false);
|
||||
},
|
||||
onError: () => {
|
||||
showToast({
|
||||
message: localize('com_ui_bookmarks_delete_error'),
|
||||
severity: NotificationSeverity.ERROR,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const confirmDelete = useCallback(async () => {
|
||||
await deleteBookmarkMutation.mutateAsync(bookmark.tag);
|
||||
}, [bookmark.tag, deleteBookmarkMutation]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
{/* Edit button */}
|
||||
<BookmarkEditDialog
|
||||
context="BookmarkCardActions"
|
||||
bookmark={bookmark}
|
||||
open={editOpen}
|
||||
setOpen={setEditOpen}
|
||||
triggerRef={editTriggerRef}
|
||||
>
|
||||
<OGDialogTrigger asChild>
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_edit')}
|
||||
side="top"
|
||||
render={
|
||||
<Button
|
||||
ref={editTriggerRef}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
aria-label={localize('com_ui_bookmarks_edit')}
|
||||
onClick={() => setEditOpen(true)}
|
||||
>
|
||||
<Pencil className="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</OGDialogTrigger>
|
||||
</BookmarkEditDialog>
|
||||
|
||||
{/* Delete button */}
|
||||
<OGDialog open={deleteOpen} onOpenChange={setDeleteOpen} triggerRef={deleteTriggerRef}>
|
||||
<OGDialogTrigger asChild>
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_delete')}
|
||||
side="top"
|
||||
render={
|
||||
<Button
|
||||
ref={deleteTriggerRef}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
aria-label={localize('com_ui_bookmarks_delete')}
|
||||
onClick={() => setDeleteOpen(true)}
|
||||
>
|
||||
<Trash2 className="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogTemplate
|
||||
showCloseButton={false}
|
||||
title={localize('com_ui_bookmarks_delete')}
|
||||
className="max-w-[450px]"
|
||||
main={
|
||||
<p className="text-left text-sm text-text-secondary">
|
||||
{localize('com_ui_bookmark_delete_confirm')} <strong>{bookmark.tag}</strong>
|
||||
</p>
|
||||
}
|
||||
selection={{
|
||||
selectHandler: confirmDelete,
|
||||
selectClasses:
|
||||
'bg-destructive text-white transition-all duration-200 hover:bg-destructive/80',
|
||||
selectText: localize('com_ui_delete'),
|
||||
}}
|
||||
/>
|
||||
</OGDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { Bookmark } from 'lucide-react';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
interface BookmarkEmptyStateProps {
|
||||
isFiltered?: boolean;
|
||||
}
|
||||
|
||||
export default function BookmarkEmptyState({ isFiltered = false }: BookmarkEmptyStateProps) {
|
||||
const localize = useLocalize();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||
<div className="mb-3 rounded-full bg-surface-secondary p-3">
|
||||
<Bookmark className="size-6 text-text-tertiary" aria-hidden="true" />
|
||||
</div>
|
||||
<p className="text-sm text-text-secondary">
|
||||
{isFiltered ? localize('com_ui_no_bookmarks_match') : localize('com_ui_no_bookmarks')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
client/src/components/SidePanel/Bookmarks/BookmarkList.tsx
Normal file
32
client/src/components/SidePanel/Bookmarks/BookmarkList.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import type { TConversationTag } from 'librechat-data-provider';
|
||||
import BookmarkEmptyState from './BookmarkEmptyState';
|
||||
import BookmarkCard from './BookmarkCard';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
interface BookmarkListProps {
|
||||
bookmarks: TConversationTag[];
|
||||
moveRow: (dragIndex: number, hoverIndex: number) => void;
|
||||
isFiltered?: boolean;
|
||||
}
|
||||
|
||||
export default function BookmarkList({
|
||||
bookmarks,
|
||||
moveRow,
|
||||
isFiltered = false,
|
||||
}: BookmarkListProps) {
|
||||
const localize = useLocalize();
|
||||
|
||||
if (bookmarks.length === 0) {
|
||||
return <BookmarkEmptyState isFiltered={isFiltered} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2" role="list" aria-label={localize('com_ui_bookmarks')}>
|
||||
{bookmarks.map((bookmark) => (
|
||||
<div key={bookmark._id} role="listitem">
|
||||
<BookmarkCard bookmark={bookmark} position={bookmark.position} moveRow={moveRow} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,22 +1,14 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { BookmarkPlusIcon } from 'lucide-react';
|
||||
import {
|
||||
Table,
|
||||
Input,
|
||||
Button,
|
||||
TableRow,
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
OGDialogTrigger,
|
||||
} from '@librechat/client';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { Button, FilterInput, OGDialogTrigger, TooltipAnchor } from '@librechat/client';
|
||||
import type { ConversationTagsResponse, TConversationTag } from 'librechat-data-provider';
|
||||
import { BookmarkContext, useBookmarkContext } from '~/Providers/BookmarkContext';
|
||||
import { BookmarkEditDialog } from '~/components/Bookmarks';
|
||||
import BookmarkTableRow from './BookmarkTableRow';
|
||||
import BookmarkList from './BookmarkList';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
const pageSize = 10;
|
||||
|
||||
const removeDuplicates = (bookmarks: TConversationTag[]) => {
|
||||
const seen = new Set();
|
||||
return bookmarks.filter((bookmark) => {
|
||||
|
|
@ -31,8 +23,7 @@ const BookmarkTable = () => {
|
|||
const [rows, setRows] = useState<ConversationTagsResponse>([]);
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [open, setOpen] = useState(false);
|
||||
const pageSize = 10;
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
|
||||
const { bookmarks = [] } = useBookmarkContext();
|
||||
|
||||
|
|
@ -41,6 +32,11 @@ const BookmarkTable = () => {
|
|||
setRows(_bookmarks);
|
||||
}, [bookmarks]);
|
||||
|
||||
// Reset page when search changes
|
||||
useEffect(() => {
|
||||
setPageIndex(0);
|
||||
}, [searchQuery]);
|
||||
|
||||
const moveRow = useCallback((dragIndex: number, hoverIndex: number) => {
|
||||
setRows((prevTags: TConversationTag[]) => {
|
||||
const updatedRows = [...prevTags];
|
||||
|
|
@ -50,86 +46,60 @@ const BookmarkTable = () => {
|
|||
});
|
||||
}, []);
|
||||
|
||||
const renderRow = useCallback(
|
||||
(row: TConversationTag) => (
|
||||
<BookmarkTableRow key={row._id} moveRow={moveRow} row={row} position={row.position} />
|
||||
),
|
||||
[moveRow],
|
||||
);
|
||||
|
||||
const filteredRows = rows.filter(
|
||||
(row) => row.tag && row.tag.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
);
|
||||
|
||||
const currentRows = filteredRows.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
|
||||
const totalPages = Math.ceil(filteredRows.length / pageSize);
|
||||
|
||||
return (
|
||||
<BookmarkContext.Provider value={{ bookmarks }}>
|
||||
<div role="region" aria-label={localize('com_ui_bookmarks')} className="mt-2 space-y-2">
|
||||
<div className="relative flex items-center gap-4">
|
||||
<Input
|
||||
id="bookmarks-filter"
|
||||
placeholder=" "
|
||||
<div role="region" aria-label={localize('com_ui_bookmarks')} className="mt-2 space-y-3">
|
||||
{/* Header: Filter + Create Button */}
|
||||
<div className="flex items-center gap-2">
|
||||
<FilterInput
|
||||
inputId="bookmarks-filter"
|
||||
label={localize('com_ui_bookmarks_filter')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
aria-label={localize('com_ui_bookmarks_filter')}
|
||||
className="peer"
|
||||
containerClassName="flex-1"
|
||||
/>
|
||||
<label
|
||||
htmlFor="bookmarks-filter"
|
||||
className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-sm text-text-secondary transition-all duration-200 peer-focus:top-0 peer-focus:bg-background peer-focus:px-1 peer-focus:text-xs peer-[:not(:placeholder-shown)]:top-0 peer-[:not(:placeholder-shown)]:bg-background peer-[:not(:placeholder-shown)]:px-1 peer-[:not(:placeholder-shown)]:text-xs"
|
||||
<BookmarkEditDialog context="BookmarkTable" open={createOpen} setOpen={setCreateOpen}>
|
||||
<OGDialogTrigger asChild>
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_bookmarks_new')}
|
||||
side="bottom"
|
||||
render={
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="shrink-0 bg-transparent"
|
||||
aria-label={localize('com_ui_bookmarks_new')}
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
<Plus className="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</OGDialogTrigger>
|
||||
</BookmarkEditDialog>
|
||||
</div>
|
||||
|
||||
{/* Bookmark List */}
|
||||
<BookmarkList
|
||||
bookmarks={currentRows}
|
||||
moveRow={moveRow}
|
||||
isFiltered={searchQuery.length > 0}
|
||||
/>
|
||||
|
||||
{/* Pagination */}
|
||||
{filteredRows.length > pageSize && (
|
||||
<div
|
||||
className="flex items-center justify-end gap-2"
|
||||
role="navigation"
|
||||
aria-label="Pagination"
|
||||
>
|
||||
{localize('com_ui_bookmarks_filter')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-border-light bg-transparent shadow-sm transition-colors">
|
||||
<Table className="w-full table-fixed">
|
||||
<TableHeader>
|
||||
<TableRow className="border-b border-border-light">
|
||||
<TableHead className="w-[70%] bg-surface-secondary py-3 text-left text-sm font-medium text-text-secondary">
|
||||
<div>{localize('com_ui_bookmarks_title')}</div>
|
||||
</TableHead>
|
||||
<TableHead className="w-[30%] bg-surface-secondary py-3 text-left text-sm font-medium text-text-secondary">
|
||||
<div>{localize('com_ui_bookmarks_count')}</div>
|
||||
</TableHead>
|
||||
<TableHead className="w-[40%] bg-surface-secondary py-3 text-left text-sm font-medium text-text-secondary">
|
||||
<div>{localize('com_assistants_actions')}</div>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{currentRows.length ? (
|
||||
currentRows.map(renderRow)
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="h-24 text-center text-sm text-text-secondary">
|
||||
{localize('com_ui_no_bookmarks')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex justify-between gap-2">
|
||||
<BookmarkEditDialog context="BookmarkPanel" open={open} setOpen={setOpen}>
|
||||
<OGDialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full gap-2 text-sm"
|
||||
aria-label={localize('com_ui_bookmarks_new')}
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
<BookmarkPlusIcon className="size-4" aria-hidden="true" />
|
||||
<div className="break-all">{localize('com_ui_bookmarks_new')}</div>
|
||||
</Button>
|
||||
</OGDialogTrigger>
|
||||
</BookmarkEditDialog>
|
||||
</div>
|
||||
<div className="flex items-center gap-2" role="navigation" aria-label="Pagination">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
|
@ -139,24 +109,20 @@ const BookmarkTable = () => {
|
|||
>
|
||||
{localize('com_ui_prev')}
|
||||
</Button>
|
||||
<div aria-live="polite" className="text-sm">
|
||||
{`${pageIndex + 1} / ${Math.ceil(filteredRows.length / pageSize)}`}
|
||||
<div className="whitespace-nowrap text-sm" aria-live="polite">
|
||||
{pageIndex + 1} / {totalPages}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setPageIndex((prev) =>
|
||||
(prev + 1) * pageSize < filteredRows.length ? prev + 1 : prev,
|
||||
)
|
||||
}
|
||||
disabled={(pageIndex + 1) * pageSize >= filteredRows.length}
|
||||
onClick={() => setPageIndex((prev) => (prev + 1 < totalPages ? prev + 1 : prev))}
|
||||
disabled={pageIndex + 1 >= totalPages}
|
||||
aria-label={localize('com_ui_next')}
|
||||
>
|
||||
{localize('com_ui_next')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</BookmarkContext.Provider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,90 +0,0 @@
|
|||
import React, { useRef } from 'react';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import type { TConversationTag } from 'librechat-data-provider';
|
||||
import { TableRow, TableCell, useToastContext } from '@librechat/client';
|
||||
import { DeleteBookmarkButton, EditBookmarkButton } from '~/components/Bookmarks';
|
||||
import { useConversationTagMutation } from '~/data-provider';
|
||||
import { NotificationSeverity } from '~/common';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
interface BookmarkTableRowProps {
|
||||
row: TConversationTag;
|
||||
moveRow: (dragIndex: number, hoverIndex: number) => void;
|
||||
position: number;
|
||||
}
|
||||
|
||||
interface DragItem {
|
||||
index: number;
|
||||
id: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
const BookmarkTableRow: React.FC<BookmarkTableRowProps> = ({ row, moveRow, position }) => {
|
||||
const ref = useRef<HTMLTableRowElement>(null);
|
||||
const mutation = useConversationTagMutation({ context: 'BookmarkTableRow', tag: row.tag });
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
|
||||
const handleDrop = (item: DragItem) => {
|
||||
mutation.mutate(
|
||||
{ ...row, position: item.index },
|
||||
{
|
||||
onSuccess: () => {
|
||||
showToast({
|
||||
message: localize('com_ui_bookmarks_update_success'),
|
||||
severity: NotificationSeverity.SUCCESS,
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
showToast({
|
||||
message: localize('com_ui_bookmarks_update_error'),
|
||||
severity: NotificationSeverity.ERROR,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const [, drop] = useDrop({
|
||||
accept: 'bookmark',
|
||||
drop: handleDrop,
|
||||
hover(item: DragItem) {
|
||||
if (!ref.current || item.index === position) {
|
||||
return;
|
||||
}
|
||||
moveRow(item.index, position);
|
||||
item.index = position;
|
||||
},
|
||||
});
|
||||
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: 'bookmark',
|
||||
item: { index: position },
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
});
|
||||
|
||||
drag(drop(ref));
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
ref={ref}
|
||||
className="cursor-move hover:bg-surface-secondary"
|
||||
style={{ opacity: isDragging ? 0.5 : 1 }}
|
||||
>
|
||||
<TableCell className="w-[70%] px-4 py-4">
|
||||
<div className="overflow-hidden text-ellipsis whitespace-nowrap">{row.tag}</div>
|
||||
</TableCell>
|
||||
<TableCell className="w-[10%] px-12 py-4">{row.count}</TableCell>
|
||||
<TableCell className="w-[20%] px-4 py-4">
|
||||
<div className="flex gap-2">
|
||||
<EditBookmarkButton bookmark={row} tabIndex={0} />
|
||||
<DeleteBookmarkButton bookmark={row.tag} tabIndex={0} />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookmarkTableRow;
|
||||
6
client/src/components/SidePanel/Bookmarks/index.ts
Normal file
6
client/src/components/SidePanel/Bookmarks/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export { default as BookmarkPanel } from './BookmarkPanel';
|
||||
export { default as BookmarkTable } from './BookmarkTable';
|
||||
export { default as BookmarkList } from './BookmarkList';
|
||||
export { default as BookmarkCard } from './BookmarkCard';
|
||||
export { default as BookmarkCardActions } from './BookmarkCardActions';
|
||||
export { default as BookmarkEmptyState } from './BookmarkEmptyState';
|
||||
Loading…
Add table
Add a link
Reference in a new issue