mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-06 20:51:36 +02:00
improve form error codes
This commit is contained in:
@@ -1721,6 +1721,11 @@
|
|||||||
"aiProviderErrorUpdate": "Failed to update AI provider",
|
"aiProviderErrorUpdate": "Failed to update AI provider",
|
||||||
"aiProviderErrorDelete": "Failed to delete AI provider",
|
"aiProviderErrorDelete": "Failed to delete AI provider",
|
||||||
"aiProviderErrorLoad": "Failed to load AI provider",
|
"aiProviderErrorLoad": "Failed to load AI provider",
|
||||||
|
"aiProviderErrorUpstreamUrlInvalid": "Enter a valid upstream URL",
|
||||||
|
"aiProviderErrorUpstreamUrlRequired": "Upstream URL is required for this provider",
|
||||||
|
"aiProviderErrorAuthTypeRequired": "Auth type is required",
|
||||||
|
"aiProviderErrorApiKeyRequired": "API key is required",
|
||||||
|
"aiProviderErrorRoutingModeTarget": "Site targets routing is only available for custom providers",
|
||||||
"aiProviderCreated": "AI provider created",
|
"aiProviderCreated": "AI provider created",
|
||||||
"aiProviderUpdated": "AI provider updated",
|
"aiProviderUpdated": "AI provider updated",
|
||||||
"aiProviderDeleted": "AI provider deleted",
|
"aiProviderDeleted": "AI provider deleted",
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
|||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import {
|
import {
|
||||||
aiProviderFormSchema,
|
createAiProviderFormSchema,
|
||||||
toAiProviderAuthPayload,
|
toAiProviderAuthPayload,
|
||||||
type AiProviderFormValues
|
type AiProviderFormValues
|
||||||
} from "@app/lib/aiProviderFormSchema";
|
} from "@app/lib/aiProviderFormSchema";
|
||||||
@@ -43,7 +43,7 @@ import type { CreateOrEditAiProviderResponse } from "@server/routers/aiProvider/
|
|||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosResponse } from "axios";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
export default function AiProviderAuthenticationPage() {
|
export default function AiProviderAuthenticationPage() {
|
||||||
@@ -54,8 +54,10 @@ export default function AiProviderAuthenticationPage() {
|
|||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const [saveLoading, setSaveLoading] = useState(false);
|
const [saveLoading, setSaveLoading] = useState(false);
|
||||||
|
|
||||||
|
const formSchema = useMemo(() => createAiProviderFormSchema(t), [t]);
|
||||||
|
|
||||||
const form = useForm<AiProviderFormValues>({
|
const form = useForm<AiProviderFormValues>({
|
||||||
resolver: zodResolver(aiProviderFormSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: provider.name,
|
name: provider.name,
|
||||||
type: provider.type as AiProviderType,
|
type: provider.type as AiProviderType,
|
||||||
|
|||||||
@@ -32,17 +32,10 @@ import type { CreateOrEditAiProviderResponse } from "@server/routers/aiProvider/
|
|||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosResponse } from "axios";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const generalSchema = z.object({
|
|
||||||
name: z.string().trim().min(1),
|
|
||||||
enabled: z.boolean()
|
|
||||||
});
|
|
||||||
|
|
||||||
type GeneralFormValues = z.infer<typeof generalSchema>;
|
|
||||||
|
|
||||||
export default function AiProviderGeneralPage() {
|
export default function AiProviderGeneralPage() {
|
||||||
const { provider, updateProvider } = useAiProviderContext();
|
const { provider, updateProvider } = useAiProviderContext();
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
@@ -51,6 +44,20 @@ export default function AiProviderGeneralPage() {
|
|||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const [saveLoading, setSaveLoading] = useState(false);
|
const [saveLoading, setSaveLoading] = useState(false);
|
||||||
|
|
||||||
|
const generalSchema = useMemo(
|
||||||
|
() =>
|
||||||
|
z.object({
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, { message: t("nameRequired") }),
|
||||||
|
enabled: z.boolean()
|
||||||
|
}),
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
|
type GeneralFormValues = z.infer<typeof generalSchema>;
|
||||||
|
|
||||||
const form = useForm<GeneralFormValues>({
|
const form = useForm<GeneralFormValues>({
|
||||||
resolver: zodResolver(generalSchema),
|
resolver: zodResolver(generalSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
|||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import {
|
import {
|
||||||
aiProviderFormSchema,
|
createAiProviderFormSchema,
|
||||||
showsUpstreamUrlField,
|
showsUpstreamUrlField,
|
||||||
toAiProviderNetworkPayload,
|
toAiProviderNetworkPayload,
|
||||||
upstreamUrlRequired,
|
upstreamUrlRequired,
|
||||||
@@ -54,7 +54,7 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosResponse } from "axios";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useRef, useState } from "react";
|
import { useMemo, useRef, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
export default function AiProviderNetworkPage() {
|
export default function AiProviderNetworkPage() {
|
||||||
@@ -68,8 +68,10 @@ export default function AiProviderNetworkPage() {
|
|||||||
const [saveLoading, setSaveLoading] = useState(false);
|
const [saveLoading, setSaveLoading] = useState(false);
|
||||||
const targetsFormRef = useRef<ProxyResourceTargetsFormHandle>(null);
|
const targetsFormRef = useRef<ProxyResourceTargetsFormHandle>(null);
|
||||||
|
|
||||||
|
const formSchema = useMemo(() => createAiProviderFormSchema(t), [t]);
|
||||||
|
|
||||||
const form = useForm<AiProviderFormValues>({
|
const form = useForm<AiProviderFormValues>({
|
||||||
resolver: zodResolver(aiProviderFormSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: provider.name,
|
name: provider.name,
|
||||||
type: provider.type as AiProviderType,
|
type: provider.type as AiProviderType,
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
|||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import {
|
import {
|
||||||
aiProviderCreateFormSchema,
|
createAiProviderCreateFormSchema,
|
||||||
defaultAuthTypeForProvider,
|
defaultAuthTypeForProvider,
|
||||||
emptyUpstreamForType,
|
emptyUpstreamForType,
|
||||||
showsUpstreamUrlField,
|
showsUpstreamUrlField,
|
||||||
@@ -52,7 +52,7 @@ import type { CreateOrEditAiProviderResponse } from "@server/routers/aiProvider/
|
|||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosResponse } from "axios";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useRef, useState } from "react";
|
import { useMemo, useRef, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
export default function CreateAiProviderPage() {
|
export default function CreateAiProviderPage() {
|
||||||
@@ -65,8 +65,10 @@ export default function CreateAiProviderPage() {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const targetsRef = useRef<LocalTarget[]>([]);
|
const targetsRef = useRef<LocalTarget[]>([]);
|
||||||
|
|
||||||
|
const formSchema = useMemo(() => createAiProviderCreateFormSchema(t), [t]);
|
||||||
|
|
||||||
const form = useForm<AiProviderFormValues>({
|
const form = useForm<AiProviderFormValues>({
|
||||||
resolver: zodResolver(aiProviderCreateFormSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
type: "openai",
|
type: "openai",
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
type AiProviderType
|
type AiProviderType
|
||||||
} from "@server/lib/aiProviderDefaults";
|
} from "@server/lib/aiProviderDefaults";
|
||||||
|
|
||||||
|
type TranslateFn = (key: string) => string;
|
||||||
|
|
||||||
export const aiProviderTypeValues = [
|
export const aiProviderTypeValues = [
|
||||||
"openai",
|
"openai",
|
||||||
"anthropic",
|
"anthropic",
|
||||||
@@ -20,82 +22,88 @@ export const aiProviderTypeValues = [
|
|||||||
"custom"
|
"custom"
|
||||||
] as const satisfies readonly AiProviderType[];
|
] as const satisfies readonly AiProviderType[];
|
||||||
|
|
||||||
export const aiProviderFormSchema = z
|
export function createAiProviderFormSchema(t: TranslateFn) {
|
||||||
.object({
|
return z
|
||||||
name: z.string().trim().min(1),
|
.object({
|
||||||
type: z.enum(aiProviderTypeValues),
|
name: z
|
||||||
upstreamUrl: z.string().optional().nullable(),
|
.string()
|
||||||
apiKey: z.string().optional(),
|
.trim()
|
||||||
authType: z.enum(AI_PROVIDER_AUTH_TYPES).optional().nullable(),
|
.min(1, { message: t("nameRequired") }),
|
||||||
routingMode: z.enum(["url", "target"]).optional(),
|
type: z.enum(aiProviderTypeValues),
|
||||||
skipTlsVerification: z.boolean().optional(),
|
upstreamUrl: z.string().optional().nullable(),
|
||||||
enabled: z.boolean().optional()
|
apiKey: z.string().optional(),
|
||||||
})
|
authType: z.enum(AI_PROVIDER_AUTH_TYPES).optional().nullable(),
|
||||||
.superRefine((data, ctx) => {
|
routingMode: z.enum(["url", "target"]).optional(),
|
||||||
const routingMode =
|
skipTlsVerification: z.boolean().optional(),
|
||||||
data.type === "custom" ? (data.routingMode ?? "url") : "url";
|
enabled: z.boolean().optional()
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
const routingMode =
|
||||||
|
data.type === "custom" ? (data.routingMode ?? "url") : "url";
|
||||||
|
|
||||||
if (data.type !== "custom" && data.routingMode === "target") {
|
if (data.type !== "custom" && data.routingMode === "target") {
|
||||||
ctx.addIssue({
|
|
||||||
code: "custom",
|
|
||||||
message:
|
|
||||||
"routingMode target is only allowed for custom providers",
|
|
||||||
path: ["routingMode"]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const upstreamUrl =
|
|
||||||
data.upstreamUrl && data.upstreamUrl.trim().length > 0
|
|
||||||
? data.upstreamUrl.trim()
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (upstreamUrl) {
|
|
||||||
try {
|
|
||||||
new URL(upstreamUrl);
|
|
||||||
} catch {
|
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: "custom",
|
code: "custom",
|
||||||
message: "Invalid URL",
|
message: t("aiProviderErrorRoutingModeTarget"),
|
||||||
|
path: ["routingMode"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const upstreamUrl =
|
||||||
|
data.upstreamUrl && data.upstreamUrl.trim().length > 0
|
||||||
|
? data.upstreamUrl.trim()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (upstreamUrl) {
|
||||||
|
try {
|
||||||
|
new URL(upstreamUrl);
|
||||||
|
} catch {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: t("aiProviderErrorUpstreamUrlInvalid"),
|
||||||
|
path: ["upstreamUrl"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
providerRequiresUpstreamUrl(data.type, routingMode) &&
|
||||||
|
!upstreamUrl
|
||||||
|
) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: t("aiProviderErrorUpstreamUrlRequired"),
|
||||||
path: ["upstreamUrl"]
|
path: ["upstreamUrl"]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (!data.authType) {
|
||||||
providerRequiresUpstreamUrl(data.type, routingMode) &&
|
ctx.addIssue({
|
||||||
!upstreamUrl
|
code: "custom",
|
||||||
) {
|
message: t("aiProviderErrorAuthTypeRequired"),
|
||||||
ctx.addIssue({
|
path: ["authType"]
|
||||||
code: "custom",
|
});
|
||||||
message: `upstreamUrl is required for ${data.type} providers`,
|
}
|
||||||
path: ["upstreamUrl"]
|
});
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.authType) {
|
export function createAiProviderCreateFormSchema(t: TranslateFn) {
|
||||||
ctx.addIssue({
|
return createAiProviderFormSchema(t).superRefine((data, ctx) => {
|
||||||
code: "custom",
|
|
||||||
message: "authType is required",
|
|
||||||
path: ["authType"]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export type AiProviderFormValues = z.infer<typeof aiProviderFormSchema>;
|
|
||||||
|
|
||||||
export const aiProviderCreateFormSchema = aiProviderFormSchema.superRefine(
|
|
||||||
(data, ctx) => {
|
|
||||||
const authType: AiProviderAuthType = data.authType ?? "bearer";
|
const authType: AiProviderAuthType = data.authType ?? "bearer";
|
||||||
|
|
||||||
if (authTypeRequiresApiKey(authType) && !data.apiKey?.trim()) {
|
if (authTypeRequiresApiKey(authType) && !data.apiKey?.trim()) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: "custom",
|
code: "custom",
|
||||||
message: "API key is required",
|
message: t("aiProviderErrorApiKeyRequired"),
|
||||||
path: ["apiKey"]
|
path: ["apiKey"]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
);
|
}
|
||||||
|
|
||||||
|
export type AiProviderFormValues = z.infer<
|
||||||
|
ReturnType<typeof createAiProviderFormSchema>
|
||||||
|
>;
|
||||||
|
|
||||||
export function defaultAuthTypeForProvider(
|
export function defaultAuthTypeForProvider(
|
||||||
type: AiProviderType
|
type: AiProviderType
|
||||||
|
|||||||
Reference in New Issue
Block a user