修复项目无法删除问题
This commit is contained in:
@@ -151,22 +151,23 @@ describe("AdminPage active tickets", () => {
|
||||
expect(screen.queryByRole("combobox", { name: "预计等待时间方式" })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("textbox", { name: "游客官方提示" })).toHaveValue("请在入口附近等候。");
|
||||
expect(screen.getByRole("button", { name: "保存项目" })).toBeVisible();
|
||||
expect(screen.getByRole("button", { name: "删除项目" })).toBeVisible();
|
||||
expect(screen.getByRole("button", { name: "移除项目" })).toBeVisible();
|
||||
expect(screen.getByText("有运营历史的已结束项目将从所有项目列表隐藏,历史记录仍保留;无运营历史项目会永久删除。")).toBeVisible();
|
||||
});
|
||||
|
||||
it("确认后删除项目,刷新数据并返回列表", async () => {
|
||||
it("确认后归档或移除项目,刷新数据并返回列表", async () => {
|
||||
let resolveDelete!: () => void;
|
||||
const deleteProject = vi.spyOn(api, "deleteProject").mockImplementation(() => new Promise<void>((resolve) => { resolveDelete = resolve; }));
|
||||
refreshMock.mockResolvedValueOnce(undefined);
|
||||
try {
|
||||
render(<MemoryRouter initialEntries={["/admin/projects/project-1"]}><AdminPage /></MemoryRouter>);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "删除项目" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "移除项目" }));
|
||||
expect(deleteProject).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole("button", { name: "确认删除" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "确认移除" }));
|
||||
|
||||
await waitFor(() => expect(deleteProject).toHaveBeenCalledWith("project-1"));
|
||||
const deletingButton = screen.getByRole("button", { name: "正在删除" });
|
||||
const deletingButton = screen.getByRole("button", { name: "正在移除" });
|
||||
expect(deletingButton).toBeDisabled();
|
||||
fireEvent.click(deletingButton);
|
||||
expect(deleteProject).toHaveBeenCalledTimes(1);
|
||||
@@ -178,15 +179,16 @@ describe("AdminPage active tickets", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("删除项目失败时留在维护页并显示服务端消息", async () => {
|
||||
it("归档或移除项目失败时留在维护页并显示服务端消息", async () => {
|
||||
const deleteProject = vi.spyOn(api, "deleteProject").mockRejectedValue(new ApiError("项目存在活动排队,无法删除", 409));
|
||||
try {
|
||||
render(<MemoryRouter initialEntries={["/admin/projects/project-1"]}><AdminPage /></MemoryRouter>);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "删除项目" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "确认删除" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "移除项目" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "确认移除" }));
|
||||
|
||||
expect(await screen.findByText("项目存在活动排队,无法删除")).toBeVisible();
|
||||
expect(screen.getByText("移除失败")).toBeVisible();
|
||||
expect(screen.getByRole("heading", { name: "东门观光车" })).toBeVisible();
|
||||
} finally {
|
||||
vi.restoreAllMocks();
|
||||
|
||||
@@ -353,7 +353,7 @@ function ProjectMaintenance({ project, onRefresh }: { project?: AdminProjectDto;
|
||||
await onRefresh();
|
||||
navigate("/admin/projects", { replace: true });
|
||||
} catch (caught) {
|
||||
setDeleteError(caught instanceof ApiError ? caught.message : "项目删除失败,请稍后重试。");
|
||||
setDeleteError(caught instanceof ApiError ? caught.message : "项目移除失败,请稍后重试。");
|
||||
setDeleting(false);
|
||||
}
|
||||
};
|
||||
@@ -363,15 +363,15 @@ function ProjectMaintenance({ project, onRefresh }: { project?: AdminProjectDto;
|
||||
<ProjectForm project={project} onSaved={onRefresh} />
|
||||
<section className="panel" aria-label="危险操作">
|
||||
<div className="panel__header">
|
||||
<div><h3>危险操作</h3><p>删除后无法撤销,请确认不再需要该项目。</p></div>
|
||||
<div><h3>危险操作</h3><p>有运营历史的已结束项目将从所有项目列表隐藏,历史记录仍保留;无运营历史项目会永久删除。</p></div>
|
||||
{!confirmingDelete
|
||||
? <button className="button button--danger" type="button" onClick={() => { setConfirmingDelete(true); setDeleteError(null); }}>删除项目</button>
|
||||
? <button className="button button--danger" type="button" onClick={() => { setConfirmingDelete(true); setDeleteError(null); }}>移除项目</button>
|
||||
: <div>
|
||||
<button className="button button--secondary" type="button" onClick={() => setConfirmingDelete(false)} disabled={deleting}>取消</button>{" "}
|
||||
<button className="button button--danger" type="button" onClick={deleteProject} disabled={deleting}>{deleting ? "正在删除" : "确认删除"}</button>
|
||||
<button className="button button--danger" type="button" onClick={deleteProject} disabled={deleting}>{deleting ? "正在移除" : "确认移除"}</button>
|
||||
</div>}
|
||||
</div>
|
||||
{deleteError ? <FeedbackBanner tone="danger" title="删除失败">{deleteError}</FeedbackBanner> : null}
|
||||
{deleteError ? <FeedbackBanner tone="danger" title="移除失败">{deleteError}</FeedbackBanner> : null}
|
||||
</section>
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { act, fireEvent, render, screen, waitFor, within } from "@testing-librar
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
|
||||
const staffPageTestState = vi.hoisted(() => ({ projectStatus: "RUNNING" }));
|
||||
const staffPageTestState = vi.hoisted(() => ({ projectStatus: "RUNNING", projectsVisible: true, exerciseQueueLoad: false }));
|
||||
|
||||
vi.mock("../components/AppShell", () => ({
|
||||
AppShell: ({ children, variant }: { children: React.ReactNode; variant?: string }) => (
|
||||
@@ -10,12 +10,12 @@ vi.mock("../components/AppShell", () => ({
|
||||
),
|
||||
}));
|
||||
vi.mock("../hooks/usePollingResource", () => ({
|
||||
usePollingResource: (_load: unknown, options: { intervalMs: number }) => options.intervalMs === 60_000
|
||||
usePollingResource: (_load: unknown, options: { intervalMs: number; enabled?: boolean }) => options.intervalMs === 60_000
|
||||
? {
|
||||
data: { projects: [
|
||||
data: { projects: staffPageTestState.projectsVisible ? [
|
||||
{ id: "project-1", name: "东门观光车", status: staffPageTestState.projectStatus, batch_size: 2, call_mode: "BOTH", default_call_ticket_count: 2, max_call_ticket_count: 20, default_call_people_count: 5, max_call_people_count: 30, min_party_size: 1, max_party_size: 8 },
|
||||
{ id: "project-2", name: "西门观光车", status: "RUNNING", batch_size: 2, call_mode: "BOTH", default_call_ticket_count: 2, max_call_ticket_count: 20, default_call_people_count: 5, max_call_people_count: 30, min_party_size: 1, max_party_size: 8 },
|
||||
] },
|
||||
] : [] },
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
error: null,
|
||||
@@ -23,7 +23,7 @@ vi.mock("../hooks/usePollingResource", () => ({
|
||||
lastClientSuccessAt: new Date().toISOString(),
|
||||
refresh: vi.fn(),
|
||||
}
|
||||
: {
|
||||
: (staffPageTestState.exerciseQueueLoad && options.enabled !== false && (_load as (signal: AbortSignal) => unknown)(new AbortController().signal), {
|
||||
data: {
|
||||
project: { id: "project-1", name: "东门观光车", status: staffPageTestState.projectStatus, batch_size: 2, call_mode: "BOTH", default_call_ticket_count: 2, max_call_ticket_count: 20, default_call_people_count: 5, max_call_people_count: 30, min_party_size: 1, max_party_size: 8 },
|
||||
revision: 8,
|
||||
@@ -70,7 +70,7 @@ vi.mock("../hooks/usePollingResource", () => ({
|
||||
offline: false,
|
||||
lastClientSuccessAt: new Date().toISOString(),
|
||||
refresh: vi.fn(),
|
||||
},
|
||||
}),
|
||||
}));
|
||||
vi.mock("../auth/AuthContext", () => ({
|
||||
useAuth: () => ({ user: { id: "staff-1", username: "staff01", display_name: "小李", role: "STAFF" } }),
|
||||
@@ -86,9 +86,28 @@ function renderScene(path: string) {
|
||||
describe("StaffPage scene-focused H5", () => {
|
||||
beforeEach(() => {
|
||||
staffPageTestState.projectStatus = "RUNNING";
|
||||
staffPageTestState.projectsVisible = true;
|
||||
staffPageTestState.exerciseQueueLoad = false;
|
||||
sessionStorage.setItem("scenic-current-project", "project-1");
|
||||
});
|
||||
|
||||
it("clears a removed project's cached selection and queue after a successful empty project response", async () => {
|
||||
staffPageTestState.exerciseQueueLoad = true;
|
||||
const queue = vi.spyOn(api, "queue").mockResolvedValue({} as never);
|
||||
const view = renderScene("/staff");
|
||||
expect(queue).toHaveBeenCalledWith("project-1", expect.any(AbortSignal));
|
||||
expect(screen.getByText("00012")).toBeVisible();
|
||||
|
||||
queue.mockClear();
|
||||
staffPageTestState.projectsVisible = false;
|
||||
view.rerender(<MemoryRouter initialEntries={["/staff"]}><StaffPage /></MemoryRouter>);
|
||||
|
||||
await waitFor(() => expect(sessionStorage.getItem("scenic-current-project")).toBeNull());
|
||||
expect(queue).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText("东门观光车")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("00012")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("allows calling but blocks ticket creation while the project is paused", () => {
|
||||
staffPageTestState.projectStatus = "PAUSED";
|
||||
renderScene("/staff");
|
||||
|
||||
@@ -106,14 +106,23 @@ export function StaffPage() {
|
||||
const callIntentRef = useRef<WriteIntent | null>(null);
|
||||
|
||||
const projects = projectResource.data?.projects ?? [];
|
||||
const hasSuccessfulEmptyProjectList = Boolean(projectResource.data && !projectResource.loading && !projectResource.error && projects.length === 0);
|
||||
const activeProjectId = hasSuccessfulEmptyProjectList ? "" : selectedProjectId;
|
||||
useEffect(() => {
|
||||
if (hasSuccessfulEmptyProjectList) {
|
||||
setSelectedProjectId("");
|
||||
setPendingProjectId("");
|
||||
setShowProjectSwitcher(false);
|
||||
sessionStorage.removeItem("scenic-current-project");
|
||||
return;
|
||||
}
|
||||
if (!projects.length) return;
|
||||
const available = projects.some((project) => project.id === selectedProjectId);
|
||||
if (!available) {
|
||||
setSelectedProjectId(projects[0].id);
|
||||
setPendingProjectId(projects[0].id);
|
||||
}
|
||||
}, [projects, selectedProjectId]);
|
||||
}, [hasSuccessfulEmptyProjectList, projects, selectedProjectId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProjectId) sessionStorage.setItem("scenic-current-project", selectedProjectId);
|
||||
@@ -130,10 +139,10 @@ export function StaffPage() {
|
||||
}, [notice]);
|
||||
|
||||
const queueResource = usePollingResource(
|
||||
(signal) => api.queue(selectedProjectId, signal),
|
||||
{ enabled: Boolean(selectedProjectId), intervalMs: 5_000, resourceKey: selectedProjectId },
|
||||
(signal) => api.queue(activeProjectId, signal),
|
||||
{ enabled: Boolean(activeProjectId), intervalMs: 5_000, resourceKey: activeProjectId },
|
||||
);
|
||||
const queue = queueResource.data;
|
||||
const queue = activeProjectId ? queueResource.data : undefined;
|
||||
const selectedProject = queue?.project ?? projects.find((project) => project.id === selectedProjectId);
|
||||
const callMode = selectedProject?.call_mode ?? "TICKET";
|
||||
const supportsTicketCall = callMode === "TICKET" || callMode === "BOTH";
|
||||
|
||||
Reference in New Issue
Block a user