mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-07 01:56:53 +00:00
♻️ refactor(channels): rebuild channel create/edit drawer with modular sections and improved form UX
Restructure the default-theme channel create/edit experience to align with classic frontend behavior, modern form UX patterns, and the project's Base UI design system. Channel editor architecture: - Split the monolithic channel mutate drawer into focused section components (basic, API access, auth, models, advanced) with shared drawer layout primitives - Extract submission, toast handling, and react-query cache invalidation into `useChannelMutateForm` - Add a dedicated loading skeleton for channel detail fetch during edit mode - Remove the top-level configuration summary block per UX feedback Form validation and data handling: - Strengthen `channel-form` Zod schema with JSON, model mapping, status code mapping, Codex credential, and Vertex AI key refinements - Move type-specific conditional validation into `superRefine` - Normalize base URL formatting and tighten model mapping value validation Model mapping editor: - Add Visual/JSON tabbed editing with inline JSON and duplicate-key feedback - Improve accessibility for icon-only actions and add model suggestion datalists MultiSelect component: - Replace the custom cmdk-based implementation with Base UI Combobox chips - Align focus, border, ring, disabled, and invalid states with standard Input styling via `ComboboxChips` - Preserve existing API (`options`, `selected`, `onChange`, `allowCreate`, `createLabel`) for all current callers - Support inline custom value creation, comma/newline batch input, searchable options, portal-based dropdown positioning, and chip removal Models & groups UX: - Integrate manual custom model entry directly into the model MultiSelect - Remove the separate manual model input/button block - Keep selected-model count and existing model-mapping guardrail behavior i18n: - Add and sync translation keys for new editor sections, validation messages, model mapping UI, and MultiSelect empty/create labels across en, zh, fr, ja, ru, and vi - Remove obsolete keys tied to the deprecated summary and manual model entry UI Affected areas: - `web/default/src/features/channels/components/drawers/` - `web/default/src/features/channels/hooks/use-channel-mutate-form.ts` - `web/default/src/features/channels/lib/channel-form.ts` - `web/default/src/features/channels/lib/model-mapping-validation.ts` - `web/default/src/features/channels/components/model-mapping-editor.tsx` - `web/default/src/components/multi-select.tsx` - `web/default/src/i18n/locales/*.json`
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import { createChannel, updateChannel } from '../api'
|
||||
import { ERROR_MESSAGES, SUCCESS_MESSAGES } from '../constants'
|
||||
import {
|
||||
transformFormDataToCreatePayload,
|
||||
transformFormDataToUpdatePayload,
|
||||
type ChannelFormValues,
|
||||
} from '../lib'
|
||||
import type { Channel } from '../types'
|
||||
|
||||
type UseChannelMutateFormParams = {
|
||||
currentRow?: Channel | null
|
||||
isEditing: boolean
|
||||
isMultiKeyChannel: boolean
|
||||
onSuccess: () => void
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null
|
||||
}
|
||||
|
||||
function getErrorMessage(error: unknown): string | undefined {
|
||||
if (error instanceof Error && typeof error.message === 'string') {
|
||||
return error.message
|
||||
}
|
||||
|
||||
if (!isRecord(error)) return undefined
|
||||
|
||||
const response = error.response
|
||||
if (isRecord(response)) {
|
||||
const data = response.data
|
||||
if (isRecord(data)) {
|
||||
const message = data.message
|
||||
if (typeof message === 'string') return message
|
||||
}
|
||||
}
|
||||
|
||||
const message = error.message
|
||||
if (typeof message === 'string') return message
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function useChannelMutateForm(props: UseChannelMutateFormParams) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (data: ChannelFormValues): Promise<string> => {
|
||||
if (props.isEditing && props.currentRow) {
|
||||
const payload = transformFormDataToUpdatePayload(
|
||||
data,
|
||||
props.currentRow.id
|
||||
)
|
||||
const payloadWithKeyMode =
|
||||
props.isMultiKeyChannel && data.key_mode
|
||||
? {
|
||||
...payload,
|
||||
key_mode: data.key_mode,
|
||||
}
|
||||
: payload
|
||||
|
||||
const response = await updateChannel(
|
||||
props.currentRow.id,
|
||||
payloadWithKeyMode
|
||||
)
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || t(ERROR_MESSAGES.UPDATE_FAILED))
|
||||
}
|
||||
return SUCCESS_MESSAGES.UPDATED
|
||||
}
|
||||
|
||||
const payload = transformFormDataToCreatePayload(data)
|
||||
const response = await createChannel(payload)
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || t(ERROR_MESSAGES.CREATE_FAILED))
|
||||
}
|
||||
return SUCCESS_MESSAGES.CREATED
|
||||
},
|
||||
onSuccess: (messageKey) => {
|
||||
toast.success(t(messageKey))
|
||||
props.onSuccess()
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
toast.error(getErrorMessage(error) || t(ERROR_MESSAGES.CREATE_FAILED))
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user