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'