From b5edd8f4e5c1e8711c0a1f024b9b3f6cf0257c9f Mon Sep 17 00:00:00 2001 From: Jaswanth-arjun <138548908+Jaswanth-arjun@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:32:32 +0530 Subject: [PATCH 1/3] fix(i18n): prevent crash when switching languages due to missing progress data --- src/components/Header/LanguageMenu.tsx | 3 ++- vite.config.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Header/LanguageMenu.tsx b/src/components/Header/LanguageMenu.tsx index 48fcd8f0ef..46b6153d76 100644 --- a/src/components/Header/LanguageMenu.tsx +++ b/src/components/Header/LanguageMenu.tsx @@ -30,7 +30,8 @@ const LangMap: Record = { }; const TranslationProgress = ({ lang }: { lang: string }) => { - const percent = i18nProgress[lang].percent; + const progressData = i18nProgress[lang as keyof typeof i18nProgress]; + const percent = progressData?.percent ?? 0; if (typeof percent === 'number' && percent < 100) { return ( `./src/locales/${lang}`, }), From cdcdee5dd2372b29c33c4ec89c1bfc4b88d69620 Mon Sep 17 00:00:00 2001 From: Jaswanth-arjun <138548908+Jaswanth-arjun@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:56:27 +0530 Subject: [PATCH 2/3] refactor(validation): handle zOneOf correctly or remove if unused --- .../FormItemPlugins/PluginCardList.tsx | 17 +++++++--- src/utils/zod.ts | 31 ------------------- 2 files changed, 13 insertions(+), 35 deletions(-) diff --git a/src/components/form-slice/FormItemPlugins/PluginCardList.tsx b/src/components/form-slice/FormItemPlugins/PluginCardList.tsx index 53de9090ad..311dc74e6a 100644 --- a/src/components/form-slice/FormItemPlugins/PluginCardList.tsx +++ b/src/components/form-slice/FormItemPlugins/PluginCardList.tsx @@ -31,7 +31,7 @@ import { PluginCard, type PluginCardProps } from './PluginCard'; type PluginCardListSearchProps = Pick & { search: string; - setSearch: (search: string) => void; + setSearch: (search: string | string[] | null | undefined) => void; }; export const PluginCardListSearch = (props: PluginCardListSearchProps) => { const { placeholder, search, setSearch } = props; @@ -98,10 +98,19 @@ export type PluginCardListProps = Omit & cols?: number; h?: number | string; mah?: number | string; - search: string; + search: string | string[] | null | undefined; plugins: string[]; }; +const normalizeSearch = ( + search: string | string[] | null | undefined +): string => { + if (Array.isArray(search)) { + return search.filter((v): v is string => typeof v === 'string').join(' '); + } + return typeof search === 'string' ? search : ''; +}; + export const PluginCardList = (props: PluginCardListProps) => { const { search = '', cols = 3, h, mah, plugins } = props; const { mode, onAdd, onEdit, onDelete, onView } = props; @@ -111,8 +120,8 @@ export const PluginCardList = (props: PluginCardListProps) => { search: '', plugins: [] as string[], mode: 'add' as OptionProps['mode'], - setSearch(search: string) { - this.search = search.toLowerCase().trim(); + setSearch(search: string | string[] | null | undefined) { + this.search = normalizeSearch(search).toLowerCase().trim(); }, setPlugins(plugins: string[]) { this.plugins = plugins; diff --git a/src/utils/zod.ts b/src/utils/zod.ts index 82435f9aae..ee750cf16b 100644 --- a/src/utils/zod.ts +++ b/src/utils/zod.ts @@ -14,37 +14,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { xor } from 'rambdax'; -import { type RefinementCtx, z } from 'zod'; import { init } from 'zod-empty'; -// ref: https://github.com/colinhacks/zod/issues/61#issuecomment-1741983149 -export const zOneOf = - < - A, - K1 extends Extract, - K2 extends Extract, - R extends A & - ( - | (Required> & { [P in K2]: undefined }) - | (Required> & { [P in K1]: undefined }) - ) - >( - key1: K1, - key2: K2 - ): ((arg: A, ctx: RefinementCtx) => arg is R) => - (arg, ctx): arg is R => { - if (xor(arg[key1] as boolean, arg[key2] as boolean)) { - [key1, key2].forEach((key) => { - ctx.addIssue({ - path: [key], - code: z.ZodIssueCode.custom, - message: `Either '${key1}' or '${key2}' must be filled, but not both`, - }); - }); - return false; - } - return true; - }; - export const zGetDefault = init; From 193a84693e50d844fe64c36647b50bbce32c2cd6 Mon Sep 17 00:00:00 2001 From: Jaswanth-arjun <138548908+Jaswanth-arjun@users.noreply.github.com> Date: Sat, 21 Mar 2026 16:18:13 +0530 Subject: [PATCH 3/3] fix(plugin): add confirmation dialog before deleting plugin card Adds a confirmation modal before deleting a plugin from forms to prevent accidental data loss and align with existing dashboard UX patterns. --- src/apis/plugins.ts | 48 +++++++++++++++---- .../form-slice/FormItemPlugins/PluginCard.tsx | 31 +++++++++++- src/routes/plugin_configs/index.tsx | 26 ++++------ 3 files changed, 78 insertions(+), 27 deletions(-) diff --git a/src/apis/plugins.ts b/src/apis/plugins.ts index 8e14b343c7..22b3a0c977 100644 --- a/src/apis/plugins.ts +++ b/src/apis/plugins.ts @@ -26,6 +26,37 @@ import { import { req } from '@/config/req'; import type { APISIXType } from '@/types/schema/apisix'; +const normalizePluginMap = ( + payload: unknown +): Record> => { + if (!payload || typeof payload !== 'object') { + return {}; + } + + const obj = payload as Record; + + // Some APISIX variants may wrap plugin definitions under `plugins`. + if (obj.plugins && typeof obj.plugins === 'object' && !Array.isArray(obj.plugins)) { + return obj.plugins as Record>; + } + + // Some variants may return an array of one-key objects. + if (Array.isArray(payload)) { + const merged: Record> = {}; + for (const item of payload) { + if (!item || typeof item !== 'object' || Array.isArray(item)) continue; + for (const [name, schemaObj] of Object.entries(item)) { + if (schemaObj && typeof schemaObj === 'object') { + merged[name] = schemaObj as Record; + } + } + } + return merged; + } + + return obj as Record>; +}; + export type NeedPluginSchema = { schema: APISIXType['PluginSchemaKeys']; @@ -53,14 +84,15 @@ export const getPluginsListWithSchemaQueryOptions = ( params: { subsystem, all: true }, }) .then((v) => { - const data = Object.entries(v.data); - const names = []; + const originObj = normalizePluginMap(v.data); + const data = Object.entries(originObj); + const names: string[] = []; for (const [name, config] of data) { if (config[schema]) { names.push(name); } } - return { names, originObj: v.data }; + return { names, originObj }; }), }); }; @@ -73,11 +105,11 @@ export const getPluginSchemaQueryOptions = ( queryKey: ['plugin-schema', name], queryFn: name ? () => - req - .get( - `${API_PLUGINS}/${name}` - ) - .then((v) => v.data) + req + .get( + `${API_PLUGINS}/${name}` + ) + .then((v) => v.data) : skipToken, enabled, }); diff --git a/src/components/form-slice/FormItemPlugins/PluginCard.tsx b/src/components/form-slice/FormItemPlugins/PluginCard.tsx index 2d9bca8f56..a8e58b713f 100644 --- a/src/components/form-slice/FormItemPlugins/PluginCard.tsx +++ b/src/components/form-slice/FormItemPlugins/PluginCard.tsx @@ -14,7 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Button, Card,Group, Text } from '@mantine/core'; +import { Button, Card, Group, Text } from '@mantine/core'; +import { useCallbackRef } from '@mantine/hooks'; +import { modals } from '@mantine/modals'; import { useTranslation } from 'react-i18next'; export type PluginCardProps = { @@ -30,6 +32,31 @@ export type PluginCardProps = { export const PluginCard = (props: PluginCardProps) => { const { name, desc, mode, onAdd, onEdit, onView, onDelete } = props; const { t } = useTranslation(); + + const handleDelete = useCallbackRef(() => + modals.openConfirmModal({ + centered: true, + confirmProps: { color: 'red' }, + title: t('info.delete.title', { name: name }), + children: ( + + {t('info.delete.content', { name: name })} + + {name} + + {t('mark.question')} + + ), + labels: { confirm: t('form.btn.delete'), cancel: t('form.btn.cancel') }, + onConfirm: () => onDelete?.(name), + }) + ); + return ( @@ -78,7 +105,7 @@ export const PluginCard = (props: PluginCardProps) => { size="compact-xs" variant="light" color="red" - onClick={() => onDelete?.(name)} + onClick={handleDelete} > {t('form.btn.delete')} diff --git a/src/routes/plugin_configs/index.tsx b/src/routes/plugin_configs/index.tsx index bccb89848f..cbfcc96370 100644 --- a/src/routes/plugin_configs/index.tsx +++ b/src/routes/plugin_configs/index.tsx @@ -102,23 +102,15 @@ function PluginConfigsList() { pagination={pagination} cardProps={{ bodyStyle: { padding: 0 } }} toolbar={{ - menu: { - type: 'inline', - items: [ - { - key: 'add', - label: ( - - ), - }, - ], - }, + actions: [ + , + ], }} />