feat: implement drag-and-drop sorting for knowledge directories and add localization for sort actions
This commit is contained in:
@@ -84,6 +84,10 @@ func KnowledgeDirectoryPostDelete(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func KnowledgeDirectoryPostUpdate_sort(ctx *gin.Context) {
|
func KnowledgeDirectoryPostUpdate_sort(ctx *gin.Context) {
|
||||||
|
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeBaseUpdate); err != nil {
|
||||||
|
httpx.WriteJSON(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
var req struct {
|
var req struct {
|
||||||
KnowledgeBaseID int64 `json:"knowledgeBaseId"`
|
KnowledgeBaseID int64 `json:"knowledgeBaseId"`
|
||||||
ParentID int64 `json:"parentId"`
|
ParentID int64 `json:"parentId"`
|
||||||
|
|||||||
@@ -1,5 +1,22 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DndContext,
|
||||||
|
KeyboardSensor,
|
||||||
|
MouseSensor,
|
||||||
|
TouchSensor,
|
||||||
|
closestCenter,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
type DragEndEvent,
|
||||||
|
} from "@dnd-kit/core";
|
||||||
|
import {
|
||||||
|
SortableContext,
|
||||||
|
sortableKeyboardCoordinates,
|
||||||
|
useSortable,
|
||||||
|
verticalListSortingStrategy,
|
||||||
|
} from "@dnd-kit/sortable";
|
||||||
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
import {
|
import {
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
@@ -11,7 +28,7 @@ import {
|
|||||||
PlusIcon,
|
PlusIcon,
|
||||||
Trash2Icon,
|
Trash2Icon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { ReactNode } from "react";
|
import type { CSSProperties, ReactNode } from "react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState, type PointerEvent } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState, type PointerEvent } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -39,10 +56,12 @@ import {
|
|||||||
deleteKnowledgeDirectory,
|
deleteKnowledgeDirectory,
|
||||||
fetchKnowledgeDirectories,
|
fetchKnowledgeDirectories,
|
||||||
updateKnowledgeDirectory,
|
updateKnowledgeDirectory,
|
||||||
|
updateKnowledgeDirectorySort,
|
||||||
type KnowledgeDirectory,
|
type KnowledgeDirectory,
|
||||||
} from "@/lib/api/admin";
|
} from "@/lib/api/admin";
|
||||||
import { useI18n } from "@/i18n/provider";
|
import { useI18n } from "@/i18n/provider";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { findDirectoryParentId, moveDirectoryWithinParent } from "./knowledge-directory-sort";
|
||||||
|
|
||||||
type KnowledgeDirectoryPanelProps = {
|
type KnowledgeDirectoryPanelProps = {
|
||||||
knowledgeBaseId: number;
|
knowledgeBaseId: number;
|
||||||
@@ -106,6 +125,7 @@ export function KnowledgeDirectoryPanel({
|
|||||||
const [contextMenuDirectoryId, setContextMenuDirectoryId] = useState<number | null>(null);
|
const [contextMenuDirectoryId, setContextMenuDirectoryId] = useState<number | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [sorting, setSorting] = useState(false);
|
||||||
const [dialog, setDialog] = useState<DirectoryDialogState>({
|
const [dialog, setDialog] = useState<DirectoryDialogState>({
|
||||||
open: false,
|
open: false,
|
||||||
id: null,
|
id: null,
|
||||||
@@ -120,6 +140,17 @@ export function KnowledgeDirectoryPanel({
|
|||||||
],
|
],
|
||||||
[directories, dialog.id, t],
|
[directories, dialog.id, t],
|
||||||
);
|
);
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(MouseSensor, {
|
||||||
|
activationConstraint: { distance: 8 },
|
||||||
|
}),
|
||||||
|
useSensor(TouchSensor, {
|
||||||
|
activationConstraint: { delay: 150, tolerance: 8 },
|
||||||
|
}),
|
||||||
|
useSensor(KeyboardSensor, {
|
||||||
|
coordinateGetter: sortableKeyboardCoordinates,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const loadDirectories = useCallback(async () => {
|
const loadDirectories = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -268,6 +299,48 @@ export function KnowledgeDirectoryPanel({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleDirectoryDragEnd(event: DragEndEvent) {
|
||||||
|
const { active, over } = event;
|
||||||
|
if (!over || active.id === over.id || sorting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const activeId = Number(active.id);
|
||||||
|
const overId = Number(over.id);
|
||||||
|
if (!Number.isFinite(activeId) || !Number.isFinite(overId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeParentId = findDirectoryParentId(directories, activeId);
|
||||||
|
const overParentId = findDirectoryParentId(directories, overId);
|
||||||
|
if (activeParentId === null || overParentId === null || activeParentId !== overParentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousDirectories = directories;
|
||||||
|
const moved = moveDirectoryWithinParent(previousDirectories, activeParentId, activeId, overId);
|
||||||
|
if (!moved.changed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDirectories(moved.items);
|
||||||
|
setSorting(true);
|
||||||
|
try {
|
||||||
|
await updateKnowledgeDirectorySort({
|
||||||
|
knowledgeBaseId,
|
||||||
|
parentId: moved.parentId,
|
||||||
|
ids: moved.orderedIds,
|
||||||
|
});
|
||||||
|
toast.success(t("knowledge.directorySortUpdated"));
|
||||||
|
await loadDirectories();
|
||||||
|
onChanged?.();
|
||||||
|
} catch (error) {
|
||||||
|
setDirectories(previousDirectories);
|
||||||
|
toast.error(error instanceof Error ? error.message : t("knowledge.directorySortUpdateFailed"));
|
||||||
|
} finally {
|
||||||
|
setSorting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
@@ -301,26 +374,37 @@ export function KnowledgeDirectoryPanel({
|
|||||||
selected={selectedDirectoryId === 0}
|
selected={selectedDirectoryId === 0}
|
||||||
onClick={() => onSelectDirectory(0)}
|
onClick={() => onSelectDirectory(0)}
|
||||||
/>
|
/>
|
||||||
{directories.map((item) => (
|
<DndContext
|
||||||
<DirectoryNode
|
sensors={sensors}
|
||||||
key={item.id}
|
collisionDetection={closestCenter}
|
||||||
item={item}
|
onDragEnd={(event) => void handleDirectoryDragEnd(event)}
|
||||||
depth={0}
|
>
|
||||||
expandedIds={expandedIds}
|
<SortableContext
|
||||||
selectedDirectoryId={selectedDirectoryId}
|
items={directories.map((item) => item.id)}
|
||||||
contextMenuDirectoryId={contextMenuDirectoryId}
|
strategy={verticalListSortingStrategy}
|
||||||
saving={saving}
|
>
|
||||||
onToggle={toggleDirectory}
|
{directories.map((item) => (
|
||||||
onSelect={onSelectDirectory}
|
<DirectoryNode
|
||||||
onContextMenuOpenChange={(directoryId, open) =>
|
key={item.id}
|
||||||
setContextMenuDirectoryId(open ? directoryId : null)
|
item={item}
|
||||||
}
|
depth={0}
|
||||||
onCreate={openCreate}
|
expandedIds={expandedIds}
|
||||||
onEdit={openEdit}
|
selectedDirectoryId={selectedDirectoryId}
|
||||||
onDelete={(directory) => void handleDelete(directory)}
|
contextMenuDirectoryId={contextMenuDirectoryId}
|
||||||
t={t}
|
saving={saving || sorting}
|
||||||
/>
|
onToggle={toggleDirectory}
|
||||||
))}
|
onSelect={onSelectDirectory}
|
||||||
|
onContextMenuOpenChange={(directoryId, open) =>
|
||||||
|
setContextMenuDirectoryId(open ? directoryId : null)
|
||||||
|
}
|
||||||
|
onCreate={openCreate}
|
||||||
|
onEdit={openEdit}
|
||||||
|
onDelete={(directory) => void handleDelete(directory)}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
<div
|
<div
|
||||||
@@ -459,21 +543,39 @@ function DirectoryNode({
|
|||||||
onDelete,
|
onDelete,
|
||||||
t,
|
t,
|
||||||
}: DirectoryNodeProps) {
|
}: DirectoryNodeProps) {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({
|
||||||
|
id: item.id,
|
||||||
|
disabled: saving,
|
||||||
|
});
|
||||||
const expanded = expandedIds.has(item.id);
|
const expanded = expandedIds.has(item.id);
|
||||||
const hasChildren = (item.children || []).length > 0;
|
const hasChildren = (item.children || []).length > 0;
|
||||||
const active = selectedDirectoryId === item.id || contextMenuDirectoryId === item.id;
|
const active = selectedDirectoryId === item.id || contextMenuDirectoryId === item.id;
|
||||||
|
const style: CSSProperties = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div ref={setNodeRef} style={style}>
|
||||||
<ContextMenu onOpenChange={(open) => onContextMenuOpenChange(item.id, open)}>
|
<ContextMenu onOpenChange={(open) => onContextMenuOpenChange(item.id, open)}>
|
||||||
<ContextMenuTrigger className="block">
|
<ContextMenuTrigger className="block">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"group flex cursor-pointer items-center gap-1 px-2 py-1.5 text-sm hover:bg-accent",
|
"group flex cursor-pointer items-center gap-1 px-2 py-1.5 text-sm hover:bg-accent",
|
||||||
active && "bg-accent text-accent-foreground",
|
active && "bg-accent text-accent-foreground",
|
||||||
|
isDragging && "bg-muted/60 shadow-sm opacity-80",
|
||||||
)}
|
)}
|
||||||
style={{ paddingLeft: 8 + depth * 16 }}
|
style={{ paddingLeft: 8 + depth * 16 }}
|
||||||
onClick={() => onSelect(item.id)}
|
onClick={() => onSelect(item.id)}
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -550,24 +652,31 @@ function DirectoryNode({
|
|||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
{expanded
|
{expanded
|
||||||
? (item.children || []).map((child) => (
|
? (
|
||||||
<DirectoryNode
|
<SortableContext
|
||||||
key={child.id}
|
items={(item.children || []).map((child) => child.id)}
|
||||||
item={child}
|
strategy={verticalListSortingStrategy}
|
||||||
depth={depth + 1}
|
>
|
||||||
expandedIds={expandedIds}
|
{(item.children || []).map((child) => (
|
||||||
selectedDirectoryId={selectedDirectoryId}
|
<DirectoryNode
|
||||||
contextMenuDirectoryId={contextMenuDirectoryId}
|
key={child.id}
|
||||||
saving={saving}
|
item={child}
|
||||||
onToggle={onToggle}
|
depth={depth + 1}
|
||||||
onSelect={onSelect}
|
expandedIds={expandedIds}
|
||||||
onContextMenuOpenChange={onContextMenuOpenChange}
|
selectedDirectoryId={selectedDirectoryId}
|
||||||
onCreate={onCreate}
|
contextMenuDirectoryId={contextMenuDirectoryId}
|
||||||
onEdit={onEdit}
|
saving={saving}
|
||||||
onDelete={onDelete}
|
onToggle={onToggle}
|
||||||
t={t}
|
onSelect={onSelect}
|
||||||
/>
|
onContextMenuOpenChange={onContextMenuOpenChange}
|
||||||
))
|
onCreate={onCreate}
|
||||||
|
onEdit={onEdit}
|
||||||
|
onDelete={onDelete}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
)
|
||||||
: null}
|
: null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import assert from "node:assert/strict"
|
||||||
|
import { describe, it } from "node:test"
|
||||||
|
import ts from "typescript"
|
||||||
|
import { readFile } from "node:fs/promises"
|
||||||
|
import vm from "node:vm"
|
||||||
|
|
||||||
|
function plain(value) {
|
||||||
|
return JSON.parse(JSON.stringify(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadModule() {
|
||||||
|
const source = await readFile(
|
||||||
|
new URL("./knowledge-directory-sort.ts", import.meta.url),
|
||||||
|
"utf8"
|
||||||
|
)
|
||||||
|
const compiled = ts.transpileModule(source, {
|
||||||
|
compilerOptions: {
|
||||||
|
target: ts.ScriptTarget.ES2017,
|
||||||
|
module: ts.ModuleKind.CommonJS,
|
||||||
|
},
|
||||||
|
fileName: "knowledge-directory-sort.ts",
|
||||||
|
})
|
||||||
|
const sandbox = {
|
||||||
|
exports: {},
|
||||||
|
module: { exports: {} },
|
||||||
|
}
|
||||||
|
sandbox.exports = sandbox.module.exports
|
||||||
|
vm.runInNewContext(compiled.outputText, sandbox)
|
||||||
|
return sandbox.module.exports
|
||||||
|
}
|
||||||
|
|
||||||
|
const tree = [
|
||||||
|
{ id: 1, parentId: 0, name: "A", children: [
|
||||||
|
{ id: 11, parentId: 1, name: "A-1", children: [] },
|
||||||
|
{ id: 12, parentId: 1, name: "A-2", children: [] },
|
||||||
|
] },
|
||||||
|
{ id: 2, parentId: 0, name: "B", children: [] },
|
||||||
|
{ id: 3, parentId: 0, name: "C", children: [] },
|
||||||
|
]
|
||||||
|
|
||||||
|
describe("knowledge directory sorting", () => {
|
||||||
|
it("moves root directories within the same parent", async () => {
|
||||||
|
const { moveDirectoryWithinParent } = await loadModule()
|
||||||
|
|
||||||
|
const next = moveDirectoryWithinParent(tree, 0, 3, 1)
|
||||||
|
|
||||||
|
assert.deepEqual(plain(next.items).map((item) => item.id), [3, 1, 2])
|
||||||
|
assert.equal(next.changed, true)
|
||||||
|
assert.equal(next.parentId, 0)
|
||||||
|
assert.deepEqual(plain(next.orderedIds), [3, 1, 2])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("moves child directories within their parent", async () => {
|
||||||
|
const { moveDirectoryWithinParent } = await loadModule()
|
||||||
|
|
||||||
|
const next = moveDirectoryWithinParent(tree, 1, 12, 11)
|
||||||
|
|
||||||
|
assert.deepEqual(plain(next.items[0].children).map((item) => item.id), [12, 11])
|
||||||
|
assert.equal(next.changed, true)
|
||||||
|
assert.equal(next.parentId, 1)
|
||||||
|
assert.deepEqual(plain(next.orderedIds), [12, 11])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not move directories across parents", async () => {
|
||||||
|
const { findDirectoryParentId, moveDirectoryWithinParent } = await loadModule()
|
||||||
|
|
||||||
|
assert.equal(findDirectoryParentId(tree, 11), 1)
|
||||||
|
assert.equal(findDirectoryParentId(tree, 2), 0)
|
||||||
|
|
||||||
|
const next = moveDirectoryWithinParent(tree, 1, 11, 2)
|
||||||
|
|
||||||
|
assert.equal(next.changed, false)
|
||||||
|
assert.equal(next.items, tree)
|
||||||
|
assert.deepEqual(plain(next.orderedIds), [])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
export type SortableKnowledgeDirectory = {
|
||||||
|
id: number
|
||||||
|
parentId: number
|
||||||
|
children?: SortableKnowledgeDirectory[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MoveDirectoryResult<T extends SortableKnowledgeDirectory> = {
|
||||||
|
items: T[]
|
||||||
|
changed: boolean
|
||||||
|
parentId: number
|
||||||
|
orderedIds: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrayMove<T>(items: T[], fromIndex: number, toIndex: number) {
|
||||||
|
const next = [...items]
|
||||||
|
const [item] = next.splice(fromIndex, 1)
|
||||||
|
next.splice(toIndex, 0, item)
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findDirectoryParentId<T extends SortableKnowledgeDirectory>(
|
||||||
|
items: T[],
|
||||||
|
id: number,
|
||||||
|
): number | null {
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.id === id) {
|
||||||
|
return item.parentId
|
||||||
|
}
|
||||||
|
const childParentId = findDirectoryParentId(item.children as T[] | undefined ?? [], id)
|
||||||
|
if (childParentId !== null) {
|
||||||
|
return childParentId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function moveDirectoryWithinParent<T extends SortableKnowledgeDirectory>(
|
||||||
|
items: T[],
|
||||||
|
parentId: number,
|
||||||
|
activeId: number,
|
||||||
|
overId: number,
|
||||||
|
): MoveDirectoryResult<T> {
|
||||||
|
if (activeId === overId) {
|
||||||
|
return { items, changed: false, parentId, orderedIds: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentId === 0) {
|
||||||
|
return moveSiblingList(items, parentId, activeId, overId)
|
||||||
|
}
|
||||||
|
|
||||||
|
let changed = false
|
||||||
|
let orderedIds: number[] = []
|
||||||
|
const nextItems = items.map((item) => {
|
||||||
|
if (item.id === parentId) {
|
||||||
|
const moved = moveSiblingList((item.children as T[] | undefined) ?? [], parentId, activeId, overId)
|
||||||
|
changed = moved.changed
|
||||||
|
orderedIds = moved.orderedIds
|
||||||
|
return { ...item, children: moved.items }
|
||||||
|
}
|
||||||
|
if (item.children?.length) {
|
||||||
|
const moved = moveDirectoryWithinParent(item.children as T[], parentId, activeId, overId)
|
||||||
|
if (moved.changed) {
|
||||||
|
changed = true
|
||||||
|
orderedIds = moved.orderedIds
|
||||||
|
return { ...item, children: moved.items }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: changed ? nextItems : items,
|
||||||
|
changed,
|
||||||
|
parentId,
|
||||||
|
orderedIds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveSiblingList<T extends SortableKnowledgeDirectory>(
|
||||||
|
items: T[],
|
||||||
|
parentId: number,
|
||||||
|
activeId: number,
|
||||||
|
overId: number,
|
||||||
|
): MoveDirectoryResult<T> {
|
||||||
|
const oldIndex = items.findIndex((item) => item.id === activeId)
|
||||||
|
const newIndex = items.findIndex((item) => item.id === overId)
|
||||||
|
if (oldIndex < 0 || newIndex < 0) {
|
||||||
|
return { items, changed: false, parentId, orderedIds: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextItems = arrayMove(items, oldIndex, newIndex)
|
||||||
|
return {
|
||||||
|
items: nextItems,
|
||||||
|
changed: true,
|
||||||
|
parentId,
|
||||||
|
orderedIds: nextItems.map((item) => item.id),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1663,6 +1663,17 @@ export function deleteKnowledgeDirectory(id: number) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateKnowledgeDirectorySort(payload: {
|
||||||
|
knowledgeBaseId: number
|
||||||
|
parentId: number
|
||||||
|
ids: number[]
|
||||||
|
}) {
|
||||||
|
return request<void>("/api/dashboard/knowledge-directory/update_sort", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchKnowledgeDocuments(
|
export function fetchKnowledgeDocuments(
|
||||||
query?: Record<string, string | number | undefined>
|
query?: Record<string, string | number | undefined>
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1804,6 +1804,8 @@
|
|||||||
"directoryDeleted": "Directory deleted: {name}",
|
"directoryDeleted": "Directory deleted: {name}",
|
||||||
"directorySaveFailed": "Could not save the directory.",
|
"directorySaveFailed": "Could not save the directory.",
|
||||||
"directoryDeleteFailed": "Could not delete the directory.",
|
"directoryDeleteFailed": "Could not delete the directory.",
|
||||||
|
"directorySortUpdated": "Directory order updated.",
|
||||||
|
"directorySortUpdateFailed": "Could not update directory order.",
|
||||||
"directoryNameRequired": "Directory name is required.",
|
"directoryNameRequired": "Directory name is required.",
|
||||||
"rebuildStarted": "Knowledge base reindex started: {name}",
|
"rebuildStarted": "Knowledge base reindex started: {name}",
|
||||||
"rebuildFailed": "Could not rebuild the knowledge base index.",
|
"rebuildFailed": "Could not rebuild the knowledge base index.",
|
||||||
|
|||||||
@@ -1804,6 +1804,8 @@
|
|||||||
"directoryDeleted": "已删除目录:{name}",
|
"directoryDeleted": "已删除目录:{name}",
|
||||||
"directorySaveFailed": "保存目录失败",
|
"directorySaveFailed": "保存目录失败",
|
||||||
"directoryDeleteFailed": "删除目录失败",
|
"directoryDeleteFailed": "删除目录失败",
|
||||||
|
"directorySortUpdated": "目录排序已更新",
|
||||||
|
"directorySortUpdateFailed": "更新目录排序失败",
|
||||||
"directoryNameRequired": "目录名称不能为空",
|
"directoryNameRequired": "目录名称不能为空",
|
||||||
"rebuildStarted": "已开始重建知识库索引:{name}",
|
"rebuildStarted": "已开始重建知识库索引:{name}",
|
||||||
"rebuildFailed": "重建知识库索引失败",
|
"rebuildFailed": "重建知识库索引失败",
|
||||||
|
|||||||
Reference in New Issue
Block a user