Pārlūkot izejas kodu

feat: add batch size limit error handling and translations

- Introduced a new error message for exceeding batch size limits in batch update operations for keys and users.
- Updated error handling in the batchUpdateKeys and batchUpdateUsers functions to return appropriate error messages when the limit is exceeded.
- Added translations for the new error message in English, Japanese, Russian, Simplified Chinese, and Traditional Chinese to enhance user feedback across multiple languages.
ding113 1 mēnesi atpakaļ
vecāks
revīzija
6e11a7b630

+ 1 - 0
messages/en/errors.json

@@ -44,6 +44,7 @@
   "NETWORK_ERROR": "Network error",
 
   "QUOTA_EXCEEDED": "Quota exceeded",
+  "BATCH_SIZE_EXCEEDED": "Batch operation cannot exceed {max} items",
   "RATE_LIMIT_EXCEEDED": "Too many requests, please try again later",
   "RESOURCE_BUSY": "Resource is currently in use",
   "INVALID_STATE": "Operation not allowed in current state",

+ 1 - 0
messages/ja/errors.json

@@ -44,6 +44,7 @@
   "NETWORK_ERROR": "ネットワークエラー",
 
   "QUOTA_EXCEEDED": "クォータを超過しました",
+  "BATCH_SIZE_EXCEEDED": "一括操作は {max} 件を超えることはできません",
   "RATE_LIMIT_EXCEEDED": "リクエストが多すぎます。後でもう一度お試しください",
   "RESOURCE_BUSY": "リソースは現在使用中です",
   "INVALID_STATE": "現在の状態では操作が許可されていません",

+ 1 - 0
messages/ru/errors.json

@@ -44,6 +44,7 @@
   "NETWORK_ERROR": "Сетевая ошибка",
 
   "QUOTA_EXCEEDED": "Квота исчерпана",
+  "BATCH_SIZE_EXCEEDED": "Пакетная операция не может превышать {max} элементов",
   "RATE_LIMIT_EXCEEDED": "Слишком много запросов, попробуйте позже",
   "RESOURCE_BUSY": "Ресурс в настоящее время используется",
   "INVALID_STATE": "Операция не разрешена в текущем состоянии",

+ 1 - 0
messages/zh-CN/errors.json

@@ -44,6 +44,7 @@
   "NETWORK_ERROR": "网络错误",
 
   "QUOTA_EXCEEDED": "配额已用尽",
+  "BATCH_SIZE_EXCEEDED": "批量操作不能超过 {max} 个项目",
   "RATE_LIMIT_EXCEEDED": "请求过于频繁,请稍后重试",
   "RATE_LIMIT_RPM_EXCEEDED": "请求频率超限:当前 {current} 次/分钟(限制:{limit} 次/分钟)。将于 {resetTime} 重置",
   "RATE_LIMIT_DAILY_QUOTA_EXCEEDED": "每日额度超限:当前 ${current} USD(限制:${limit} USD)。将于 {resetTime} 重置",

+ 1 - 0
messages/zh-TW/errors.json

@@ -44,6 +44,7 @@
   "NETWORK_ERROR": "網路錯誤",
 
   "QUOTA_EXCEEDED": "配額已用盡",
+  "BATCH_SIZE_EXCEEDED": "批次操作不能超過 {max} 個項目",
   "RATE_LIMIT_EXCEEDED": "請求過於頻繁,請稍後重試",
   "RESOURCE_BUSY": "資源正在使用中",
   "INVALID_STATE": "當前狀態不允許此操作",

+ 9 - 1
src/actions/keys.ts

@@ -733,9 +733,17 @@ export async function batchUpdateKeys(
       };
     }
 
+    const MAX_BATCH_SIZE = 500;
     const requestedIds = Array.from(new Set(params.keyIds)).filter((id) => Number.isInteger(id));
     if (requestedIds.length === 0) {
-      return { ok: false, error: "keyIds 不能为空", errorCode: ERROR_CODES.REQUIRED_FIELD };
+      return { ok: false, error: tError("REQUIRED_FIELD"), errorCode: ERROR_CODES.REQUIRED_FIELD };
+    }
+    if (requestedIds.length > MAX_BATCH_SIZE) {
+      return {
+        ok: false,
+        error: tError("BATCH_SIZE_EXCEEDED", { max: MAX_BATCH_SIZE }),
+        errorCode: ERROR_CODES.INVALID_FORMAT,
+      };
     }
 
     const updates = params.updates ?? {};

+ 9 - 1
src/actions/users.ts

@@ -468,9 +468,17 @@ export async function batchUpdateUsers(
       };
     }
 
+    const MAX_BATCH_SIZE = 500;
     const requestedIds = Array.from(new Set(params.userIds)).filter((id) => Number.isInteger(id));
     if (requestedIds.length === 0) {
-      return { ok: false, error: "userIds 不能为空", errorCode: ERROR_CODES.REQUIRED_FIELD };
+      return { ok: false, error: tError("REQUIRED_FIELD"), errorCode: ERROR_CODES.REQUIRED_FIELD };
+    }
+    if (requestedIds.length > MAX_BATCH_SIZE) {
+      return {
+        ok: false,
+        error: tError("BATCH_SIZE_EXCEEDED", { max: MAX_BATCH_SIZE }),
+        errorCode: ERROR_CODES.INVALID_FORMAT,
+      };
     }
 
     const updatesSchema = UpdateUserSchema.pick({

+ 2 - 36
src/app/[locale]/dashboard/_components/user/batch-edit/batch-key-section.tsx

@@ -1,9 +1,9 @@
 "use client";
 
-import type { ReactNode } from "react";
 import { Input } from "@/components/ui/input";
 import { Switch } from "@/components/ui/switch";
-import { cn } from "@/lib/utils";
+import { FieldCard } from "./field-card";
+import { formatMessage } from "./utils";
 
 export interface BatchKeySectionState {
   providerGroupEnabled: boolean;
@@ -22,12 +22,6 @@ export interface BatchKeySectionState {
   isEnabled: boolean;
 }
 
-function formatMessage(template: string, values: Record<string, string | number>) {
-  return template.replace(/\{(\w+)\}/g, (_match, key: string) =>
-    Object.hasOwn(values, key) ? String(values[key]) : `{${key}}`
-  );
-}
-
 export interface BatchKeySectionProps {
   affectedKeysCount: number;
   state: BatchKeySectionState;
@@ -53,34 +47,6 @@ export interface BatchKeySectionProps {
   };
 }
 
-function FieldCard({
-  title,
-  enabled,
-  onEnabledChange,
-  enableFieldAria,
-  children,
-}: {
-  title: string;
-  enabled: boolean;
-  onEnabledChange: (enabled: boolean) => void;
-  enableFieldAria: string;
-  children: ReactNode;
-}) {
-  return (
-    <div className={cn("rounded-md border p-3 space-y-3", !enabled && "opacity-80")}>
-      <div className="flex items-center justify-between gap-3">
-        <div className="text-sm font-medium">{title}</div>
-        <Switch
-          checked={enabled}
-          onCheckedChange={onEnabledChange}
-          aria-label={formatMessage(enableFieldAria, { title })}
-        />
-      </div>
-      {children}
-    </div>
-  );
-}
-
 export function BatchKeySection({
   affectedKeysCount,
   state,

+ 2 - 37
src/app/[locale]/dashboard/_components/user/batch-edit/batch-user-section.tsx

@@ -1,10 +1,9 @@
 "use client";
 
-import type { ReactNode } from "react";
 import { Input } from "@/components/ui/input";
-import { Switch } from "@/components/ui/switch";
 import { TagInput } from "@/components/ui/tag-input";
-import { cn } from "@/lib/utils";
+import { FieldCard } from "./field-card";
+import { formatMessage } from "./utils";
 
 export interface BatchUserSectionState {
   noteEnabled: boolean;
@@ -19,12 +18,6 @@ export interface BatchUserSectionState {
   limitMonthlyUsd: string;
 }
 
-function formatMessage(template: string, values: Record<string, string | number>) {
-  return template.replace(/\{(\w+)\}/g, (_match, key: string) =>
-    Object.hasOwn(values, key) ? String(values[key]) : `{${key}}`
-  );
-}
-
 export interface BatchUserSectionProps {
   affectedUsersCount: number;
   state: BatchUserSectionState;
@@ -48,34 +41,6 @@ export interface BatchUserSectionProps {
   };
 }
 
-function FieldCard({
-  title,
-  enabled,
-  onEnabledChange,
-  enableFieldAria,
-  children,
-}: {
-  title: string;
-  enabled: boolean;
-  onEnabledChange: (enabled: boolean) => void;
-  enableFieldAria: string;
-  children: ReactNode;
-}) {
-  return (
-    <div className={cn("rounded-md border p-3 space-y-3", !enabled && "opacity-80")}>
-      <div className="flex items-center justify-between gap-3">
-        <div className="text-sm font-medium">{title}</div>
-        <Switch
-          checked={enabled}
-          onCheckedChange={onEnabledChange}
-          aria-label={formatMessage(enableFieldAria, { title })}
-        />
-      </div>
-      {children}
-    </div>
-  );
-}
-
 export function BatchUserSection({
   affectedUsersCount,
   state,

+ 39 - 0
src/app/[locale]/dashboard/_components/user/batch-edit/field-card.tsx

@@ -0,0 +1,39 @@
+"use client";
+
+import type { ReactNode } from "react";
+import { Switch } from "@/components/ui/switch";
+import { cn } from "@/lib/utils";
+import { formatMessage } from "./utils";
+
+export interface FieldCardProps {
+  title: string;
+  enabled: boolean;
+  onEnabledChange: (enabled: boolean) => void;
+  enableFieldAria: string;
+  children: ReactNode;
+}
+
+/**
+ * A card component for batch edit fields with an enable/disable switch
+ */
+export function FieldCard({
+  title,
+  enabled,
+  onEnabledChange,
+  enableFieldAria,
+  children,
+}: FieldCardProps) {
+  return (
+    <div className={cn("rounded-md border p-3 space-y-3", !enabled && "opacity-80")}>
+      <div className="flex items-center justify-between gap-3">
+        <div className="text-sm font-medium">{title}</div>
+        <Switch
+          checked={enabled}
+          onCheckedChange={onEnabledChange}
+          aria-label={formatMessage(enableFieldAria, { title })}
+        />
+      </div>
+      {children}
+    </div>
+  );
+}

+ 15 - 0
src/app/[locale]/dashboard/_components/user/batch-edit/utils.ts

@@ -0,0 +1,15 @@
+/**
+ * Shared utilities for batch edit components
+ */
+
+/**
+ * Format a template string with values using ICU-style {placeholder} syntax
+ */
+export function formatMessage(
+  template: string,
+  values: Record<string, string | number>
+): string {
+  return template.replace(/\{(\w+)\}/g, (_match, key: string) =>
+    Object.hasOwn(values, key) ? String(values[key]) : `{${key}}`
+  );
+}