mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
feat(providers): support drag-and-drop reordering of the providers list
为服务商列表添加拖拽排序(#753)。复用项目已有的原生 HTML5 拖拽模式(对齐 Sidebar),零新增依赖;通过重排 providers.json 的数组顺序持久化,不引入 order 字段;仅行首手柄可拖,避免与行内操作按钮冲突;采用乐观更新 + 失败回滚。 后端: reorderProviders service、PUT /api/providers/reorder 路由、 ReorderProvidersSchema(校验 orderedIds 为现有 id 的排列)。 前端: providersApi.reorder、store action、Settings 拖拽手柄 UI、中/英/日/韩/繁文案。 测试: 后端 9 用例 + 前端 3 用例。
This commit is contained in:
parent
a932ebe017
commit
8c99b0f892
@ -61,6 +61,10 @@ export const providersApi = {
|
||||
return api.post<{ ok: true }>('/api/providers/official')
|
||||
},
|
||||
|
||||
reorder(orderedIds: string[]) {
|
||||
return api.put<{ providers: SavedProvider[] }>('/api/providers/reorder', { orderedIds })
|
||||
},
|
||||
|
||||
test(id: string, overrides?: { baseUrl?: string; modelId?: string; apiFormat?: string; authStrategy?: string }) {
|
||||
return api.post<TestResultResponse>(`/api/providers/${id}/test`, overrides)
|
||||
},
|
||||
|
||||
@ -381,6 +381,7 @@ export const en = {
|
||||
'settings.providers.setDefault': 'Set default',
|
||||
'settings.providers.test': 'Test',
|
||||
'settings.providers.edit': 'Edit',
|
||||
'settings.providers.dragToReorder': 'Drag to reorder',
|
||||
'settings.providers.requestFailed': 'Request failed',
|
||||
'settings.providers.addTitle': 'Add Provider',
|
||||
'settings.providers.editTitle': 'Edit Provider',
|
||||
|
||||
@ -383,6 +383,7 @@ export const jp: Record<TranslationKey, string> = {
|
||||
'settings.providers.setDefault': 'デフォルトに設定',
|
||||
'settings.providers.test': 'テスト',
|
||||
'settings.providers.edit': '編集',
|
||||
'settings.providers.dragToReorder': 'ドラッグして並べ替え',
|
||||
'settings.providers.requestFailed': 'リクエストに失敗しました',
|
||||
'settings.providers.addTitle': 'プロバイダーを追加',
|
||||
'settings.providers.editTitle': 'プロバイダーを編集',
|
||||
|
||||
@ -383,6 +383,7 @@ export const kr: Record<TranslationKey, string> = {
|
||||
'settings.providers.setDefault': '기본값으로 설정',
|
||||
'settings.providers.test': '테스트',
|
||||
'settings.providers.edit': '편집',
|
||||
'settings.providers.dragToReorder': '드래그하여 순서 변경',
|
||||
'settings.providers.requestFailed': '요청에 실패했습니다',
|
||||
'settings.providers.addTitle': '공급자 추가',
|
||||
'settings.providers.editTitle': '공급자 편집',
|
||||
|
||||
@ -383,6 +383,7 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.providers.setDefault': '設為預設',
|
||||
'settings.providers.test': '測試',
|
||||
'settings.providers.edit': '編輯',
|
||||
'settings.providers.dragToReorder': '拖曳排序',
|
||||
'settings.providers.requestFailed': '請求失敗',
|
||||
'settings.providers.addTitle': '新增服務商',
|
||||
'settings.providers.editTitle': '編輯服務商',
|
||||
|
||||
@ -383,6 +383,7 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.providers.setDefault': '设为默认',
|
||||
'settings.providers.test': '测试',
|
||||
'settings.providers.edit': '编辑',
|
||||
'settings.providers.dragToReorder': '拖动排序',
|
||||
'settings.providers.requestFailed': '请求失败',
|
||||
'settings.providers.addTitle': '添加服务商',
|
||||
'settings.providers.editTitle': '编辑服务商',
|
||||
|
||||
@ -229,6 +229,7 @@ function ProviderSettings() {
|
||||
fetchProviders,
|
||||
fetchPresets,
|
||||
deleteProvider,
|
||||
reorderProviders,
|
||||
activateProvider,
|
||||
activateOfficial,
|
||||
testProvider,
|
||||
@ -240,6 +241,8 @@ function ProviderSettings() {
|
||||
const [pendingDeleteProvider, setPendingDeleteProvider] = useState<SavedProvider | null>(null)
|
||||
const [isDeletingProvider, setIsDeletingProvider] = useState(false)
|
||||
const [testResults, setTestResults] = useState<Record<string, { loading: boolean; result?: ProviderTestResult }>>({})
|
||||
const [draggingId, setDraggingId] = useState<string | null>(null)
|
||||
const [dragOverId, setDragOverId] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
void fetchProviders()
|
||||
@ -289,6 +292,46 @@ function ProviderSettings() {
|
||||
await fetchSettings()
|
||||
}
|
||||
|
||||
const handleProviderDragStart = (event: React.DragEvent, id: string) => {
|
||||
setDraggingId(id)
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
event.dataTransfer.setData('text/plain', id)
|
||||
}
|
||||
|
||||
const handleProviderDragOver = (event: React.DragEvent, overId: string) => {
|
||||
if (!draggingId || draggingId === overId) return
|
||||
event.preventDefault()
|
||||
event.dataTransfer.dropEffect = 'move'
|
||||
if (dragOverId !== overId) setDragOverId(overId)
|
||||
}
|
||||
|
||||
const clearProviderDragState = () => {
|
||||
setDraggingId(null)
|
||||
setDragOverId(null)
|
||||
}
|
||||
|
||||
const handleProviderDrop = (event: React.DragEvent, targetId: string) => {
|
||||
event.preventDefault()
|
||||
const sourceId = draggingId || event.dataTransfer.getData('text/plain')
|
||||
if (!sourceId || sourceId === targetId) {
|
||||
clearProviderDragState()
|
||||
return
|
||||
}
|
||||
|
||||
const ids = providers.map((p) => p.id)
|
||||
const from = ids.indexOf(sourceId)
|
||||
const to = ids.indexOf(targetId)
|
||||
if (from === -1 || to === -1) {
|
||||
clearProviderDragState()
|
||||
return
|
||||
}
|
||||
|
||||
ids.splice(from, 1)
|
||||
ids.splice(to, 0, sourceId)
|
||||
clearProviderDragState()
|
||||
void reorderProviders(ids)
|
||||
}
|
||||
|
||||
const isClaudeOfficialActive = hasLoadedProviders && activeId === null
|
||||
const isOpenAIOfficialActive = hasLoadedProviders && activeId === OPENAI_OFFICIAL_PROVIDER_ID
|
||||
|
||||
@ -382,12 +425,28 @@ function ProviderSettings() {
|
||||
return (
|
||||
<div
|
||||
key={provider.id}
|
||||
className={`relative flex items-center gap-4 px-4 py-3.5 rounded-xl border transition-all group ${
|
||||
onDragOver={(e) => handleProviderDragOver(e, provider.id)}
|
||||
onDrop={(e) => handleProviderDrop(e, provider.id)}
|
||||
onDragLeave={() => setDragOverId((cur) => (cur === provider.id ? null : cur))}
|
||||
className={`relative flex items-center gap-3 px-4 py-3.5 rounded-xl border transition-all group ${
|
||||
isActive
|
||||
? 'border-[var(--color-brand)] bg-[var(--color-surface-container)] shadow-[var(--shadow-focus-ring)]'
|
||||
: 'border-[var(--color-border)] hover:border-[var(--color-border-focus)]'
|
||||
} ${draggingId === provider.id ? 'opacity-50' : ''} ${
|
||||
dragOverId === provider.id ? 'border-[var(--color-brand)] border-dashed' : ''
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
draggable
|
||||
onDragStart={(e) => handleProviderDragStart(e, provider.id)}
|
||||
onDragEnd={clearProviderDragState}
|
||||
role="button"
|
||||
aria-label={t('settings.providers.dragToReorder')}
|
||||
title={t('settings.providers.dragToReorder')}
|
||||
className="material-symbols-outlined text-[18px] text-[var(--color-text-tertiary)] flex-shrink-0 cursor-grab active:cursor-grabbing opacity-0 group-hover:opacity-100 transition-opacity hover:text-[var(--color-text-secondary)]"
|
||||
>
|
||||
drag_indicator
|
||||
</span>
|
||||
<span className={`w-2.5 h-2.5 rounded-full flex-shrink-0 ${isActive ? 'bg-[var(--color-success)]' : 'bg-[var(--color-text-tertiary)]'}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@ -19,6 +19,7 @@ const {
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
reorder: vi.fn(),
|
||||
activate: vi.fn(),
|
||||
activateOfficial: vi.fn(),
|
||||
test: vi.fn(),
|
||||
@ -181,3 +182,69 @@ describe('providerStore runtime refresh', () => {
|
||||
expect(settingsFetchAllMock).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('providerStore reorderProviders', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
chatStoreState.sessions = {}
|
||||
runtimeStoreState.selections = {}
|
||||
providersApiMock.list.mockResolvedValue({ providers: [], activeId: null })
|
||||
})
|
||||
|
||||
it('optimistically applies the new order before the request resolves', async () => {
|
||||
const a = makeProvider({ id: 'a', name: 'A' })
|
||||
const b = makeProvider({ id: 'b', name: 'B' })
|
||||
const c = makeProvider({ id: 'c', name: 'C' })
|
||||
|
||||
let resolveReorder: (value: { providers: SavedProvider[] }) => void = () => {}
|
||||
providersApiMock.reorder.mockReturnValue(
|
||||
new Promise((resolve) => {
|
||||
resolveReorder = resolve
|
||||
}),
|
||||
)
|
||||
|
||||
const { useProviderStore } = await import('./providerStore')
|
||||
useProviderStore.setState({ providers: [a, b, c], activeId: null })
|
||||
|
||||
const promise = useProviderStore.getState().reorderProviders(['c', 'a', 'b'])
|
||||
|
||||
// Optimistic update is visible immediately, before the API resolves.
|
||||
expect(useProviderStore.getState().providers.map((p) => p.id)).toEqual(['c', 'a', 'b'])
|
||||
|
||||
resolveReorder({ providers: [c, a, b] })
|
||||
await promise
|
||||
|
||||
expect(providersApiMock.reorder).toHaveBeenCalledWith(['c', 'a', 'b'])
|
||||
expect(useProviderStore.getState().providers.map((p) => p.id)).toEqual(['c', 'a', 'b'])
|
||||
})
|
||||
|
||||
it('rolls back to the previous order when the request fails', async () => {
|
||||
const a = makeProvider({ id: 'a', name: 'A' })
|
||||
const b = makeProvider({ id: 'b', name: 'B' })
|
||||
providersApiMock.reorder.mockRejectedValue(new Error('network down'))
|
||||
|
||||
const { useProviderStore } = await import('./providerStore')
|
||||
useProviderStore.setState({ providers: [a, b], activeId: null })
|
||||
|
||||
await useProviderStore.getState().reorderProviders(['b', 'a'])
|
||||
|
||||
// Rolls back to the pre-drag order and surfaces the error.
|
||||
expect(useProviderStore.getState().providers.map((p) => p.id)).toEqual(['a', 'b'])
|
||||
expect(useProviderStore.getState().error).toBe('network down')
|
||||
})
|
||||
|
||||
it('refetches instead of reordering when the id set is stale', async () => {
|
||||
const a = makeProvider({ id: 'a', name: 'A' })
|
||||
const b = makeProvider({ id: 'b', name: 'B' })
|
||||
providersApiMock.list.mockResolvedValue({ providers: [a, b], activeId: null })
|
||||
|
||||
const { useProviderStore } = await import('./providerStore')
|
||||
useProviderStore.setState({ providers: [a, b], activeId: null })
|
||||
|
||||
// Only one id supplied — the list changed under us, so don't persist a bad order.
|
||||
await useProviderStore.getState().reorderProviders(['a'])
|
||||
|
||||
expect(providersApiMock.reorder).not.toHaveBeenCalled()
|
||||
expect(providersApiMock.list).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -34,6 +34,7 @@ type ProviderStore = {
|
||||
createProvider: (input: CreateProviderInput) => Promise<SavedProvider>
|
||||
updateProvider: (id: string, input: UpdateProviderInput) => Promise<SavedProvider>
|
||||
deleteProvider: (id: string) => Promise<void>
|
||||
reorderProviders: (orderedIds: string[]) => Promise<void>
|
||||
activateProvider: (id: string) => Promise<void>
|
||||
activateOfficial: () => Promise<void>
|
||||
testProvider: (id: string, overrides?: { baseUrl?: string; modelId?: string; apiFormat?: string; authStrategy?: string }) => Promise<ProviderTestResult>
|
||||
@ -145,6 +146,29 @@ export const useProviderStore = create<ProviderStore>((set, get) => ({
|
||||
await get().fetchProviders()
|
||||
},
|
||||
|
||||
reorderProviders: async (orderedIds) => {
|
||||
const previous = get().providers
|
||||
// Optimistically reorder locally so the drag feels instant…
|
||||
const byId = new Map(previous.map((p) => [p.id, p]))
|
||||
const next = orderedIds
|
||||
.map((id) => byId.get(id))
|
||||
.filter((p): p is SavedProvider => p !== undefined)
|
||||
// Bail out if the id set is stale (e.g. a provider was deleted mid-drag).
|
||||
if (next.length !== previous.length) {
|
||||
await get().fetchProviders()
|
||||
return
|
||||
}
|
||||
set({ providers: next })
|
||||
try {
|
||||
const { providers } = await providersApi.reorder(orderedIds)
|
||||
set({ providers })
|
||||
} catch (err) {
|
||||
// …and roll back to the pre-drag order if persistence fails. `previous`
|
||||
// is the server's last-known truth, so no refetch is needed.
|
||||
set({ providers: previous, error: err instanceof Error ? err.message : String(err) })
|
||||
}
|
||||
},
|
||||
|
||||
activateProvider: async (id) => {
|
||||
await providersApi.activate(id)
|
||||
await get().fetchProviders()
|
||||
|
||||
@ -625,6 +625,81 @@ describe('ProviderService', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ─── reorderProviders ────────────────────────────────────────────────────
|
||||
|
||||
describe('reorderProviders', () => {
|
||||
test('should reorder providers to match the given id order and persist it', async () => {
|
||||
const svc = new ProviderService()
|
||||
const a = await svc.addProvider(sampleInput({ name: 'A' }))
|
||||
const b = await svc.addProvider(sampleInput({ name: 'B' }))
|
||||
const c = await svc.addProvider(sampleInput({ name: 'C' }))
|
||||
|
||||
const result = await svc.reorderProviders([c.id, a.id, b.id])
|
||||
expect(result.map((p) => p.name)).toEqual(['C', 'A', 'B'])
|
||||
|
||||
// Persisted order survives a fresh read
|
||||
const { providers } = await svc.listProviders()
|
||||
expect(providers.map((p) => p.name)).toEqual(['C', 'A', 'B'])
|
||||
|
||||
const config = await readProvidersConfig()
|
||||
expect((config.providers as Array<{ name: string }>).map((p) => p.name)).toEqual(['C', 'A', 'B'])
|
||||
})
|
||||
|
||||
test('should not change activeId when reordering', async () => {
|
||||
const svc = new ProviderService()
|
||||
const a = await svc.addProvider(sampleInput({ name: 'A' }))
|
||||
const b = await svc.addProvider(sampleInput({ name: 'B' }))
|
||||
await svc.activateProvider(a.id)
|
||||
|
||||
await svc.reorderProviders([b.id, a.id])
|
||||
|
||||
const { activeId } = await svc.listProviders()
|
||||
expect(activeId).toBe(a.id)
|
||||
})
|
||||
|
||||
test('should throw 400 when orderedIds is missing a provider', async () => {
|
||||
const svc = new ProviderService()
|
||||
const a = await svc.addProvider(sampleInput({ name: 'A' }))
|
||||
await svc.addProvider(sampleInput({ name: 'B' }))
|
||||
|
||||
try {
|
||||
await svc.reorderProviders([a.id])
|
||||
expect(true).toBe(false)
|
||||
} catch (err: unknown) {
|
||||
const apiErr = err as { statusCode: number }
|
||||
expect(apiErr.statusCode).toBe(400)
|
||||
}
|
||||
})
|
||||
|
||||
test('should throw 400 when orderedIds contains an unknown id', async () => {
|
||||
const svc = new ProviderService()
|
||||
const a = await svc.addProvider(sampleInput({ name: 'A' }))
|
||||
const b = await svc.addProvider(sampleInput({ name: 'B' }))
|
||||
|
||||
try {
|
||||
await svc.reorderProviders([a.id, b.id, 'ghost-id'])
|
||||
expect(true).toBe(false)
|
||||
} catch (err: unknown) {
|
||||
const apiErr = err as { statusCode: number }
|
||||
expect(apiErr.statusCode).toBe(400)
|
||||
}
|
||||
})
|
||||
|
||||
test('should throw 400 when orderedIds contains duplicates', async () => {
|
||||
const svc = new ProviderService()
|
||||
const a = await svc.addProvider(sampleInput({ name: 'A' }))
|
||||
await svc.addProvider(sampleInput({ name: 'B' }))
|
||||
|
||||
try {
|
||||
await svc.reorderProviders([a.id, a.id])
|
||||
expect(true).toBe(false)
|
||||
} catch (err: unknown) {
|
||||
const apiErr = err as { statusCode: number }
|
||||
expect(apiErr.statusCode).toBe(400)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// ─── activateProvider ────────────────────────────────────────────────────
|
||||
|
||||
describe('activateProvider', () => {
|
||||
@ -1518,6 +1593,54 @@ describe('Providers API', () => {
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
// ─── PUT /api/providers/reorder ──────────────────────────────────────────
|
||||
|
||||
test('PUT /api/providers/reorder should reorder providers', async () => {
|
||||
const svc = new ProviderService()
|
||||
const a = await svc.addProvider(sampleInput({ name: 'A' }))
|
||||
const b = await svc.addProvider(sampleInput({ name: 'B' }))
|
||||
|
||||
const { req, url, segments } = makeRequest('PUT', '/api/providers/reorder', {
|
||||
orderedIds: [b.id, a.id],
|
||||
})
|
||||
const res = await handleProvidersApi(req, url, segments)
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
const body = (await res.json()) as { providers: { name: string }[] }
|
||||
expect(body.providers.map((p) => p.name)).toEqual(['B', 'A'])
|
||||
})
|
||||
|
||||
test('PUT /api/providers/reorder should return 400 for a non-permutation', async () => {
|
||||
const svc = new ProviderService()
|
||||
const a = await svc.addProvider(sampleInput({ name: 'A' }))
|
||||
await svc.addProvider(sampleInput({ name: 'B' }))
|
||||
|
||||
const { req, url, segments } = makeRequest('PUT', '/api/providers/reorder', {
|
||||
orderedIds: [a.id], // missing B
|
||||
})
|
||||
const res = await handleProvidersApi(req, url, segments)
|
||||
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
test('PUT /api/providers/reorder should return 400 for empty orderedIds', async () => {
|
||||
const { req, url, segments } = makeRequest('PUT', '/api/providers/reorder', {
|
||||
orderedIds: [],
|
||||
})
|
||||
const res = await handleProvidersApi(req, url, segments)
|
||||
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
test('POST /api/providers/reorder should be method-not-allowed', async () => {
|
||||
const { req, url, segments } = makeRequest('POST', '/api/providers/reorder', {
|
||||
orderedIds: [],
|
||||
})
|
||||
const res = await handleProvidersApi(req, url, segments)
|
||||
|
||||
expect(res.status).toBe(405)
|
||||
})
|
||||
|
||||
test('POST /api/providers should return 400 for invalid auto compact window', async () => {
|
||||
const { req, url, segments } = makeRequest('POST', '/api/providers', {
|
||||
presetId: 'custom',
|
||||
|
||||
@ -22,6 +22,7 @@ import {
|
||||
CreateProviderSchema,
|
||||
UpdateProviderSchema,
|
||||
TestProviderSchema,
|
||||
ReorderProvidersSchema,
|
||||
} from '../types/provider.js'
|
||||
import { ApiError, errorResponse } from '../middleware/errorHandler.js'
|
||||
import { diagnosticsService } from '../services/diagnosticsService.js'
|
||||
@ -72,6 +73,12 @@ export async function handleProvidersApi(
|
||||
return Response.json({ ok: true })
|
||||
}
|
||||
|
||||
// PUT /api/providers/reorder
|
||||
if (id === 'reorder') {
|
||||
if (req.method !== 'PUT') throw methodNotAllowed(req.method)
|
||||
return await handleReorder(req)
|
||||
}
|
||||
|
||||
// /api/providers (no ID)
|
||||
if (!id) {
|
||||
if (req.method === 'GET') {
|
||||
@ -161,6 +168,18 @@ async function handleUpdate(req: Request, id: string): Promise<Response> {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReorder(req: Request): Promise<Response> {
|
||||
const body = await parseJsonBody(req)
|
||||
try {
|
||||
const input = ReorderProvidersSchema.parse(body)
|
||||
const providers = await providerService.reorderProviders(input.orderedIds)
|
||||
return Response.json({ providers })
|
||||
} catch (err) {
|
||||
if (err instanceof z.ZodError) throw ApiError.badRequest(err.issues.map((i) => i.message).join('; '))
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTestUnsaved(req: Request): Promise<Response> {
|
||||
const body = await parseJsonBody(req)
|
||||
try {
|
||||
|
||||
@ -213,6 +213,36 @@ export class ProviderService {
|
||||
await this.writeIndex(index)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder saved providers to match the given id order.
|
||||
*
|
||||
* The display order is simply the array order in providers.json, so reordering
|
||||
* persists by rewriting the array — no separate `order` field is needed.
|
||||
* `orderedIds` must be a permutation of the existing provider ids (same members,
|
||||
* no duplicates, none missing); otherwise the request is rejected so a stale
|
||||
* client can't silently drop or duplicate a provider.
|
||||
*/
|
||||
async reorderProviders(orderedIds: string[]): Promise<SavedProvider[]> {
|
||||
const index = await this.readIndex()
|
||||
|
||||
const currentIds = index.providers.map((p) => p.id)
|
||||
const orderedSet = new Set(orderedIds)
|
||||
const isPermutation =
|
||||
orderedIds.length === currentIds.length &&
|
||||
orderedSet.size === orderedIds.length &&
|
||||
currentIds.every((id) => orderedSet.has(id))
|
||||
|
||||
if (!isPermutation) {
|
||||
throw ApiError.badRequest('orderedIds must be a permutation of existing provider ids')
|
||||
}
|
||||
|
||||
const byId = new Map(index.providers.map((p) => [p.id, p]))
|
||||
index.providers = orderedIds.map((id) => byId.get(id) as SavedProvider)
|
||||
await this.writeIndex(index)
|
||||
|
||||
return index.providers
|
||||
}
|
||||
|
||||
// --- Activation ---
|
||||
|
||||
async activateProvider(id: string): Promise<void> {
|
||||
|
||||
@ -98,6 +98,11 @@ export const TestProviderSchema = z.object({
|
||||
apiFormat: ApiFormatSchema.default('anthropic'),
|
||||
})
|
||||
|
||||
export const ReorderProvidersSchema = z.object({
|
||||
// A permutation of the existing saved provider ids, in the desired display order.
|
||||
orderedIds: z.array(z.string().min(1)).min(1),
|
||||
})
|
||||
|
||||
// TypeScript types
|
||||
export type ModelMapping = z.infer<typeof ModelMappingSchema>
|
||||
export type SavedProvider = z.infer<typeof SavedProviderSchema>
|
||||
@ -105,6 +110,7 @@ export type ProvidersIndex = z.infer<typeof ProvidersIndexSchema>
|
||||
export type CreateProviderInput = z.infer<typeof CreateProviderSchema>
|
||||
export type UpdateProviderInput = z.infer<typeof UpdateProviderSchema>
|
||||
export type TestProviderInput = z.infer<typeof TestProviderSchema>
|
||||
export type ReorderProvidersInput = z.infer<typeof ReorderProvidersSchema>
|
||||
|
||||
export interface ProviderTestStepResult {
|
||||
success: boolean
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user