fix: stabilize selected batch submit state
This commit is contained in:
parent
b308b49368
commit
3992d4c325
@ -139,6 +139,7 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
|
|
||||||
const toolbarHandlers = {
|
const toolbarHandlers = {
|
||||||
onExport: async () => {
|
onExport: async () => {
|
||||||
|
syncSelectionStateFromDom();
|
||||||
const exportTarget = readToolbarExportTarget(toolbar);
|
const exportTarget = readToolbarExportTarget(toolbar);
|
||||||
if (!exportTarget.target) {
|
if (!exportTarget.target) {
|
||||||
setToolbarExportStatus(toolbar, exportTarget.error ?? "导出配置无效");
|
setToolbarExportStatus(toolbar, exportTarget.error ?? "导出配置无效");
|
||||||
@ -164,6 +165,7 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSubmitBatch: async () => {
|
onSubmitBatch: async () => {
|
||||||
|
syncSelectionStateFromDom();
|
||||||
const exportTarget = readToolbarExportTarget(toolbar);
|
const exportTarget = readToolbarExportTarget(toolbar);
|
||||||
if (!exportTarget.target) {
|
if (!exportTarget.target) {
|
||||||
setToolbarExportStatus(toolbar, exportTarget.error ?? "导出配置无效");
|
setToolbarExportStatus(toolbar, exportTarget.error ?? "导出配置无效");
|
||||||
@ -182,8 +184,15 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
|
|
||||||
setToolbarBusyState(toolbar, true);
|
setToolbarBusyState(toolbar, true);
|
||||||
try {
|
try {
|
||||||
|
const hasSelectedAuthors = selectedAuthorIds.size > 0;
|
||||||
const records = filterRecordsBySelection(
|
const records = filterRecordsBySelection(
|
||||||
await exportRecords(exportTarget.target, "提交中")
|
await exportRecords(
|
||||||
|
exportTarget.target,
|
||||||
|
hasSelectedAuthors ? "提交已选达人中" : "提交中",
|
||||||
|
{
|
||||||
|
showDetailedProgress: !hasSelectedAuthors
|
||||||
|
}
|
||||||
|
)
|
||||||
);
|
);
|
||||||
const authState = await getAuthState();
|
const authState = await getAuthState();
|
||||||
if (!authState.isAuthenticated) {
|
if (!authState.isAuthenticated) {
|
||||||
@ -457,6 +466,32 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
syncMarketSelectionState(table, selectedAuthorIds);
|
syncMarketSelectionState(table, selectedAuthorIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncSelectionStateFromDom(): void {
|
||||||
|
const rowSelectionCheckboxes = Array.from(
|
||||||
|
options.document.querySelectorAll('[data-market-selection-checkbox="row"]')
|
||||||
|
).filter(
|
||||||
|
(element): element is HTMLInputElement => element instanceof HTMLInputElement
|
||||||
|
);
|
||||||
|
if (rowSelectionCheckboxes.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rowSelectionCheckboxes.forEach((checkbox) => {
|
||||||
|
const authorId = checkbox.dataset.marketSelectionAuthorId?.trim();
|
||||||
|
if (!authorId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkbox.checked) {
|
||||||
|
selectedAuthorIds.add(authorId);
|
||||||
|
} else {
|
||||||
|
selectedAuthorIds.delete(authorId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
refreshSelectionControls();
|
||||||
|
}
|
||||||
|
|
||||||
function toggleSortFromHeader(field: MarketSortState["field"]): void {
|
function toggleSortFromHeader(field: MarketSortState["field"]): void {
|
||||||
activeSort = getNextSortState(activeSort, field);
|
activeSort = getNextSortState(activeSort, field);
|
||||||
applyCurrentView();
|
applyCurrentView();
|
||||||
|
|||||||
@ -1877,6 +1877,242 @@ describe("market-content-entry", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"selected batch submit keeps a generic loading status while submitting the default paged range",
|
||||||
|
async () => {
|
||||||
|
const pages = [
|
||||||
|
[
|
||||||
|
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" },
|
||||||
|
{ authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" }
|
||||||
|
],
|
||||||
|
[{ authorId: "333", authorName: "达人 C", price21To60s: "¥33,000" }],
|
||||||
|
[{ authorId: "444", authorName: "达人 D", price21To60s: "¥44,000" }],
|
||||||
|
[{ authorId: "555", authorName: "达人 E", price21To60s: "¥55,000" }],
|
||||||
|
[{ authorId: "666", authorName: "达人 F", price21To60s: "¥66,000" }]
|
||||||
|
];
|
||||||
|
const secondPageDeferred = createDeferred<{
|
||||||
|
json(): Promise<unknown>;
|
||||||
|
ok: boolean;
|
||||||
|
}>();
|
||||||
|
const submitBatch = vi.fn(async () => ({ ok: true }));
|
||||||
|
|
||||||
|
document.body.innerHTML = buildRealMarketFixture(pages[0]);
|
||||||
|
const fetchMock = vi.fn(async (_input: string, init?: RequestInit) => {
|
||||||
|
const body = JSON.parse(String(init?.body ?? "{}")) as { page?: number };
|
||||||
|
const pageNumber = body.page ?? 1;
|
||||||
|
const response = {
|
||||||
|
json: async () => ({
|
||||||
|
data: {
|
||||||
|
marketList: buildMarketListResponseRows(pages[pageNumber - 1] ?? []),
|
||||||
|
totalPages: 5
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ok: true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pageNumber === 2) {
|
||||||
|
return secondPageDeferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
|
||||||
|
(
|
||||||
|
globalThis as typeof globalThis & {
|
||||||
|
fetch?: typeof fetchMock;
|
||||||
|
}
|
||||||
|
).fetch = fetchMock;
|
||||||
|
document.documentElement.setAttribute(
|
||||||
|
"data-sces-market-request-snapshot",
|
||||||
|
JSON.stringify({
|
||||||
|
body: JSON.stringify({
|
||||||
|
page: 1
|
||||||
|
}),
|
||||||
|
method: "POST",
|
||||||
|
url: "https://xingtu.cn/api/mock-market-search"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { createMarketController } = await import("../src/content/market/index");
|
||||||
|
const controller = trackController(createMarketController({
|
||||||
|
document,
|
||||||
|
getAuthState: async () => ({
|
||||||
|
isAuthenticated: true,
|
||||||
|
resource: "https://talent-search.intelligrow.cn",
|
||||||
|
userInfo: { name: "王少卿", sub: "p7pdhhtde8kj" }
|
||||||
|
}),
|
||||||
|
loadAuthorMetrics: async () => ({
|
||||||
|
success: false,
|
||||||
|
reason: "request-failed"
|
||||||
|
}),
|
||||||
|
promptBatchName: vi.fn(() => "自动选择批次"),
|
||||||
|
submitBatch,
|
||||||
|
window
|
||||||
|
}));
|
||||||
|
|
||||||
|
await controller.ready;
|
||||||
|
clickSelectionCheckboxForAuthor("111");
|
||||||
|
|
||||||
|
click('[data-plugin-batch-submit="button"]');
|
||||||
|
for (let attempt = 0; attempt < 40; attempt += 1) {
|
||||||
|
if (
|
||||||
|
fetchMock.mock.calls.some(([, init]) => {
|
||||||
|
const body = JSON.parse(
|
||||||
|
String((init as RequestInit | undefined)?.body ?? "{}")
|
||||||
|
) as { page?: number };
|
||||||
|
return body.page === 2;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
await Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(
|
||||||
|
document.querySelector('[data-plugin-export-status="text"]')?.textContent
|
||||||
|
).toBe("提交已选达人中...");
|
||||||
|
|
||||||
|
secondPageDeferred.resolve({
|
||||||
|
json: async () => ({
|
||||||
|
data: {
|
||||||
|
marketList: buildMarketListResponseRows(pages[1]),
|
||||||
|
totalPages: 5
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ok: true
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForMockCall(submitBatch, 120, 50);
|
||||||
|
|
||||||
|
expect(submitBatch).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
authors: [{ authorId: "111", authorName: "达人 A" }]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"batch submit respects checked row selection even when the selection change event was missed",
|
||||||
|
async () => {
|
||||||
|
const pages = [
|
||||||
|
[
|
||||||
|
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" },
|
||||||
|
{ authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" }
|
||||||
|
],
|
||||||
|
[{ authorId: "333", authorName: "达人 C", price21To60s: "¥33,000" }],
|
||||||
|
[{ authorId: "444", authorName: "达人 D", price21To60s: "¥44,000" }],
|
||||||
|
[{ authorId: "555", authorName: "达人 E", price21To60s: "¥55,000" }],
|
||||||
|
[{ authorId: "666", authorName: "达人 F", price21To60s: "¥66,000" }]
|
||||||
|
];
|
||||||
|
const secondPageDeferred = createDeferred<{
|
||||||
|
json(): Promise<unknown>;
|
||||||
|
ok: boolean;
|
||||||
|
}>();
|
||||||
|
const submitBatch = vi.fn(async () => ({ ok: true }));
|
||||||
|
|
||||||
|
document.body.innerHTML = buildRealMarketFixture(pages[0]);
|
||||||
|
const fetchMock = vi.fn(async (_input: string, init?: RequestInit) => {
|
||||||
|
const body = JSON.parse(String(init?.body ?? "{}")) as { page?: number };
|
||||||
|
const pageNumber = body.page ?? 1;
|
||||||
|
const response = {
|
||||||
|
json: async () => ({
|
||||||
|
data: {
|
||||||
|
marketList: buildMarketListResponseRows(pages[pageNumber - 1] ?? []),
|
||||||
|
totalPages: 5
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ok: true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pageNumber === 2) {
|
||||||
|
return secondPageDeferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
|
||||||
|
(
|
||||||
|
globalThis as typeof globalThis & {
|
||||||
|
fetch?: typeof fetchMock;
|
||||||
|
}
|
||||||
|
).fetch = fetchMock;
|
||||||
|
document.documentElement.setAttribute(
|
||||||
|
"data-sces-market-request-snapshot",
|
||||||
|
JSON.stringify({
|
||||||
|
body: JSON.stringify({
|
||||||
|
page: 1
|
||||||
|
}),
|
||||||
|
method: "POST",
|
||||||
|
url: "https://xingtu.cn/api/mock-market-search"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { createMarketController } = await import("../src/content/market/index");
|
||||||
|
const controller = trackController(createMarketController({
|
||||||
|
document,
|
||||||
|
getAuthState: async () => ({
|
||||||
|
isAuthenticated: true,
|
||||||
|
resource: "https://talent-search.intelligrow.cn",
|
||||||
|
userInfo: { name: "王少卿", sub: "p7pdhhtde8kj" }
|
||||||
|
}),
|
||||||
|
loadAuthorMetrics: async () => ({
|
||||||
|
success: false,
|
||||||
|
reason: "request-failed"
|
||||||
|
}),
|
||||||
|
promptBatchName: vi.fn(() => "自动选择批次"),
|
||||||
|
submitBatch,
|
||||||
|
window
|
||||||
|
}));
|
||||||
|
|
||||||
|
await controller.ready;
|
||||||
|
|
||||||
|
const rowSelectionCheckbox = readSelectionCheckboxForAuthor("111");
|
||||||
|
rowSelectionCheckbox.checked = true;
|
||||||
|
|
||||||
|
click('[data-plugin-batch-submit="button"]');
|
||||||
|
for (let attempt = 0; attempt < 40; attempt += 1) {
|
||||||
|
if (
|
||||||
|
fetchMock.mock.calls.some(([, init]) => {
|
||||||
|
const body = JSON.parse(
|
||||||
|
String((init as RequestInit | undefined)?.body ?? "{}")
|
||||||
|
) as { page?: number };
|
||||||
|
return body.page === 2;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
await Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(
|
||||||
|
document.querySelector('[data-plugin-export-status="text"]')?.textContent
|
||||||
|
).toBe("提交已选达人中...");
|
||||||
|
|
||||||
|
secondPageDeferred.resolve({
|
||||||
|
json: async () => ({
|
||||||
|
data: {
|
||||||
|
marketList: buildMarketListResponseRows(pages[1]),
|
||||||
|
totalPages: 5
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ok: true
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForMockCall(submitBatch, 120, 50);
|
||||||
|
|
||||||
|
expect(submitBatch).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
authors: [{ authorId: "111", authorName: "达人 A" }]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
test("selected batch submit falls back to all creators in the current range when no selection matches", async () => {
|
test("selected batch submit falls back to all creators in the current range when no selection matches", async () => {
|
||||||
const pages = [
|
const pages = [
|
||||||
[
|
[
|
||||||
@ -1930,6 +2166,116 @@ describe("market-content-entry", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"default paged batch submit keeps detailed progress when no creators are selected",
|
||||||
|
async () => {
|
||||||
|
const pages = [
|
||||||
|
[
|
||||||
|
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" },
|
||||||
|
{ authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" }
|
||||||
|
],
|
||||||
|
[{ authorId: "333", authorName: "达人 C", price21To60s: "¥33,000" }],
|
||||||
|
[{ authorId: "444", authorName: "达人 D", price21To60s: "¥44,000" }],
|
||||||
|
[{ authorId: "555", authorName: "达人 E", price21To60s: "¥55,000" }],
|
||||||
|
[{ authorId: "666", authorName: "达人 F", price21To60s: "¥66,000" }]
|
||||||
|
];
|
||||||
|
const secondPageDeferred = createDeferred<{
|
||||||
|
json(): Promise<unknown>;
|
||||||
|
ok: boolean;
|
||||||
|
}>();
|
||||||
|
const submitBatch = vi.fn(async () => ({ ok: true }));
|
||||||
|
|
||||||
|
document.body.innerHTML = buildRealMarketFixture(pages[0]);
|
||||||
|
const fetchMock = vi.fn(async (_input: string, init?: RequestInit) => {
|
||||||
|
const body = JSON.parse(String(init?.body ?? "{}")) as { page?: number };
|
||||||
|
const pageNumber = body.page ?? 1;
|
||||||
|
const response = {
|
||||||
|
json: async () => ({
|
||||||
|
data: {
|
||||||
|
marketList: buildMarketListResponseRows(pages[pageNumber - 1] ?? []),
|
||||||
|
totalPages: 5
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ok: true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pageNumber === 2) {
|
||||||
|
return secondPageDeferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
|
||||||
|
(
|
||||||
|
globalThis as typeof globalThis & {
|
||||||
|
fetch?: typeof fetchMock;
|
||||||
|
}
|
||||||
|
).fetch = fetchMock;
|
||||||
|
document.documentElement.setAttribute(
|
||||||
|
"data-sces-market-request-snapshot",
|
||||||
|
JSON.stringify({
|
||||||
|
body: JSON.stringify({
|
||||||
|
page: 1
|
||||||
|
}),
|
||||||
|
method: "POST",
|
||||||
|
url: "https://xingtu.cn/api/mock-market-search"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { createMarketController } = await import("../src/content/market/index");
|
||||||
|
const controller = trackController(createMarketController({
|
||||||
|
document,
|
||||||
|
getAuthState: async () => ({
|
||||||
|
isAuthenticated: true,
|
||||||
|
resource: "https://talent-search.intelligrow.cn",
|
||||||
|
userInfo: { name: "王少卿", sub: "p7pdhhtde8kj" }
|
||||||
|
}),
|
||||||
|
loadAuthorMetrics: async () => ({
|
||||||
|
success: false,
|
||||||
|
reason: "request-failed"
|
||||||
|
}),
|
||||||
|
promptBatchName: vi.fn(() => "默认批次"),
|
||||||
|
submitBatch,
|
||||||
|
window
|
||||||
|
}));
|
||||||
|
|
||||||
|
await controller.ready;
|
||||||
|
|
||||||
|
click('[data-plugin-batch-submit="button"]');
|
||||||
|
for (let attempt = 0; attempt < 40; attempt += 1) {
|
||||||
|
if (
|
||||||
|
fetchMock.mock.calls.some(([, init]) => {
|
||||||
|
const body = JSON.parse(
|
||||||
|
String((init as RequestInit | undefined)?.body ?? "{}")
|
||||||
|
) as { page?: number };
|
||||||
|
return body.page === 2;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
await Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(
|
||||||
|
document.querySelector('[data-plugin-export-status="text"]')?.textContent
|
||||||
|
).toBe("提交中 2/5 页...");
|
||||||
|
|
||||||
|
secondPageDeferred.resolve({
|
||||||
|
json: async () => ({
|
||||||
|
data: {
|
||||||
|
marketList: buildMarketListResponseRows(pages[1]),
|
||||||
|
totalPages: 5
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ok: true
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForMockCall(submitBatch, 120, 50);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
test("shows an error when the batch name is blank", async () => {
|
test("shows an error when the batch name is blank", async () => {
|
||||||
document.body.innerHTML = buildMarketFixture();
|
document.body.innerHTML = buildMarketFixture();
|
||||||
const promptBatchName = vi.fn(() => " ");
|
const promptBatchName = vi.fn(() => " ");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user