feat: add context menu support to DocumentList, FAQList, and KnowledgeBaseList for improved user interaction

This commit is contained in:
mlogclub
2026-06-02 16:19:11 +08:00
parent 5b8bb388db
commit 44ec2921d4
3 changed files with 40 additions and 9 deletions
@@ -152,6 +152,7 @@ export function DocumentList({ knowledgeBaseId, onActionStateChange }: DocumentL
const [moving, setMoving] = useState(false); const [moving, setMoving] = useState(false);
const [bulkMoveOpen, setBulkMoveOpen] = useState(false); const [bulkMoveOpen, setBulkMoveOpen] = useState(false);
const [selectedIds, setSelectedIds] = useState<number[]>([]); const [selectedIds, setSelectedIds] = useState<number[]>([]);
const [contextMenuDocumentId, setContextMenuDocumentId] = useState<number | null>(null);
const [selectedDirectoryId, setSelectedDirectoryId] = useState<number | null>(null); const [selectedDirectoryId, setSelectedDirectoryId] = useState<number | null>(null);
const [actionLoadingMap, setActionLoadingMap] = useState<Record<number, { rebuildIndex: boolean; delete: boolean }>>({}); const [actionLoadingMap, setActionLoadingMap] = useState<Record<number, { rebuildIndex: boolean; delete: boolean }>>({});
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
@@ -484,10 +485,16 @@ export function DocumentList({ knowledgeBaseId, onActionStateChange }: DocumentL
) : null} ) : null}
{documents.results.map((item) => ( {documents.results.map((item) => (
viewMode === "grid" ? ( viewMode === "grid" ? (
<ContextMenu key={item.id}> <ContextMenu
key={item.id}
onOpenChange={(open) => setContextMenuDocumentId(open ? item.id : null)}
>
<ContextMenuTrigger className="w-full"> <ContextMenuTrigger className="w-full">
<div <div
className="bg-background p-3 transition-colors hover:bg-accent w-full" className={cn(
"bg-background p-3 transition-colors hover:bg-accent w-full",
contextMenuDocumentId === item.id && "bg-accent text-accent-foreground",
)}
> >
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div className="flex min-w-0 flex-1 items-start gap-2"> <div className="flex min-w-0 flex-1 items-start gap-2">
@@ -569,10 +576,16 @@ export function DocumentList({ knowledgeBaseId, onActionStateChange }: DocumentL
</ContextMenuContent> </ContextMenuContent>
</ContextMenu> </ContextMenu>
) : ( ) : (
<ContextMenu key={item.id}> <ContextMenu
key={item.id}
onOpenChange={(open) => setContextMenuDocumentId(open ? item.id : null)}
>
<ContextMenuTrigger className="w-full"> <ContextMenuTrigger className="w-full">
<div <div
className="flex items-center gap-3 bg-background p-2 transition-colors hover:bg-accent w-full" className={cn(
"flex items-center gap-3 bg-background p-2 transition-colors hover:bg-accent w-full",
contextMenuDocumentId === item.id && "bg-accent text-accent-foreground",
)}
> >
<Checkbox <Checkbox
checked={selectedIds.includes(item.id)} checked={selectedIds.includes(item.id)}
@@ -55,7 +55,7 @@ import {
} from "@/lib/api/admin"; } from "@/lib/api/admin";
import { KnowledgeDocumentIndexStatus } from "@/lib/generated/enums"; import { KnowledgeDocumentIndexStatus } from "@/lib/generated/enums";
import { useI18n } from "@/i18n/provider"; import { useI18n } from "@/i18n/provider";
import { formatDateTime } from "@/lib/utils"; import { cn, formatDateTime } from "@/lib/utils";
import { FAQEditDialog } from "./faq-edit"; import { FAQEditDialog } from "./faq-edit";
import { FAQImportDialog } from "./faq-import-dialog"; import { FAQImportDialog } from "./faq-import-dialog";
import { KnowledgeBulkMoveDialog } from "./knowledge-bulk-move-dialog"; import { KnowledgeBulkMoveDialog } from "./knowledge-bulk-move-dialog";
@@ -141,6 +141,7 @@ export function FAQList({
const [moving, setMoving] = useState(false); const [moving, setMoving] = useState(false);
const [bulkMoveOpen, setBulkMoveOpen] = useState(false); const [bulkMoveOpen, setBulkMoveOpen] = useState(false);
const [selectedIds, setSelectedIds] = useState<number[]>([]); const [selectedIds, setSelectedIds] = useState<number[]>([]);
const [contextMenuFAQId, setContextMenuFAQId] = useState<number | null>(null);
const [importing, setImporting] = useState(false); const [importing, setImporting] = useState(false);
const [importDialogOpen, setImportDialogOpen] = useState(false); const [importDialogOpen, setImportDialogOpen] = useState(false);
const [selectedDirectoryId, setSelectedDirectoryId] = useState<number | null>(null); const [selectedDirectoryId, setSelectedDirectoryId] = useState<number | null>(null);
@@ -429,9 +430,17 @@ export function FAQList({
</label> </label>
) : null} ) : null}
{faqs.results.map((item) => ( {faqs.results.map((item) => (
<ContextMenu key={item.id}> <ContextMenu
key={item.id}
onOpenChange={(open) => setContextMenuFAQId(open ? item.id : null)}
>
<ContextMenuTrigger className="w-full"> <ContextMenuTrigger className="w-full">
<div className="flex items-center gap-3 bg-background p-2 transition-colors hover:bg-accent w-full"> <div
className={cn(
"flex items-center gap-3 bg-background p-2 transition-colors hover:bg-accent w-full",
contextMenuFAQId === item.id && "bg-accent text-accent-foreground",
)}
>
<Checkbox <Checkbox
checked={selectedIds.includes(item.id)} checked={selectedIds.includes(item.id)}
onClick={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}
@@ -82,7 +82,9 @@ function getStatusOptions(t: TFunction) {
type SortableKnowledgeBaseCardProps = { type SortableKnowledgeBaseCardProps = {
item: KnowledgeBase; item: KnowledgeBase;
isSelected: boolean; isSelected: boolean;
isContextMenuOpen: boolean;
disabled: boolean; disabled: boolean;
onContextMenuOpenChange: (open: boolean) => void;
onSelect: () => void; onSelect: () => void;
onEdit: () => void; onEdit: () => void;
onDelete: () => void; onDelete: () => void;
@@ -95,7 +97,9 @@ type SortableKnowledgeBaseCardProps = {
function SortableKnowledgeBaseCard({ function SortableKnowledgeBaseCard({
item, item,
isSelected, isSelected,
isContextMenuOpen,
disabled, disabled,
onContextMenuOpenChange,
onSelect, onSelect,
onEdit, onEdit,
onDelete, onDelete,
@@ -122,13 +126,13 @@ function SortableKnowledgeBaseCard({
}; };
return ( return (
<ContextMenu> <ContextMenu onOpenChange={onContextMenuOpenChange}>
<ContextMenuTrigger <ContextMenuTrigger
ref={setNodeRef} ref={setNodeRef}
style={style} style={style}
className={cn( className={cn(
"group flex items-center gap-1 rounded mx-2 px-2 py-1.5 text-sm transition-colors hover:bg-accent cursor-pointer", "group flex items-center gap-1 rounded mx-2 px-2 py-1.5 text-sm transition-colors hover:bg-accent cursor-pointer",
isSelected && "bg-accent text-accent-foreground", (isSelected || isContextMenuOpen) && "bg-accent text-accent-foreground",
isDragging && "bg-muted/60 shadow-sm opacity-80", isDragging && "bg-muted/60 shadow-sm opacity-80",
)} )}
onClick={onSelect} onClick={onSelect}
@@ -248,6 +252,7 @@ export function KnowledgeBaseList({
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [editingItemId, setEditingItemId] = useState<number | null>(null); const [editingItemId, setEditingItemId] = useState<number | null>(null);
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([]); const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([]);
const [contextMenuKnowledgeBaseId, setContextMenuKnowledgeBaseId] = useState<number | null>(null);
const statusOptions = useMemo(() => getStatusOptions(t), [t]); const statusOptions = useMemo(() => getStatusOptions(t), [t]);
const sensors = useSensors( const sensors = useSensors(
@@ -487,7 +492,11 @@ export function KnowledgeBaseList({
key={item.id} key={item.id}
item={item} item={item}
isSelected={selectedKnowledgeBaseId === item.id} isSelected={selectedKnowledgeBaseId === item.id}
isContextMenuOpen={contextMenuKnowledgeBaseId === item.id}
disabled={loading || sorting} disabled={loading || sorting}
onContextMenuOpenChange={(open) =>
setContextMenuKnowledgeBaseId(open ? item.id : null)
}
onSelect={() => onSelectKnowledgeBase(item)} onSelect={() => onSelectKnowledgeBase(item)}
onEdit={() => openEditDialog(item)} onEdit={() => openEditDialog(item)}
onDelete={() => void handleDelete(item)} onDelete={() => void handleDelete(item)}