fix: scope H5 config by app code
This commit is contained in:
parent
ef260df749
commit
9065382192
@ -1214,11 +1214,16 @@
|
||||
}
|
||||
},
|
||||
"/admin/app-config/h5-links": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/AppCodeHeader"
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"operationId": "listH5Links",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
"$ref": "#/components/responses/H5LinkListResponse"
|
||||
}
|
||||
},
|
||||
"x-permission": "app-config:view",
|
||||
@ -1226,9 +1231,19 @@
|
||||
},
|
||||
"post": {
|
||||
"operationId": "createH5Link",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/H5LinkPayload"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
"201": {
|
||||
"$ref": "#/components/responses/H5LinkResponse"
|
||||
}
|
||||
},
|
||||
"x-permission": "app-config:update",
|
||||
@ -1236,9 +1251,19 @@
|
||||
},
|
||||
"put": {
|
||||
"operationId": "updateH5Links",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/H5LinkBatchUpdateInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
"$ref": "#/components/responses/H5LinkListResponse"
|
||||
}
|
||||
},
|
||||
"x-permission": "app-config:update",
|
||||
@ -1246,11 +1271,26 @@
|
||||
}
|
||||
},
|
||||
"/admin/app-config/h5-links/{key}": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/AppCodeHeader"
|
||||
}
|
||||
],
|
||||
"put": {
|
||||
"operationId": "updateH5Link",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/H5LinkPayload"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
"$ref": "#/components/responses/H5LinkResponse"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
@ -1270,7 +1310,7 @@
|
||||
"operationId": "deleteH5Link",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
"$ref": "#/components/responses/H5LinkDeleteResponse"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
@ -6729,6 +6769,15 @@
|
||||
},
|
||||
"components": {
|
||||
"parameters": {
|
||||
"AppCodeHeader": {
|
||||
"name": "X-App-Code",
|
||||
"in": "header",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"AgencyId": {
|
||||
"name": "agency_id",
|
||||
"in": "query",
|
||||
@ -7167,6 +7216,36 @@
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"H5LinkListResponse": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseH5LinkList"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"H5LinkResponse": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseH5Link"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"H5LinkDeleteResponse": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseH5LinkDelete"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"AgencyPageResponse": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
@ -7609,6 +7688,143 @@
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"ApiResponseH5LinkList": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Envelope"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/H5LinkList"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ApiResponseH5Link": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Envelope"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/H5Link"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ApiResponseH5LinkDelete": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Envelope"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/H5LinkDeleteResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"H5Link": {
|
||||
"type": "object",
|
||||
"required": ["appCode", "key", "label", "url", "updatedAtMs"],
|
||||
"properties": {
|
||||
"appCode": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 80,
|
||||
"pattern": "^[A-Za-z0-9_.:-]+$"
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 2048,
|
||||
"pattern": "^\\S+$"
|
||||
},
|
||||
"updatedAtMs": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"H5LinkPayload": {
|
||||
"type": "object",
|
||||
"required": ["key", "label", "url"],
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 80,
|
||||
"pattern": "^[A-Za-z0-9_.:-]+$"
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 2048,
|
||||
"pattern": "^\\S+$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"H5LinkBatchUpdateInput": {
|
||||
"type": "object",
|
||||
"required": ["items"],
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/H5LinkPayload"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"H5LinkList": {
|
||||
"type": "object",
|
||||
"required": ["items", "total"],
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/H5Link"
|
||||
}
|
||||
},
|
||||
"total": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"H5LinkDeleteResult": {
|
||||
"type": "object",
|
||||
"required": ["deleted"],
|
||||
"properties": {
|
||||
"deleted": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApiPageAgency": {
|
||||
"type": "object",
|
||||
"required": ["items", "page", "pageSize", "total"],
|
||||
|
||||
@ -1,12 +1,61 @@
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { setAccessToken } from "@/shared/api/request";
|
||||
import { createSystemMessagePushFanout } from "./api";
|
||||
import { setAccessToken, setSelectedAppCode } from "@/shared/api/request";
|
||||
import {
|
||||
createH5Link,
|
||||
createSystemMessagePushFanout,
|
||||
deleteH5Link,
|
||||
listH5Links,
|
||||
updateH5Link,
|
||||
updateH5Links,
|
||||
} from "./api";
|
||||
|
||||
afterEach(() => {
|
||||
setAccessToken("");
|
||||
setSelectedAppCode("lalu");
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test("H5 config API pins every request to its explicit app scope", async () => {
|
||||
setSelectedAppCode("lalu");
|
||||
const link = {
|
||||
appCode: "huwaa",
|
||||
key: "achievement",
|
||||
label: "成就",
|
||||
updatedAtMs: 1000,
|
||||
url: "https://h5.example.com/achievement",
|
||||
};
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (_url, init) => {
|
||||
const method = String(init?.method || "GET");
|
||||
const data = method === "GET" || (method === "PUT" && !String(_url).endsWith("/achievement"))
|
||||
? { items: [link], total: 1 }
|
||||
: method === "DELETE"
|
||||
? { deleted: true }
|
||||
: link;
|
||||
return new Response(JSON.stringify({ code: 0, data }), { status: method === "POST" ? 201 : 200 });
|
||||
}),
|
||||
);
|
||||
|
||||
const payload = { key: link.key, label: link.label, url: link.url };
|
||||
await listH5Links(" Huwaa ");
|
||||
await createH5Link("huwaa", payload);
|
||||
await updateH5Links("huwaa", { items: [payload] });
|
||||
await updateH5Link("huwaa", link.key, payload);
|
||||
await deleteH5Link("huwaa", link.key);
|
||||
|
||||
const calls = vi.mocked(fetch).mock.calls;
|
||||
expect(calls.map(([, init]) => init?.method)).toEqual(["GET", "POST", "PUT", "PUT", "DELETE"]);
|
||||
calls.forEach(([, init]) => {
|
||||
expect(init?.headers).toMatchObject({ "X-App-Code": "huwaa" });
|
||||
});
|
||||
expect(String(calls[0][0])).toContain("/api/v1/admin/app-config/h5-links");
|
||||
expect(String(calls[3][0])).toContain("/api/v1/admin/app-config/h5-links/achievement");
|
||||
expect(JSON.parse(String(calls[1][1]?.body))).toEqual(payload);
|
||||
expect(JSON.parse(String(calls[2][1]?.body))).toEqual({ items: [payload] });
|
||||
expect(JSON.parse(String(calls[3][1]?.body))).toEqual(payload);
|
||||
});
|
||||
|
||||
test("system message push fanout API keeps existing operations path", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
|
||||
@ -53,47 +53,57 @@ export function createSystemMessagePushFanout(payload: SystemMessagePushPayload)
|
||||
);
|
||||
}
|
||||
|
||||
export function listH5Links(): Promise<ApiList<H5LinkConfigDto>> {
|
||||
export function listH5Links(appCode: string): Promise<ApiList<H5LinkConfigDto>> {
|
||||
const endpoint = API_ENDPOINTS.listH5Links;
|
||||
return apiRequest<ApiList<H5LinkConfigDto>>(apiEndpointPath(API_OPERATIONS.listH5Links), {
|
||||
headers: appScopeHeaders(appCode),
|
||||
method: endpoint.method,
|
||||
});
|
||||
}
|
||||
|
||||
export function updateH5Links(payload: H5LinkConfigUpdatePayload): Promise<ApiList<H5LinkConfigDto>> {
|
||||
export function updateH5Links(appCode: string, payload: H5LinkConfigUpdatePayload): Promise<ApiList<H5LinkConfigDto>> {
|
||||
const endpoint = API_ENDPOINTS.updateH5Links;
|
||||
return apiRequest<ApiList<H5LinkConfigDto>, H5LinkConfigUpdatePayload>(
|
||||
apiEndpointPath(API_OPERATIONS.updateH5Links),
|
||||
{
|
||||
body: payload,
|
||||
headers: appScopeHeaders(appCode),
|
||||
method: endpoint.method,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function createH5Link(payload: H5LinkConfigPayload): Promise<H5LinkConfigDto> {
|
||||
export function createH5Link(appCode: string, payload: H5LinkConfigPayload): Promise<H5LinkConfigDto> {
|
||||
const endpoint = API_ENDPOINTS.createH5Link;
|
||||
return apiRequest<H5LinkConfigDto, H5LinkConfigPayload>(apiEndpointPath(API_OPERATIONS.createH5Link), {
|
||||
body: payload,
|
||||
headers: appScopeHeaders(appCode),
|
||||
method: endpoint.method,
|
||||
});
|
||||
}
|
||||
|
||||
export function updateH5Link(key: EntityId, payload: H5LinkConfigPayload): Promise<H5LinkConfigDto> {
|
||||
export function updateH5Link(appCode: string, key: EntityId, payload: H5LinkConfigPayload): Promise<H5LinkConfigDto> {
|
||||
const endpoint = API_ENDPOINTS.updateH5Link;
|
||||
return apiRequest<H5LinkConfigDto, H5LinkConfigPayload>(apiEndpointPath(API_OPERATIONS.updateH5Link, { key }), {
|
||||
body: payload,
|
||||
headers: appScopeHeaders(appCode),
|
||||
method: endpoint.method,
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteH5Link(key: EntityId): Promise<{ deleted: boolean }> {
|
||||
export function deleteH5Link(appCode: string, key: EntityId): Promise<{ deleted: boolean }> {
|
||||
const endpoint = API_ENDPOINTS.deleteH5Link;
|
||||
return apiRequest<{ deleted: boolean }>(apiEndpointPath(API_OPERATIONS.deleteH5Link, { key }), {
|
||||
headers: appScopeHeaders(appCode),
|
||||
method: endpoint.method,
|
||||
});
|
||||
}
|
||||
|
||||
function appScopeHeaders(appCode: string) {
|
||||
// H5 配置是强 App 隔离数据;显式固定请求头可避免用户切换 App 时全局请求上下文变化导致写入错误租户。
|
||||
return { "X-App-Code": String(appCode || "").trim().toLowerCase() };
|
||||
}
|
||||
|
||||
export function listExploreTabs(query: PageQuery = {}): Promise<ApiList<ExploreTabDto>> {
|
||||
const endpoint = API_ENDPOINTS.listExploreTabs;
|
||||
return apiRequest<ApiList<ExploreTabDto>>(apiEndpointPath(API_OPERATIONS.listExploreTabs), {
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useAppScope } from "@/app/app-scope/AppScopeProvider.jsx";
|
||||
import { getSelectedAppCode } from "@/shared/api/request";
|
||||
import { parseForm } from "@/shared/forms/validation";
|
||||
import { useAdminQuery } from "@/shared/hooks/useAdminQuery.js";
|
||||
import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx";
|
||||
@ -11,45 +13,66 @@ const emptyData = { items: [], total: 0 };
|
||||
const emptyForm = () => ({ key: "", label: "", url: "" });
|
||||
|
||||
export function useH5ConfigPage() {
|
||||
const { appCode } = useAppScope();
|
||||
const abilities = useAppConfigAbilities();
|
||||
const confirm = useConfirm();
|
||||
const { showToast } = useToast();
|
||||
const [activeAction, setActiveAction] = useState("");
|
||||
const [actionAppCode, setActionAppCode] = useState("");
|
||||
const [editingItem, setEditingItem] = useState(null);
|
||||
const [form, setForm] = useState(emptyForm);
|
||||
const [loadingAction, setLoadingAction] = useState("");
|
||||
const queryFn = useCallback(() => listH5Links(), []);
|
||||
const queryFn = useCallback(() => listH5Links(appCode), [appCode]);
|
||||
const { data = emptyData, error, loading, reload } = useAdminQuery(queryFn, {
|
||||
enabled: Boolean(appCode),
|
||||
errorMessage: "加载 H5 配置失败",
|
||||
initialData: emptyData,
|
||||
queryKey: ["app-config", "h5-links"]
|
||||
queryKey: ["app-config", "h5-links", appCode]
|
||||
});
|
||||
|
||||
const openCreate = () => {
|
||||
setEditingItem(null);
|
||||
setForm(emptyForm());
|
||||
setActionAppCode(appCode);
|
||||
setActiveAction("create");
|
||||
};
|
||||
|
||||
const openEdit = (item) => {
|
||||
const itemAppCode = normalizeAppCode(item.appCode) || appCode;
|
||||
setEditingItem(item);
|
||||
setForm({ key: item.key || "", label: item.label || "", url: item.url || "" });
|
||||
// 响应中的 appCode 是该行数据的归属凭证;若页面正在切换,effect 会发现它与当前 scope 不一致并关闭弹窗。
|
||||
setActionAppCode(itemAppCode);
|
||||
setActiveAction("edit");
|
||||
};
|
||||
|
||||
const closeEdit = () => {
|
||||
const closeEdit = useCallback(() => {
|
||||
setActiveAction("");
|
||||
setActionAppCode("");
|
||||
setEditingItem(null);
|
||||
setForm(emptyForm());
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeAction || sameAppCode(actionAppCode, appCode)) {
|
||||
return;
|
||||
}
|
||||
// 弹窗内容来自旧 App;切换后必须丢弃,不能让同名 key 被保存到新 App。
|
||||
closeEdit();
|
||||
}, [activeAction, actionAppCode, appCode, closeEdit]);
|
||||
|
||||
const submitEdit = async (event) => {
|
||||
event.preventDefault();
|
||||
if (!isCurrentActionScope(actionAppCode, appCode)) {
|
||||
closeEdit();
|
||||
showToast("应用已切换,请重新操作", "warning");
|
||||
return;
|
||||
}
|
||||
if (!editingItem) {
|
||||
const payload = parseForm(h5LinkUpdateSchema, form);
|
||||
setLoadingAction("create");
|
||||
try {
|
||||
await createH5Link(payload);
|
||||
await createH5Link(actionAppCode, payload);
|
||||
closeEdit();
|
||||
await reload();
|
||||
showToast("H5配置已新增", "success");
|
||||
@ -63,7 +86,7 @@ export function useH5ConfigPage() {
|
||||
const payload = parseForm(h5LinkUpdateSchema, { ...form, key: editingItem.key });
|
||||
setLoadingAction("edit");
|
||||
try {
|
||||
await updateH5Link(editingItem.key, payload);
|
||||
await updateH5Link(actionAppCode, editingItem.key, payload);
|
||||
closeEdit();
|
||||
await reload();
|
||||
showToast("H5配置已更新", "success");
|
||||
@ -75,6 +98,7 @@ export function useH5ConfigPage() {
|
||||
};
|
||||
|
||||
const removeH5Link = async (item) => {
|
||||
const targetAppCode = normalizeAppCode(item.appCode) || appCode;
|
||||
const ok = await confirm({
|
||||
confirmText: "删除",
|
||||
message: item.label || item.key,
|
||||
@ -84,9 +108,13 @@ export function useH5ConfigPage() {
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
if (!isCurrentActionScope(targetAppCode, appCode)) {
|
||||
showToast("应用已切换,请重新操作", "warning");
|
||||
return;
|
||||
}
|
||||
setLoadingAction(`delete:${item.key}`);
|
||||
try {
|
||||
await deleteH5Link(item.key);
|
||||
await deleteH5Link(targetAppCode, item.key);
|
||||
await reload();
|
||||
showToast("H5配置已删除", "success");
|
||||
} catch (err) {
|
||||
@ -114,3 +142,18 @@ export function useH5ConfigPage() {
|
||||
submitEdit
|
||||
};
|
||||
}
|
||||
|
||||
function isCurrentActionScope(actionAppCode, appCode) {
|
||||
// Header 的全局值在 AppScope state 提交前已更新,双重校验能挡住切换瞬间点击提交的竞态。
|
||||
return Boolean(normalizeAppCode(actionAppCode))
|
||||
&& sameAppCode(actionAppCode, appCode)
|
||||
&& sameAppCode(actionAppCode, getSelectedAppCode());
|
||||
}
|
||||
|
||||
function sameAppCode(left, right) {
|
||||
return normalizeAppCode(left) === normalizeAppCode(right);
|
||||
}
|
||||
|
||||
function normalizeAppCode(value) {
|
||||
return String(value || "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
179
src/features/app-config/hooks/useH5ConfigPage.test.jsx
Normal file
179
src/features/app-config/hooks/useH5ConfigPage.test.jsx
Normal file
@ -0,0 +1,179 @@
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
||||
import { setSelectedAppCode } from "@/shared/api/request";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
appCode: "lalu",
|
||||
confirm: vi.fn(),
|
||||
createH5Link: vi.fn(),
|
||||
deleteH5Link: vi.fn(),
|
||||
listH5Links: vi.fn(),
|
||||
queryFn: null,
|
||||
queryOptions: null,
|
||||
reload: vi.fn(),
|
||||
showToast: vi.fn(),
|
||||
updateH5Link: vi.fn()
|
||||
}));
|
||||
|
||||
vi.mock("@/app/app-scope/AppScopeProvider.jsx", () => ({
|
||||
useAppScope: () => ({ appCode: mocks.appCode })
|
||||
}));
|
||||
|
||||
vi.mock("@/features/app-config/api", () => ({
|
||||
createH5Link: mocks.createH5Link,
|
||||
deleteH5Link: mocks.deleteH5Link,
|
||||
listH5Links: mocks.listH5Links,
|
||||
updateH5Link: mocks.updateH5Link
|
||||
}));
|
||||
|
||||
vi.mock("@/features/app-config/permissions.js", () => ({
|
||||
useAppConfigAbilities: () => ({ canUpdate: true, canView: true })
|
||||
}));
|
||||
|
||||
vi.mock("@/shared/hooks/useAdminQuery.js", () => ({
|
||||
useAdminQuery: (queryFn, options) => {
|
||||
mocks.queryFn = queryFn;
|
||||
mocks.queryOptions = options;
|
||||
return {
|
||||
data: { items: [], total: 0 },
|
||||
error: "",
|
||||
loading: false,
|
||||
reload: mocks.reload
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock("@/shared/ui/ConfirmProvider.jsx", () => ({
|
||||
useConfirm: () => mocks.confirm
|
||||
}));
|
||||
|
||||
vi.mock("@/shared/ui/ToastProvider.jsx", () => ({
|
||||
useToast: () => ({ showToast: mocks.showToast })
|
||||
}));
|
||||
|
||||
import { useH5ConfigPage } from "./useH5ConfigPage.js";
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.appCode = "lalu";
|
||||
mocks.confirm.mockResolvedValue(true);
|
||||
mocks.createH5Link.mockResolvedValue({});
|
||||
mocks.deleteH5Link.mockResolvedValue({ deleted: true });
|
||||
mocks.listH5Links.mockResolvedValue({ items: [], total: 0 });
|
||||
mocks.reload.mockResolvedValue({ items: [], total: 0 });
|
||||
mocks.updateH5Link.mockResolvedValue({});
|
||||
setSelectedAppCode("lalu");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
setSelectedAppCode("lalu");
|
||||
});
|
||||
|
||||
test("uses appCode in the H5 query key and read request", async () => {
|
||||
const { rerender } = renderHook(() => useH5ConfigPage());
|
||||
|
||||
expect(mocks.queryOptions.queryKey).toEqual(["app-config", "h5-links", "lalu"]);
|
||||
await mocks.queryFn();
|
||||
expect(mocks.listH5Links).toHaveBeenLastCalledWith("lalu");
|
||||
|
||||
mocks.appCode = "huwaa";
|
||||
setSelectedAppCode("huwaa");
|
||||
rerender();
|
||||
|
||||
expect(mocks.queryOptions.queryKey).toEqual(["app-config", "h5-links", "huwaa"]);
|
||||
await mocks.queryFn();
|
||||
expect(mocks.listH5Links).toHaveBeenLastCalledWith("huwaa");
|
||||
});
|
||||
|
||||
test("closes an edit dialog when the selected App changes", () => {
|
||||
const item = {
|
||||
appCode: "lalu",
|
||||
key: "achievement",
|
||||
label: "成就",
|
||||
updatedAtMs: 1000,
|
||||
url: "https://h5.example.com/achievement"
|
||||
};
|
||||
const { result, rerender } = renderHook(() => useH5ConfigPage());
|
||||
|
||||
act(() => result.current.openEdit(item));
|
||||
expect(result.current.activeAction).toBe("edit");
|
||||
|
||||
mocks.appCode = "huwaa";
|
||||
setSelectedAppCode("huwaa");
|
||||
rerender();
|
||||
|
||||
expect(result.current.activeAction).toBe("");
|
||||
expect(result.current.editingItem).toBeNull();
|
||||
expect(result.current.form).toEqual({ key: "", label: "", url: "" });
|
||||
});
|
||||
|
||||
test("does not submit stale form state after an App switch", async () => {
|
||||
const { result, rerender } = renderHook(() => useH5ConfigPage());
|
||||
|
||||
act(() => {
|
||||
result.current.openEdit({
|
||||
appCode: "lalu",
|
||||
key: "achievement",
|
||||
label: "成就",
|
||||
updatedAtMs: 1000,
|
||||
url: "https://h5.example.com/achievement"
|
||||
});
|
||||
});
|
||||
|
||||
mocks.appCode = "huwaa";
|
||||
setSelectedAppCode("huwaa");
|
||||
rerender();
|
||||
await act(async () => {
|
||||
await result.current.submitEdit({ preventDefault: vi.fn() });
|
||||
});
|
||||
|
||||
expect(mocks.createH5Link).not.toHaveBeenCalled();
|
||||
expect(mocks.updateH5Link).not.toHaveBeenCalled();
|
||||
expect(mocks.showToast).toHaveBeenCalledWith("应用已切换,请重新操作", "warning");
|
||||
});
|
||||
|
||||
test("pins an update mutation to the App that owns the edited row", async () => {
|
||||
const { result } = renderHook(() => useH5ConfigPage());
|
||||
|
||||
act(() => {
|
||||
result.current.openEdit({
|
||||
appCode: "lalu",
|
||||
key: "achievement",
|
||||
label: "成就",
|
||||
updatedAtMs: 1000,
|
||||
url: "https://h5.example.com/achievement"
|
||||
});
|
||||
});
|
||||
await act(async () => {
|
||||
await result.current.submitEdit({ preventDefault: vi.fn() });
|
||||
});
|
||||
|
||||
expect(mocks.updateH5Link).toHaveBeenCalledWith("lalu", "achievement", {
|
||||
key: "achievement",
|
||||
label: "成就",
|
||||
url: "https://h5.example.com/achievement"
|
||||
});
|
||||
});
|
||||
|
||||
test("aborts delete when App changes while confirmation is open", async () => {
|
||||
let resolveConfirm;
|
||||
mocks.confirm.mockReturnValue(new Promise((resolve) => {
|
||||
resolveConfirm = resolve;
|
||||
}));
|
||||
const { result, rerender } = renderHook(() => useH5ConfigPage());
|
||||
let pendingDelete;
|
||||
|
||||
act(() => {
|
||||
pendingDelete = result.current.removeH5Link({ key: "achievement", label: "成就" });
|
||||
});
|
||||
mocks.appCode = "huwaa";
|
||||
setSelectedAppCode("huwaa");
|
||||
rerender();
|
||||
await act(async () => {
|
||||
resolveConfirm(true);
|
||||
await pendingDelete;
|
||||
});
|
||||
|
||||
expect(mocks.deleteH5Link).not.toHaveBeenCalled();
|
||||
expect(mocks.showToast).toHaveBeenCalledWith("应用已切换,请重新操作", "warning");
|
||||
});
|
||||
116
src/shared/api/generated/schema.d.ts
vendored
116
src/shared/api/generated/schema.d.ts
vendored
@ -887,7 +887,9 @@ export interface paths {
|
||||
"/admin/app-config/h5-links": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
header: {
|
||||
"X-App-Code": components["parameters"]["AppCodeHeader"];
|
||||
};
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
@ -903,7 +905,9 @@ export interface paths {
|
||||
"/admin/app-config/h5-links/{key}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
header: {
|
||||
"X-App-Code": components["parameters"]["AppCodeHeader"];
|
||||
};
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
@ -4136,6 +4140,38 @@ export interface paths {
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
schemas: {
|
||||
ApiResponseH5LinkList: components["schemas"]["Envelope"] & {
|
||||
data?: components["schemas"]["H5LinkList"];
|
||||
};
|
||||
ApiResponseH5Link: components["schemas"]["Envelope"] & {
|
||||
data?: components["schemas"]["H5Link"];
|
||||
};
|
||||
ApiResponseH5LinkDelete: components["schemas"]["Envelope"] & {
|
||||
data?: components["schemas"]["H5LinkDeleteResult"];
|
||||
};
|
||||
H5Link: {
|
||||
appCode: string;
|
||||
key: string;
|
||||
label: string;
|
||||
url: string;
|
||||
/** Format: int64 */
|
||||
updatedAtMs: number;
|
||||
};
|
||||
H5LinkPayload: {
|
||||
key: string;
|
||||
label: string;
|
||||
url: string;
|
||||
};
|
||||
H5LinkBatchUpdateInput: {
|
||||
items: components["schemas"]["H5LinkPayload"][];
|
||||
};
|
||||
H5LinkList: {
|
||||
items: components["schemas"]["H5Link"][];
|
||||
total: number;
|
||||
};
|
||||
H5LinkDeleteResult: {
|
||||
deleted: boolean;
|
||||
};
|
||||
ApiPageAgency: {
|
||||
items: components["schemas"]["Agency"][];
|
||||
page: number;
|
||||
@ -5143,6 +5179,33 @@ export interface components {
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
H5LinkListResponse: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ApiResponseH5LinkList"];
|
||||
};
|
||||
};
|
||||
/** @description OK */
|
||||
H5LinkResponse: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ApiResponseH5Link"];
|
||||
};
|
||||
};
|
||||
/** @description OK */
|
||||
H5LinkDeleteResponse: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ApiResponseH5LinkDelete"];
|
||||
};
|
||||
};
|
||||
/** @description OK */
|
||||
AgencyPageResponse: {
|
||||
headers: {
|
||||
@ -5541,6 +5604,7 @@ export interface components {
|
||||
};
|
||||
};
|
||||
parameters: {
|
||||
AppCodeHeader: string;
|
||||
AgencyId: number;
|
||||
Id: number;
|
||||
Keyword: string;
|
||||
@ -6766,57 +6830,79 @@ export interface operations {
|
||||
listH5Links: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
header: {
|
||||
"X-App-Code": components["parameters"]["AppCodeHeader"];
|
||||
};
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
200: components["responses"]["H5LinkListResponse"];
|
||||
};
|
||||
};
|
||||
updateH5Links: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
header: {
|
||||
"X-App-Code": components["parameters"]["AppCodeHeader"];
|
||||
};
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["H5LinkBatchUpdateInput"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
200: components["responses"]["H5LinkListResponse"];
|
||||
};
|
||||
};
|
||||
createH5Link: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
header: {
|
||||
"X-App-Code": components["parameters"]["AppCodeHeader"];
|
||||
};
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["H5LinkPayload"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
201: components["responses"]["H5LinkResponse"];
|
||||
};
|
||||
};
|
||||
updateH5Link: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
header: {
|
||||
"X-App-Code": components["parameters"]["AppCodeHeader"];
|
||||
};
|
||||
path: {
|
||||
key: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["H5LinkPayload"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
200: components["responses"]["H5LinkResponse"];
|
||||
};
|
||||
};
|
||||
deleteH5Link: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
header: {
|
||||
"X-App-Code": components["parameters"]["AppCodeHeader"];
|
||||
};
|
||||
path: {
|
||||
key: string;
|
||||
};
|
||||
@ -6824,7 +6910,7 @@ export interface operations {
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
200: components["responses"]["H5LinkDeleteResponse"];
|
||||
};
|
||||
};
|
||||
listExploreTabs: {
|
||||
|
||||
@ -1506,6 +1506,7 @@ export interface AdminAppDto {
|
||||
}
|
||||
|
||||
export interface H5LinkConfigDto {
|
||||
appCode: string;
|
||||
key: string;
|
||||
label: string;
|
||||
updatedAtMs?: number;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user