fix(web): prevent plugin config form overflow

This commit is contained in:
Junyan Qin
2026-05-20 19:55:21 +08:00
parent aa8d53dde6
commit 49064ffc2d
6 changed files with 86 additions and 77 deletions

View File

@@ -126,13 +126,13 @@ function WebhookUrlField({
};
return (
<FormItem>
<FormLabel>{label}</FormLabel>
<div className="flex items-center gap-2">
<FormItem className="min-w-0">
<FormLabel className="break-words">{label}</FormLabel>
<div className="flex min-w-0 items-center gap-2">
<Input
value={url}
readOnly
className="flex-1 bg-muted"
className="min-w-0 flex-1 bg-muted"
onClick={(e) => (e.target as HTMLInputElement).select()}
/>
<Button
@@ -149,11 +149,11 @@ function WebhookUrlField({
</Button>
</div>
{extraUrl && (
<div className="flex items-center gap-2 mt-2">
<div className="mt-2 flex min-w-0 items-center gap-2">
<Input
value={extraUrl}
readOnly
className="flex-1 bg-muted"
className="min-w-0 flex-1 bg-muted"
onClick={(e) => (e.target as HTMLInputElement).select()}
/>
<Button
@@ -171,12 +171,14 @@ function WebhookUrlField({
</div>
)}
{description && (
<p className="text-sm text-muted-foreground">{description}</p>
<p className="text-sm break-words text-muted-foreground">
{description}
</p>
)}
{systemInfo.edition === 'community' && (
<div className="flex items-start gap-2.5 rounded-md border border-border/60 bg-muted/40 px-3 py-2.5 mt-1 max-w-2xl">
<div className="mt-1 flex max-w-full min-w-0 items-start gap-2.5 rounded-md border border-border/60 bg-muted/40 px-3 py-2.5">
<Globe className="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" />
<p className="text-sm text-muted-foreground leading-relaxed">
<p className="text-sm leading-relaxed break-words text-muted-foreground">
{t('bots.webhookSaasHint')}{' '}
<a
href="https://space.langbot.app/cloud?utm_source=local_webui&utm_medium=webhook_alert&utm_campaign=saas_conversion"
@@ -442,7 +444,7 @@ export default function DynamicFormComponent({
return (
<Form {...form}>
<div className="space-y-4">
<div className="min-w-0 max-w-full space-y-4 overflow-x-hidden">
{itemConfigList.map((config) => {
if (config.show_if) {
const dependValue = resolveShowIfValue(
@@ -582,20 +584,20 @@ export default function DynamicFormComponent({
control={form.control}
name={config.name as keyof FormValues}
render={({ field }) => (
<FormItem>
<FormItem className="min-w-0">
<div
className={cn(
'flex flex-row items-center justify-between rounded-lg border p-4 max-w-2xl',
'flex w-full min-w-0 max-w-full flex-row items-center justify-between rounded-lg border p-4',
isFieldDisabled && 'pointer-events-none opacity-60',
)}
>
<div className="space-y-0.5">
<FormLabel className="text-base flex items-center gap-1.5">
<div className="min-w-0 space-y-0.5">
<FormLabel className="flex min-w-0 items-center gap-1.5 text-base">
{extractI18nObject(config.label)}
{renderDisabledTooltipIcon()}
</FormLabel>
{config.description && (
<p className="text-sm text-muted-foreground">
<p className="text-sm break-words text-muted-foreground">
{extractI18nObject(config.description)}
</p>
)}
@@ -621,9 +623,9 @@ export default function DynamicFormComponent({
control={form.control}
name={config.name as keyof FormValues}
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-1.5">
<span>
<FormItem className="min-w-0">
<FormLabel className="flex min-w-0 items-center gap-1.5">
<span className="min-w-0 break-words">
{extractI18nObject(config.label)}{' '}
{config.required && (
<span className="text-red-500">*</span>
@@ -633,9 +635,10 @@ export default function DynamicFormComponent({
</FormLabel>
<FormControl>
<div
className={
isFieldDisabled ? 'pointer-events-none opacity-60' : ''
}
className={cn(
'min-w-0 max-w-full overflow-x-hidden',
isFieldDisabled && 'pointer-events-none opacity-60',
)}
>
<DynamicFormItemComponent
config={config}
@@ -645,7 +648,7 @@ export default function DynamicFormComponent({
</div>
</FormControl>
{config.description && (
<p className="text-sm text-muted-foreground">
<p className="text-sm break-words text-muted-foreground">
{extractI18nObject(config.description)}
</p>
)}

View File

@@ -69,7 +69,6 @@ export default function DynamicFormItemComponent({
onFileUploaded,
}: {
config: IDynamicFormItemSchema;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
field: ControllerRenderProps<any, any>;
onFileUploaded?: (fileKey: string) => void;
}) {
@@ -251,7 +250,7 @@ export default function DynamicFormItemComponent({
return (
<Input
type="number"
className="max-w-xs"
className="w-full max-w-xs"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
@@ -260,8 +259,8 @@ export default function DynamicFormItemComponent({
case DynamicFormItemType.STRING:
if (config.options && config.options.length > 0) {
return (
<div className="flex items-center gap-1.5 max-w-md">
<Input className="flex-1" {...field} />
<div className="flex w-full max-w-md min-w-0 items-center gap-1.5">
<Input className="min-w-0 flex-1" {...field} />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
@@ -292,21 +291,26 @@ export default function DynamicFormItemComponent({
</div>
);
}
return <Input className="max-w-md" {...field} />;
return <Input className="w-full max-w-md" {...field} />;
case DynamicFormItemType.TEXT:
return <Textarea {...field} className="min-h-[120px] max-w-2xl" />;
return (
<Textarea
{...field}
className="min-h-[120px] w-full max-w-full resize-y overflow-x-hidden break-all"
/>
);
case DynamicFormItemType.BOOLEAN:
return <Switch checked={field.value} onCheckedChange={field.onChange} />;
case DynamicFormItemType.STRING_ARRAY:
return (
<div className="space-y-2 max-w-md">
<div className="w-full max-w-md min-w-0 space-y-2">
{field.value.map((item: string, index: number) => (
<div key={index} className="flex gap-1.5 items-center">
<div key={index} className="flex min-w-0 items-center gap-1.5">
<Input
className="flex-1"
className="min-w-0 flex-1"
value={item}
onChange={(e) => {
const newValue = [...field.value];
@@ -347,7 +351,7 @@ export default function DynamicFormItemComponent({
case DynamicFormItemType.SELECT:
return (
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger className="max-w-md bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectTrigger className="w-full max-w-md bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectValue placeholder={t('common.select')} />
</SelectTrigger>
<SelectContent>
@@ -409,10 +413,10 @@ export default function DynamicFormItemComponent({
];
return (
<div className="max-w-md flex items-center gap-1.5">
<div className="flex-1">
<div className="flex w-full max-w-md min-w-0 items-center gap-1.5">
<div className="min-w-0 flex-1">
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectTrigger className="min-w-0 bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectValue placeholder={t('models.selectModel')} />
</SelectTrigger>
<SelectContent>
@@ -577,9 +581,9 @@ export default function DynamicFormItemComponent({
);
return (
<div className="max-w-md">
<div className="w-full max-w-md min-w-0">
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectTrigger className="min-w-0 bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectValue placeholder={t('knowledge.selectEmbeddingModel')} />
</SelectTrigger>
<SelectContent>
@@ -612,12 +616,12 @@ export default function DynamicFormItemComponent({
);
return (
<div className="max-w-md">
<div className="w-full max-w-md min-w-0">
<Select
value={field.value || '__none__'}
onValueChange={(v) => field.onChange(v === '__none__' ? '' : v)}
>
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectTrigger className="min-w-0 bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectValue placeholder={t('models.rerank')} />
</SelectTrigger>
<SelectContent>
@@ -713,7 +717,7 @@ export default function DynamicFormItemComponent({
placeholder: string,
) => (
<Select value={value} onValueChange={onChange}>
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectTrigger className="min-w-0 bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
@@ -879,14 +883,14 @@ export default function DynamicFormItemComponent({
};
return (
<div className="space-y-3">
<div className="w-full min-w-0 space-y-3">
{/* Primary model selector */}
<div>
<p className="text-xs text-muted-foreground mb-1">
{t('models.fallback.primary')}
</p>
<div className="flex items-center gap-1.5">
<div className="flex-1">
<div className="flex min-w-0 items-center gap-1.5">
<div className="min-w-0 flex-1">
{renderModelSelect(
modelValue.primary,
(val) => updateValue({ primary: val }),
@@ -918,16 +922,16 @@ export default function DynamicFormItemComponent({
{/* Fallback models */}
{modelValue.fallbacks.length > 0 && (
<div className="space-y-2">
<div className="min-w-0 space-y-2">
<p className="text-xs text-muted-foreground">
{t('models.fallback.fallbackList')}
</p>
{modelValue.fallbacks.map((fbUuid: string, index: number) => (
<div key={index} className="flex items-center gap-2">
<div key={index} className="flex min-w-0 items-center gap-2">
<span className="text-xs text-muted-foreground w-4 shrink-0">
{index + 1}.
</span>
<div className="flex-1">
<div className="min-w-0 flex-1">
{renderModelSelect(
fbUuid,
(val) => updateFallbackModel(index, val),
@@ -1003,20 +1007,22 @@ export default function DynamicFormItemComponent({
return (
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectTrigger className="min-w-0 bg-[#ffffff] dark:bg-[#2a2a2e]">
{field.value && field.value !== '__none__' ? (
(() => {
const selectedKb = knowledgeBases.find(
(kb) => kb.uuid === field.value,
);
return (
<div className="flex items-center gap-2">
<div className="flex min-w-0 items-center gap-2">
{selectedKb?.emoji && (
<span className="text-sm shrink-0">
{selectedKb.emoji}
</span>
)}
<span>{selectedKb?.name ?? field.value}</span>
<span className="truncate">
{selectedKb?.name ?? field.value}
</span>
</div>
);
})()
@@ -1066,9 +1072,9 @@ export default function DynamicFormItemComponent({
return (
<>
<div className="space-y-2">
<div className="min-w-0 space-y-2">
{field.value && field.value.length > 0 ? (
<div className="space-y-2">
<div className="min-w-0 space-y-2">
{field.value.map((kbId: string) => {
const currentKb = knowledgeBases.find(
(base) => base.uuid === kbId,
@@ -1078,17 +1084,17 @@ export default function DynamicFormItemComponent({
return (
<div
key={kbId}
className="flex items-center justify-between rounded-lg border p-3 hover:bg-accent"
className="flex min-w-0 items-center justify-between rounded-lg border p-3 hover:bg-accent"
>
<div className="flex items-center gap-2 flex-1">
<div className="flex min-w-0 flex-1 items-center gap-2">
<div className="flex-1 min-w-0">
<div className="font-medium flex items-center gap-2">
<div className="flex min-w-0 items-center gap-2 font-medium">
{currentKb.emoji && (
<span className="text-sm shrink-0">
{currentKb.emoji}
</span>
)}
{currentKb.name}
<span className="truncate">{currentKb.name}</span>
{currentKb.knowledge_engine?.name && (
<span className="text-xs px-2 py-0.5 rounded-full bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300">
{extractI18nObject(
@@ -1098,7 +1104,7 @@ export default function DynamicFormItemComponent({
)}
</div>
{currentKb.description && (
<div className="text-sm text-muted-foreground">
<div className="text-sm break-words text-muted-foreground">
{currentKb.description}
</div>
)}
@@ -1221,7 +1227,7 @@ export default function DynamicFormItemComponent({
case DynamicFormItemType.BOT_SELECTOR:
return (
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectTrigger className="min-w-0 bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectValue placeholder={t('bots.selectBot')} />
</SelectTrigger>
<SelectContent>
@@ -1239,9 +1245,9 @@ export default function DynamicFormItemComponent({
case DynamicFormItemType.TOOLS_SELECTOR:
return (
<>
<div className="space-y-2">
<div className="min-w-0 space-y-2">
{field.value && field.value.length > 0 ? (
<div className="space-y-2">
<div className="min-w-0 space-y-2">
{field.value.map((toolName: string) => {
const currentTool = tools.find(
(tool) => tool.name === toolName,
@@ -1250,12 +1256,12 @@ export default function DynamicFormItemComponent({
return (
<div
key={toolName}
className="flex items-center justify-between rounded-lg border p-3 hover:bg-accent"
className="flex min-w-0 items-center justify-between rounded-lg border p-3 hover:bg-accent"
>
<div className="flex items-center gap-2 flex-1">
<div className="flex min-w-0 flex-1 items-center gap-2">
<Wrench className="h-4 w-4 shrink-0 text-muted-foreground" />
<div className="flex-1 min-w-0">
<div className="font-medium">{toolName}</div>
<div className="truncate font-medium">{toolName}</div>
{currentTool?.human_desc && (
<div className="text-sm text-muted-foreground truncate">
{currentTool.human_desc}
@@ -1379,13 +1385,16 @@ export default function DynamicFormItemComponent({
? field.value
: [{ role: 'system', content: '' }];
return (
<div className="space-y-2">
<div className="min-w-0 space-y-2">
{promptItems.map(
(item: { role: string; content: string }, index: number) => (
<div key={index} className="flex gap-2 items-center">
<div
key={index}
className="flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center"
>
{/* 角色选择 */}
{index === 0 ? (
<div className="w-[120px] px-3 py-2 border rounded bg-gray-50 dark:bg-[#2a292e] text-gray-500 dark:text-white dark:border-gray-600">
<div className="w-full shrink-0 rounded border bg-gray-50 px-3 py-2 text-gray-500 sm:w-[120px] dark:border-gray-600 dark:bg-[#2a292e] dark:text-white">
system
</div>
) : (
@@ -1410,7 +1419,7 @@ export default function DynamicFormItemComponent({
)}
{/* 内容输入 */}
<Textarea
className="w-[300px]"
className="min-h-20 w-full min-w-0 flex-1 resize-y overflow-x-hidden break-all sm:w-[300px]"
value={item.content}
onChange={(e) => {
const newValue = [...(field.value ?? promptItems)];
@@ -1428,7 +1437,6 @@ export default function DynamicFormItemComponent({
className="p-2 hover:bg-gray-100 rounded"
onClick={() => {
const newValue = (field.value ?? promptItems).filter(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(_: any, i: number) => i !== index,
);
field.onChange(newValue);

View File

@@ -208,7 +208,7 @@ export default function PluginDetailContent({ id }: { id: string }) {
</div>
<div className="flex min-h-0 max-w-full flex-1 flex-col gap-6 overflow-y-auto md:flex-row md:overflow-hidden">
<div className="space-y-4 pb-6 md:min-h-0 md:w-[380px] md:flex-shrink-0 md:overflow-y-auto md:overflow-x-hidden xl:w-[420px]">
<div className="min-w-0 max-w-full space-y-4 pb-6 md:min-h-0 md:w-[380px] md:flex-shrink-0 md:overflow-y-auto md:overflow-x-hidden xl:w-[420px]">
<PluginForm
pluginAuthor={pluginAuthor}
pluginName={pluginName}

View File

@@ -42,7 +42,6 @@ export default function PluginForm({
setPluginConfig(res);
// 提取初始配置中的所有文件 key
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const extractFileKeys = (obj: any): string[] => {
const keys: string[] = [];
if (obj && typeof obj === 'object') {
@@ -77,7 +76,6 @@ export default function PluginForm({
);
// 提取最终保存的配置中的所有文件 key
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const extractFileKeys = (obj: any): string[] => {
const keys: string[] = [];
if (obj && typeof obj === 'object') {
@@ -143,13 +141,13 @@ export default function PluginForm({
}
return (
<div className="space-y-4">
<Card>
<div className="min-w-0 max-w-full space-y-4">
<Card className="min-w-0 overflow-x-hidden">
<CardHeader>
<CardTitle>{t('plugins.pluginConfig')}</CardTitle>
<CardDescription>{t('plugins.saveConfig')}</CardDescription>
</CardHeader>
<CardContent>
<CardContent className="min-w-0 overflow-x-hidden">
{pluginInfo.manifest.manifest.spec.config.length > 0 ? (
<DynamicFormComponent
itemConfigList={pluginInfo.manifest.manifest.spec.config}

View File

@@ -78,7 +78,7 @@ function FormItem({ className, ...props }: React.ComponentProps<'div'>) {
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn('grid gap-2', className)}
className={cn('grid min-w-0 gap-2', className)}
{...props}
/>
</FormItemContext.Provider>
@@ -128,7 +128,7 @@ function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
<p
data-slot="form-description"
id={formDescriptionId}
className={cn('text-muted-foreground text-sm', className)}
className={cn('text-muted-foreground text-sm break-words', className)}
{...props}
/>
);
@@ -146,7 +146,7 @@ function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
<p
data-slot="form-message"
id={formMessageId}
className={cn('text-destructive text-sm', className)}
className={cn('text-destructive text-sm break-words', className)}
{...props}
>
{body}

View File

@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
<textarea
data-slot="textarea"
className={cn(
'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex min-h-16 w-full min-w-0 max-w-full resize-y overflow-x-hidden rounded-md border bg-transparent px-3 py-2 text-base break-words shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className,
)}
{...props}