Files
flvx/vite-frontend/src/pages/forward/batch-actions.ts
T
sagit e56dd898ef fix(dialog): prevent scroll to top on modal close (#318)
* fix(dialog): prevent both open and close auto focus to avoid page scroll

Add onOpenAutoFocus handler to prevent Radix Dialog from auto-focusing
content on open, which can cause unwanted scroll behavior.

* fix(dialog): remove onOpenAutoFocus, keep only onCloseAutoFocus

Remove onOpenAutoFocus handler that was causing scroll issues on first open.
Keep onCloseAutoFocus to prevent scroll to trigger element on close.

Key fix: Move {...props} before onCloseAutoFocus to prevent override.

* fix(dialog): prevent scroll to top on modal close

- Move {...props} before onCloseAutoFocus to prevent override
- Simplify handler to just e.preventDefault()
- Apply fix to both dialog.tsx and modal.tsx
2026-03-14 02:32:35 +00:00

227 lines
5.6 KiB
TypeScript

import type { BatchOperationFailure, BatchOperationResult } from "@/api/types";
import {
batchChangeTunnel,
batchDeleteForwards,
batchPauseForwards,
batchRedeployForwards,
batchResumeForwards,
} from "@/api";
import {
buildBatchFailureMessage,
extractBatchFailures,
extractApiErrorMessage,
} from "@/api/error-message";
export interface ForwardBatchActionOutcome {
toastVariant: "success" | "error";
toastMessage: string;
shouldRefresh: boolean;
resultTitle?: string;
resultSummary?: string;
failureDetails?: BatchOperationFailure[];
progressPercent?: number;
progressLabel?: string;
closeDeleteModal?: boolean;
closeChangeTunnelModal?: boolean;
resetTargetTunnel?: boolean;
}
const normalizeBatchResult = (value: unknown): BatchOperationResult => {
const raw = (value ?? {}) as Partial<BatchOperationResult>;
return {
successCount: Number(raw.successCount ?? 0),
failCount: Number(raw.failCount ?? 0),
failures: extractBatchFailures(raw),
};
};
const buildBatchToast = (
result: BatchOperationResult,
successText: string,
resultTitle: string,
): Pick<
ForwardBatchActionOutcome,
| "toastVariant"
| "toastMessage"
| "resultTitle"
| "resultSummary"
| "failureDetails"
> => {
if (result.failCount === 0) {
return {
toastVariant: "success",
toastMessage: successText,
resultTitle,
resultSummary: successText,
failureDetails: [],
};
}
return {
toastVariant: "error",
toastMessage: buildBatchFailureMessage(
result,
`成功 ${result.successCount} 项,失败 ${result.failCount} 项`,
),
resultTitle,
resultSummary: `成功 ${result.successCount} 项,失败 ${result.failCount} 项`,
failureDetails: result.failures || [],
};
};
export const executeForwardBatchDelete = async (
ids: number[],
): Promise<ForwardBatchActionOutcome> => {
try {
const response = await batchDeleteForwards(ids);
if (response.code !== 0) {
return {
toastVariant: "error",
toastMessage: response.msg || "删除失败",
shouldRefresh: false,
};
}
const summary = normalizeBatchResult(response.data);
return {
...buildBatchToast(
summary,
`成功删除 ${summary.successCount} 项`,
"批量删除结果",
),
shouldRefresh: true,
progressPercent: 100,
progressLabel: `删除完成:成功 ${summary.successCount} 项`,
closeDeleteModal: true,
};
} catch (error) {
return {
toastVariant: "error",
toastMessage: extractApiErrorMessage(error, "删除失败"),
shouldRefresh: false,
};
}
};
export const executeForwardBatchToggleService = async (
ids: number[],
enable: boolean,
): Promise<ForwardBatchActionOutcome> => {
const fallback = enable ? "启用失败" : "停用失败";
try {
const response = enable
? await batchResumeForwards(ids)
: await batchPauseForwards(ids);
if (response.code !== 0) {
return {
toastVariant: "error",
toastMessage: response.msg || fallback,
shouldRefresh: false,
};
}
const summary = normalizeBatchResult(response.data);
return {
...buildBatchToast(
summary,
enable
? `成功启用 ${summary.successCount} 项`
: `成功停用 ${summary.successCount} 项`,
enable ? "批量启用结果" : "批量停用结果",
),
shouldRefresh: true,
progressPercent: 100,
progressLabel: `${enable ? "启用" : "停用"}完成:成功 ${summary.successCount} 项`,
};
} catch (error) {
return {
toastVariant: "error",
toastMessage: extractApiErrorMessage(error, fallback),
shouldRefresh: false,
};
}
};
export const executeForwardBatchRedeploy = async (
ids: number[],
): Promise<ForwardBatchActionOutcome> => {
try {
const response = await batchRedeployForwards(ids);
if (response.code !== 0) {
return {
toastVariant: "error",
toastMessage: response.msg || "下发失败",
shouldRefresh: false,
};
}
const summary = normalizeBatchResult(response.data);
return {
...buildBatchToast(
summary,
`成功重新下发 ${summary.successCount} 项`,
"批量下发结果",
),
shouldRefresh: true,
progressPercent: 100,
progressLabel: `重新下发完成:成功 ${summary.successCount} 项`,
};
} catch (error) {
return {
toastVariant: "error",
toastMessage: extractApiErrorMessage(error, "下发失败"),
shouldRefresh: false,
};
}
};
export const executeForwardBatchChangeTunnel = async (
ids: number[],
targetTunnelId: number,
): Promise<ForwardBatchActionOutcome> => {
try {
const response = await batchChangeTunnel({
forwardIds: ids,
targetTunnelId,
});
if (response.code !== 0) {
return {
toastVariant: "error",
toastMessage: response.msg || "隧道失败",
shouldRefresh: false,
};
}
const summary = normalizeBatchResult(response.data);
return {
...buildBatchToast(
summary,
`成功换隧道 ${summary.successCount} 项`,
"批量换隧道结果",
),
shouldRefresh: true,
progressPercent: 100,
progressLabel: `批量换隧道完成:成功 ${summary.successCount} 项`,
closeChangeTunnelModal: true,
resetTargetTunnel: true,
};
} catch (error) {
return {
toastVariant: "error",
toastMessage: extractApiErrorMessage(error, "隧道失败"),
shouldRefresh: false,
};
}
};