R34: 收口搜索查看更多跳转与采购 submit 二次减重
这个提交包含在:
@@ -1,7 +1,7 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { searchGlobal } from "../api/search";
|
||||
import { searchGlobal, type GlobalSearchHit } from "../api/search";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { Button } from "./ui/button";
|
||||
@@ -56,10 +56,46 @@ function resolveSearchGroup(entityType: string): SearchGroupKey {
|
||||
return "other";
|
||||
}
|
||||
|
||||
function resolveBrowseHref(groupKey: SearchGroupKey, items: GlobalSearchHit[]) {
|
||||
const firstItem = items[0];
|
||||
if (!firstItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (groupKey === "people") {
|
||||
return "/people";
|
||||
}
|
||||
|
||||
if (groupKey === "documents") {
|
||||
if (firstItem.entityType === "reimbursement") return "/reimbursements";
|
||||
if (firstItem.entityType === "leave") return "/leave-requests";
|
||||
if (firstItem.entityType === "purchase_request") return "/purchase-requests";
|
||||
if (firstItem.entityType === "travel_request") return "/travel-requests";
|
||||
if (firstItem.entityType === "weekly_report") return "/weekly-reports";
|
||||
if (firstItem.entityType === "todo") return "/todos";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (groupKey === "projectAnnouncement") {
|
||||
return firstItem.entityType === "announcement" ? "/announcements" : "/projects";
|
||||
}
|
||||
|
||||
if (firstItem.entityType === "asset") return "/assets";
|
||||
if (["contract_registry", "confidential_doc_ref"].includes(firstItem.entityType)) {
|
||||
return "/contracts-registry";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function GlobalSearchDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
|
||||
const navigate = useNavigate();
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const trimmed = keyword.trim();
|
||||
const closeAndNavigate = (targetHref: string) => {
|
||||
onOpenChange(false);
|
||||
window.setTimeout(() => navigate(targetHref), 0);
|
||||
};
|
||||
const searchQuery = useQuery({
|
||||
enabled: open && trimmed.length >= 2,
|
||||
queryKey: ["global-search", trimmed],
|
||||
@@ -74,6 +110,7 @@ export function GlobalSearchDialog({ open, onOpenChange }: { open: boolean; onOp
|
||||
...group,
|
||||
total: matches.length,
|
||||
items: matches.slice(0, 3),
|
||||
browseHref: resolveBrowseHref(group.key, matches),
|
||||
};
|
||||
}).filter((group) => group.total > 0);
|
||||
}, [searchQuery.data?.items]);
|
||||
@@ -93,7 +130,7 @@ export function GlobalSearchDialog({ open, onOpenChange }: { open: boolean; onOp
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogContent className="max-h-[85vh] max-w-2xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>全局搜索</DialogTitle>
|
||||
</DialogHeader>
|
||||
@@ -121,10 +158,7 @@ export function GlobalSearchDialog({ open, onOpenChange }: { open: boolean; onOp
|
||||
key={`${item.entityType}-${item.entityId}`}
|
||||
type="button"
|
||||
className="w-full rounded-lg border border-zinc-200 px-3 py-3 text-left hover:bg-zinc-50"
|
||||
onClick={() => {
|
||||
onOpenChange(false);
|
||||
navigate(item.href);
|
||||
}}
|
||||
onClick={() => closeAndNavigate(item.href)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="font-medium text-zinc-900">{item.title}</div>
|
||||
@@ -136,8 +170,18 @@ export function GlobalSearchDialog({ open, onOpenChange }: { open: boolean; onOp
|
||||
))}
|
||||
</div>
|
||||
|
||||
{group.total > group.items.length ? (
|
||||
<div className="text-xs text-zinc-400">查看更多结果</div>
|
||||
{group.total > group.items.length && group.browseHref ? (
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs font-medium text-zinc-500 transition-colors hover:text-zinc-800"
|
||||
onClick={() => {
|
||||
const browseHref = group.browseHref;
|
||||
if (!browseHref) return;
|
||||
closeAndNavigate(browseHref);
|
||||
}}
|
||||
>
|
||||
查看更多结果
|
||||
</button>
|
||||
) : null}
|
||||
</section>
|
||||
))}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { test, expect, type Page } from "@playwright/test";
|
||||
|
||||
import { ADMIN, login } from "./helpers/auth";
|
||||
|
||||
@@ -9,7 +9,7 @@ function envelope<T>(data: T) {
|
||||
};
|
||||
}
|
||||
|
||||
test("E2E 全局搜索:按钮打开后按分组展示结果并支持点击跳转", async ({ page }) => {
|
||||
async function mockSearchResults(page: Page) {
|
||||
await page.route("**/api/search?**", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
@@ -30,6 +30,10 @@ test("E2E 全局搜索:按钮打开后按分组展示结果并支持点击跳
|
||||
),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test("E2E 全局搜索:按钮打开后按分组展示结果并支持点击跳转", async ({ page }) => {
|
||||
await mockSearchResults(page);
|
||||
|
||||
await login(page, ADMIN);
|
||||
await page.goto("/portal");
|
||||
@@ -56,6 +60,26 @@ test("E2E 全局搜索:按钮打开后按分组展示结果并支持点击跳
|
||||
await expect(page).toHaveURL(/\/reimbursements\/reimbursement-1/);
|
||||
});
|
||||
|
||||
test("E2E 全局搜索:查看更多结果会关闭弹窗并跳到分组列表页", async ({ page }) => {
|
||||
await mockSearchResults(page);
|
||||
|
||||
await login(page, ADMIN);
|
||||
await page.goto("/portal");
|
||||
await page.getByTestId("global-search-trigger").click();
|
||||
await expect(page.getByRole("heading", { name: "全局搜索" })).toBeVisible();
|
||||
|
||||
const searchResponse = page.waitForResponse((response) => response.url().includes("/api/search?"));
|
||||
await page.getByPlaceholder("搜索人员、项目、合同、公告、资产、请假、报销、周报、待办").fill("海音");
|
||||
await searchResponse;
|
||||
|
||||
const documentsSection = page.locator("section").filter({ has: page.getByRole("heading", { name: "业务单据" }) });
|
||||
await expect(documentsSection.getByRole("button", { name: "查看更多结果" })).toBeVisible();
|
||||
await documentsSection.getByRole("button", { name: "查看更多结果" }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/reimbursements$/);
|
||||
await expect(page.getByRole("heading", { name: "全局搜索" })).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("E2E 全局搜索:Ctrl+K 和 Cmd+K 可打开搜索弹窗", async ({ page }) => {
|
||||
await login(page, ADMIN);
|
||||
await page.goto("/portal");
|
||||
|
||||
在新工单中引用
屏蔽一个用户