From 4d20e053cb11a4361b33662eb2bf2187f520f1a1 Mon Sep 17 00:00:00 2001 From: QuentinHsu Date: Tue, 2 Jun 2026 12:09:47 +0800 Subject: [PATCH 1/3] fix(channels): reveal advanced validation errors - add channel form error detection for JSON validation errors inside advanced settings. - expand advanced settings on invalid channel drawer submit and prompt users to fix highlighted fields. - add regression tests for advanced error detection, non-advanced exclusions, and schema error classification. --- .../drawers/channel-mutate-drawer.tsx | 35 +++++---- .../channels/lib/channel-form-errors.test.ts | 71 +++++++++++++++++++ .../channels/lib/channel-form-errors.ts | 67 +++++++++++++++++ .../src/features/channels/lib/index.ts | 1 + 4 files changed, 162 insertions(+), 12 deletions(-) create mode 100644 web/default/src/features/channels/lib/channel-form-errors.test.ts create mode 100644 web/default/src/features/channels/lib/channel-form-errors.ts diff --git a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx index 6b26cd17..5c6c53de 100644 --- a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx +++ b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx @@ -24,7 +24,7 @@ import { useCallback, useRef, } from 'react' -import { useForm } from 'react-hook-form' +import { type SubmitErrorHandler, useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { useQuery, useQueryClient } from '@tanstack/react-query' import { @@ -140,6 +140,7 @@ import { hasModelConfigChanged, findMissingModelsInMapping, validateModelMappingJson, + hasAdvancedSettingsErrors, } from '../../lib' import { collectInvalidStatusCodeEntries, @@ -1008,6 +1009,26 @@ export function ChannelMutateDrawer({ ] ) + const handleAdvancedSettingsOpenChange = useCallback((nextOpen: boolean) => { + setAdvancedSettingsOpen(nextOpen) + if (typeof window !== 'undefined') { + window.localStorage.setItem( + ADVANCED_SETTINGS_EXPANDED_KEY, + String(nextOpen) + ) + } + }, []) + + const onInvalid: SubmitErrorHandler = useCallback( + (errors) => { + if (hasAdvancedSettingsErrors(errors)) { + handleAdvancedSettingsOpenChange(true) + } + toast.error(t('Please fix the highlighted fields before saving')) + }, + [handleAdvancedSettingsOpenChange, t] + ) + // Handle drawer close const handleOpenChange = useCallback( (v: boolean) => { @@ -1020,16 +1041,6 @@ export function ChannelMutateDrawer({ [onOpenChange, form] ) - const handleAdvancedSettingsOpenChange = useCallback((nextOpen: boolean) => { - setAdvancedSettingsOpen(nextOpen) - if (typeof window !== 'undefined') { - window.localStorage.setItem( - ADVANCED_SETTINGS_EXPANDED_KEY, - String(nextOpen) - ) - } - }, []) - return ( <> @@ -1060,7 +1071,7 @@ export function ChannelMutateDrawer({
{isChannelDetailLoading ? ( diff --git a/web/default/src/features/channels/lib/channel-form-errors.test.ts b/web/default/src/features/channels/lib/channel-form-errors.test.ts new file mode 100644 index 00000000..c5be0bcd --- /dev/null +++ b/web/default/src/features/channels/lib/channel-form-errors.test.ts @@ -0,0 +1,71 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import type { FieldErrors } from 'react-hook-form' +import assert from 'node:assert/strict' +import { describe, test } from 'node:test' +import { + CHANNEL_FORM_DEFAULT_VALUES, + channelFormSchema, + type ChannelFormValues, +} from './channel-form' +import { hasAdvancedSettingsErrors } from './channel-form-errors' + +describe('channel form errors', () => { + test('detects validation errors in collapsed advanced JSON fields', () => { + const errors = { + param_override: { + type: 'custom', + message: 'Invalid JSON', + }, + } satisfies FieldErrors + + assert.equal(hasAdvancedSettingsErrors(errors), true) + }) + + test('ignores validation errors outside advanced settings', () => { + const errors = { + name: { + type: 'too_small', + message: 'Name is required', + }, + } satisfies FieldErrors + + assert.equal(hasAdvancedSettingsErrors(errors), false) + }) + + test('classifies schema errors from invalid advanced JSON fields', () => { + const result = channelFormSchema.safeParse({ + ...CHANNEL_FORM_DEFAULT_VALUES, + name: 'OpenAI', + type: 1, + key: 'sk-test', + models: 'gpt-4o', + group: ['default'], + param_override: '{', + }) + + assert.equal(result.success, false) + if (result.success) return + + const errors = result.error.flatten().fieldErrors + + assert.ok(errors.param_override?.length) + assert.equal(hasAdvancedSettingsErrors(errors), true) + }) +}) diff --git a/web/default/src/features/channels/lib/channel-form-errors.ts b/web/default/src/features/channels/lib/channel-form-errors.ts new file mode 100644 index 00000000..68de4b8d --- /dev/null +++ b/web/default/src/features/channels/lib/channel-form-errors.ts @@ -0,0 +1,67 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import type { FieldPath } from 'react-hook-form' +import type { ChannelFormValues } from './channel-form' + +type ChannelFormErrorMap = Partial< + Record, unknown> +> + +const ADVANCED_SETTINGS_FIELDS = new Set>([ + 'priority', + 'weight', + 'test_model', + 'auto_ban', + 'tag', + 'remark', + 'model_mapping', + 'param_override', + 'header_override', + 'status_code_mapping', + 'force_format', + 'thinking_to_content', + 'pass_through_body_enabled', + 'proxy', + 'system_prompt', + 'system_prompt_override', + 'allow_service_tier', + 'disable_store', + 'allow_safety_identifier', + 'allow_include_obfuscation', + 'allow_inference_geo', + 'allow_speed', + 'claude_beta_query', + 'upstream_model_update_check_enabled', + 'upstream_model_update_auto_sync_enabled', + 'upstream_model_update_ignored_models', +]) + +export function isAdvancedSettingsField( + fieldName: string +): fieldName is FieldPath { + return ADVANCED_SETTINGS_FIELDS.has(fieldName as FieldPath) +} + +export function hasAdvancedSettingsErrors( + errors: ChannelFormErrorMap +): boolean { + return Object.keys(errors).some((fieldName) => + isAdvancedSettingsField(fieldName) + ) +} diff --git a/web/default/src/features/channels/lib/index.ts b/web/default/src/features/channels/lib/index.ts index c22feb66..32059281 100644 --- a/web/default/src/features/channels/lib/index.ts +++ b/web/default/src/features/channels/lib/index.ts @@ -18,6 +18,7 @@ For commercial licensing, please contact support@quantumnous.com */ // Re-export all library functions export * from './channel-actions' +export * from './channel-form-errors' export * from './channel-form' export * from './channel-type-config' export * from './channel-utils' From cb5c0453f5c4de44df4a86ffc973517123da1454 Mon Sep 17 00:00:00 2001 From: QuentinHsu Date: Tue, 2 Jun 2026 12:31:32 +0800 Subject: [PATCH 2/3] fix(channels): avoid expanding advanced settings for model mapping - remove model mapping from advanced settings error detection so visible model configuration errors do not expand the advanced panel. - update the edit-time advanced settings auto-expand check to only depend on fields actually rendered in the advanced section. - add regression coverage to verify model_mapping errors are not classified as advanced settings errors. --- .../components/drawers/channel-mutate-drawer.tsx | 1 - .../features/channels/lib/channel-form-errors.test.ts | 11 +++++++++++ .../src/features/channels/lib/channel-form-errors.ts | 1 - 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx index 5c6c53de..25258d92 100644 --- a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx +++ b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx @@ -205,7 +205,6 @@ function readAdvancedSettingsPreference(): boolean { function hasAdvancedSettingsValues(values: ChannelFormValues): boolean { return Boolean( - values.model_mapping?.trim() || values.param_override?.trim() || values.header_override?.trim() || values.status_code_mapping?.trim() || diff --git a/web/default/src/features/channels/lib/channel-form-errors.test.ts b/web/default/src/features/channels/lib/channel-form-errors.test.ts index c5be0bcd..286b8d5c 100644 --- a/web/default/src/features/channels/lib/channel-form-errors.test.ts +++ b/web/default/src/features/channels/lib/channel-form-errors.test.ts @@ -49,6 +49,17 @@ describe('channel form errors', () => { assert.equal(hasAdvancedSettingsErrors(errors), false) }) + test('does not treat model mapping errors as advanced settings errors', () => { + const errors = { + model_mapping: { + type: 'custom', + message: 'Invalid model mapping', + }, + } satisfies FieldErrors + + assert.equal(hasAdvancedSettingsErrors(errors), false) + }) + test('classifies schema errors from invalid advanced JSON fields', () => { const result = channelFormSchema.safeParse({ ...CHANNEL_FORM_DEFAULT_VALUES, diff --git a/web/default/src/features/channels/lib/channel-form-errors.ts b/web/default/src/features/channels/lib/channel-form-errors.ts index 68de4b8d..5f3a5bfd 100644 --- a/web/default/src/features/channels/lib/channel-form-errors.ts +++ b/web/default/src/features/channels/lib/channel-form-errors.ts @@ -30,7 +30,6 @@ const ADVANCED_SETTINGS_FIELDS = new Set>([ 'auto_ban', 'tag', 'remark', - 'model_mapping', 'param_override', 'header_override', 'status_code_mapping', From 7791b7842983c7476636b1be9793ed89c8f3703a Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Tue, 2 Jun 2026 14:28:35 +0800 Subject: [PATCH 3/3] chore(fd): delete the test file --- .../channels/lib/channel-form-errors.test.ts | 82 ------------------- 1 file changed, 82 deletions(-) delete mode 100644 web/default/src/features/channels/lib/channel-form-errors.test.ts diff --git a/web/default/src/features/channels/lib/channel-form-errors.test.ts b/web/default/src/features/channels/lib/channel-form-errors.test.ts deleted file mode 100644 index 286b8d5c..00000000 --- a/web/default/src/features/channels/lib/channel-form-errors.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* -Copyright (C) 2023-2026 QuantumNous - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact support@quantumnous.com -*/ -import type { FieldErrors } from 'react-hook-form' -import assert from 'node:assert/strict' -import { describe, test } from 'node:test' -import { - CHANNEL_FORM_DEFAULT_VALUES, - channelFormSchema, - type ChannelFormValues, -} from './channel-form' -import { hasAdvancedSettingsErrors } from './channel-form-errors' - -describe('channel form errors', () => { - test('detects validation errors in collapsed advanced JSON fields', () => { - const errors = { - param_override: { - type: 'custom', - message: 'Invalid JSON', - }, - } satisfies FieldErrors - - assert.equal(hasAdvancedSettingsErrors(errors), true) - }) - - test('ignores validation errors outside advanced settings', () => { - const errors = { - name: { - type: 'too_small', - message: 'Name is required', - }, - } satisfies FieldErrors - - assert.equal(hasAdvancedSettingsErrors(errors), false) - }) - - test('does not treat model mapping errors as advanced settings errors', () => { - const errors = { - model_mapping: { - type: 'custom', - message: 'Invalid model mapping', - }, - } satisfies FieldErrors - - assert.equal(hasAdvancedSettingsErrors(errors), false) - }) - - test('classifies schema errors from invalid advanced JSON fields', () => { - const result = channelFormSchema.safeParse({ - ...CHANNEL_FORM_DEFAULT_VALUES, - name: 'OpenAI', - type: 1, - key: 'sk-test', - models: 'gpt-4o', - group: ['default'], - param_override: '{', - }) - - assert.equal(result.success, false) - if (result.success) return - - const errors = result.error.flatten().fieldErrors - - assert.ok(errors.param_override?.length) - assert.equal(hasAdvancedSettingsErrors(errors), true) - }) -})