feat: implement TagSelector component for improved tag management across dashboard features
- Added TagSelector component to streamline tag selection in various components. - Refactored existing tag handling in DashboardConversationsPage, ConversationTagPicker, EditDialog, and TicketsPage to utilize the new TagSelector. - Removed redundant tag handling functions and optimized state management for tags. - Updated tests to ensure proper functionality of the new tag handling logic.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import assert from "node:assert/strict"
|
||||
import { readFile } from "node:fs/promises"
|
||||
import test from "node:test"
|
||||
import ts from "typescript"
|
||||
import vm from "node:vm"
|
||||
|
||||
async function loadModule() {
|
||||
const source = await readFile(new URL("./tag-tree.ts", import.meta.url), "utf8")
|
||||
const compiled = ts.transpileModule(source, {
|
||||
compilerOptions: {
|
||||
target: ts.ScriptTarget.ES2017,
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
},
|
||||
fileName: "tag-tree.ts",
|
||||
})
|
||||
const sandbox = {
|
||||
exports: {},
|
||||
module: { exports: {} },
|
||||
}
|
||||
sandbox.exports = sandbox.module.exports
|
||||
vm.runInNewContext(compiled.outputText, sandbox)
|
||||
return sandbox.module.exports
|
||||
}
|
||||
|
||||
const tags = [
|
||||
{
|
||||
id: 1,
|
||||
parentId: 0,
|
||||
name: "产品",
|
||||
remark: "",
|
||||
sortNo: 1,
|
||||
status: 0,
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
children: [
|
||||
{
|
||||
id: 2,
|
||||
parentId: 1,
|
||||
name: "退款",
|
||||
remark: "售后",
|
||||
sortNo: 1,
|
||||
status: 0,
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
parentId: 0,
|
||||
name: "技术",
|
||||
remark: "",
|
||||
sortNo: 2,
|
||||
status: 0,
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
children: [],
|
||||
},
|
||||
]
|
||||
|
||||
test("flattens tag tree with depth and full path", async () => {
|
||||
const { flattenTagTree } = await loadModule()
|
||||
|
||||
assert.equal(
|
||||
JSON.stringify(flattenTagTree(tags).map((item) => ({
|
||||
id: item.id,
|
||||
depth: item.depth,
|
||||
path: item.path,
|
||||
searchableText: item.searchableText,
|
||||
}))),
|
||||
JSON.stringify([
|
||||
{ id: 1, depth: 0, path: "产品", searchableText: "产品 1 " },
|
||||
{ id: 2, depth: 1, path: "产品 / 退款", searchableText: "产品 / 退款 2 售后" },
|
||||
{ id: 3, depth: 0, path: "技术", searchableText: "技术 3 " },
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
test("excludes a tag and its descendants for parent selection", async () => {
|
||||
const { flattenTagTree } = await loadModule()
|
||||
|
||||
assert.equal(
|
||||
JSON.stringify(flattenTagTree(tags, { excludeIds: [1] }).map((item) => item.id)),
|
||||
JSON.stringify([3])
|
||||
)
|
||||
})
|
||||
|
||||
test("builds full-path map for selected tag badges", async () => {
|
||||
const { buildTagPathMap } = await loadModule()
|
||||
|
||||
assert.equal(buildTagPathMap(tags).get(2), "产品 / 退款")
|
||||
})
|
||||
|
||||
test("flattens only visible branches when a parent is collapsed", async () => {
|
||||
const { flattenVisibleTagTree } = await loadModule()
|
||||
|
||||
assert.equal(
|
||||
JSON.stringify(
|
||||
flattenVisibleTagTree(tags, { collapsedIds: [1] }).map((item) => item.id)
|
||||
),
|
||||
JSON.stringify([1, 3])
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { TagTree } from "@/lib/api/admin"
|
||||
|
||||
export type FlatTagNode = TagTree & {
|
||||
depth: number
|
||||
path: string
|
||||
searchableText: string
|
||||
}
|
||||
|
||||
type FlattenTagTreeOptions = {
|
||||
excludeIds?: number[]
|
||||
}
|
||||
|
||||
type FlattenVisibleTagTreeOptions = FlattenTagTreeOptions & {
|
||||
collapsedIds?: number[]
|
||||
}
|
||||
|
||||
export function flattenTagTree(
|
||||
nodes: TagTree[] | null | undefined,
|
||||
options: FlattenTagTreeOptions = {}
|
||||
): FlatTagNode[] {
|
||||
const excluded = new Set(options.excludeIds ?? [])
|
||||
const result: FlatTagNode[] = []
|
||||
|
||||
function walk(items: TagTree[] | null | undefined, depth: number, parentPath: string) {
|
||||
const safeItems = Array.isArray(items) ? items : []
|
||||
|
||||
safeItems.forEach((item) => {
|
||||
if (excluded.has(item.id)) {
|
||||
return
|
||||
}
|
||||
|
||||
const path = parentPath ? `${parentPath} / ${item.name}` : item.name
|
||||
result.push({
|
||||
...item,
|
||||
depth,
|
||||
path,
|
||||
searchableText: `${path} ${item.id} ${item.remark ?? ""}`,
|
||||
})
|
||||
walk(item.children, depth + 1, path)
|
||||
})
|
||||
}
|
||||
|
||||
walk(nodes, 0, "")
|
||||
return result
|
||||
}
|
||||
|
||||
export function buildTagPathMap(
|
||||
nodes: TagTree[] | null | undefined
|
||||
): Map<number, string> {
|
||||
const result = new Map<number, string>()
|
||||
flattenTagTree(nodes).forEach((item) => {
|
||||
result.set(item.id, item.path)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
export function flattenVisibleTagTree(
|
||||
nodes: TagTree[] | null | undefined,
|
||||
options: FlattenVisibleTagTreeOptions = {}
|
||||
): FlatTagNode[] {
|
||||
const excluded = new Set(options.excludeIds ?? [])
|
||||
const collapsed = new Set(options.collapsedIds ?? [])
|
||||
const result: FlatTagNode[] = []
|
||||
|
||||
function walk(items: TagTree[] | null | undefined, depth: number, parentPath: string) {
|
||||
const safeItems = Array.isArray(items) ? items : []
|
||||
|
||||
safeItems.forEach((item) => {
|
||||
if (excluded.has(item.id)) {
|
||||
return
|
||||
}
|
||||
|
||||
const path = parentPath ? `${parentPath} / ${item.name}` : item.name
|
||||
result.push({
|
||||
...item,
|
||||
depth,
|
||||
path,
|
||||
searchableText: `${path} ${item.id} ${item.remark ?? ""}`,
|
||||
})
|
||||
|
||||
if (!collapsed.has(item.id)) {
|
||||
walk(item.children, depth + 1, path)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
walk(nodes, 0, "")
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user