fix: add form validation

This commit is contained in:
jialin
2026-05-25 13:56:44 +08:00
committed by jialin
parent d99c25dd03
commit acef2ef89a
9 changed files with 167 additions and 39 deletions
+22 -26
View File
@@ -3,17 +3,21 @@ import {
Input as CInput,
Cascader as SealCascader
} from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import _ from 'lodash';
import { useEffect, useMemo, useState } from 'react';
import useQueryModelLoraList, {
LoraOptionGroup
} from '../services/use-query-lora-list';
import loraSelectionStyles from '../style/lora-selection.less';
interface LoraListItemProps {
item: { value: any[]; lora_name: string };
base: string;
defaultDataList: LoraOptionGroup[];
selectedRepoNames: Set<string>;
duplicateNames: Set<string>;
validated: boolean;
onChange: (partial: { value?: any[]; lora_name?: string }) => void;
}
@@ -22,8 +26,11 @@ const LoraListItem: React.FC<LoraListItemProps> = ({
base,
defaultDataList,
selectedRepoNames,
duplicateNames,
validated,
onChange
}) => {
const intl = useIntl();
const { dataList: ownSearchList, fetchData } = useQueryModelLoraList();
const [hasSearched, setHasSearched] = useState(false);
@@ -79,11 +86,15 @@ const LoraListItem: React.FC<LoraListItemProps> = ({
};
const cascaderEmpty = !item.value || item.value.length === 0;
const nameEmpty = !item.lora_name;
const nameEmpty = !item.lora_name?.trim();
const isDuplicate =
!!item.lora_name?.trim() && duplicateNames.has(item.lora_name.trim());
const cascaderStatus =
cascaderEmpty && !nameEmpty ? ('error' as const) : undefined;
validated && cascaderEmpty ? ('error' as const) : 'success';
const inputStatus =
nameEmpty && !cascaderEmpty ? ('error' as const) : undefined;
validated && (nameEmpty || isDuplicate) ? ('error' as const) : 'success';
const displayRender = (labels: any[]) => {
return (
@@ -105,41 +116,27 @@ const LoraListItem: React.FC<LoraListItemProps> = ({
const optionNode = (option: any) => {
const { data } = option;
if (data.isParent) {
return (
<AutoTooltip ghost maxWidth={100}>
{data.label}
</AutoTooltip>
);
}
return (
<AutoTooltip ghost maxWidth={180}>
<AutoTooltip ghost maxWidth={'100%'}>
{data.label}
</AutoTooltip>
);
};
return (
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 120px',
gap: 8,
alignItems: 'center',
flex: 1
}}
>
<div className={loraSelectionStyles.item}>
<SealCascader
showSearch
expandTrigger="hover"
expandTrigger="click"
multiple={false}
alwaysFocus={true}
status={cascaderStatus}
value={item.value}
options={groupedOptions}
onChange={handleCascaderChange}
onSearch={handleSearch}
placeholder="Select LoRA"
showSearch={{
onSearch: handleSearch
}}
placeholder={intl.formatMessage({ id: 'models.form.lora.select' })}
showCheckedStrategy="SHOW_CHILD"
displayRender={displayRender}
optionNode={optionNode}
@@ -158,11 +155,10 @@ const LoraListItem: React.FC<LoraListItemProps> = ({
getPopupContainer={(triggerNode) => triggerNode.parentNode}
></SealCascader>
<CInput.Input
style={{ flex: 1 }}
status={inputStatus}
value={item.lora_name}
onChange={handleNameChange}
placeholder="LoRA name"
placeholder={intl.formatMessage({ id: 'models.form.lora.name' })}
/>
</div>
);
+59 -4
View File
@@ -1,4 +1,5 @@
import { MetadataList } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import { useEffect, useMemo, useRef, useState } from 'react';
import { FormData, LoraListItem } from '../config/types';
@@ -8,6 +9,7 @@ import LoraItem from './lora-list-item';
type ItemValue = { value: any[]; lora_name: string };
const ModelLoraList = () => {
const intl = useIntl();
const form = Form.useFormInstance<FormData>();
const huggingfaceRepoId = Form.useWatch('huggingface_repo_id', form);
const modelScopeModelId = Form.useWatch('model_scope_model_id', form);
@@ -18,6 +20,7 @@ const ModelLoraList = () => {
const { dataList: defaultDataList, fetchData } = useQueryModelLoraList();
const [itemList, setItemList] = useState<ItemValue[]>([]);
const [validated, setValidated] = useState(false);
const initializedRef = useRef(false);
const prevBaseRef = useRef<string>('');
@@ -59,6 +62,19 @@ const ModelLoraList = () => {
);
}, [itemList]);
const duplicateNames = useMemo(() => {
const counts = new Map<string, number>();
itemList.forEach((it) => {
const n = it.lora_name?.trim();
if (n) counts.set(n, (counts.get(n) ?? 0) + 1);
});
return new Set(
Array.from(counts.entries())
.filter(([, c]) => c > 1)
.map(([n]) => n)
);
}, [itemList]);
const syncFormField = (newItemList: ItemValue[]) => {
const newFormList = newItemList
.map((it) => ({
@@ -66,10 +82,16 @@ const ModelLoraList = () => {
lora_repo_name: it.value?.[1] || '',
lora_name: it.lora_name || ''
}))
.filter((it) => it.lora_repo_name?.trim() && it.lora_name?.trim());
.filter((it) => it.lora_repo_name?.trim() || it.lora_name?.trim());
form.setFieldValue('lora_list', newFormList);
};
useEffect(() => {
if (validated) {
form.validateFields(['lora_list']).catch(() => {});
}
}, [itemList, validated]);
const handleItemChange = (
index: number,
partial: { value?: any[]; lora_name?: string }
@@ -93,11 +115,42 @@ const ModelLoraList = () => {
};
return (
<Form.Item<FormData> name="lora_list">
<Form.Item<FormData>
name="lora_list"
rules={[
{
validator: async (_, value: LoraListItem[]) => {
if (!validated) {
setValidated(true);
}
if (!value || value.length === 0) return;
for (const it of value) {
const hasRepo = !!it.lora_repo_name?.trim();
const hasName = !!it.lora_name?.trim();
if (hasRepo !== hasName) {
throw new Error(
intl.formatMessage({ id: 'models.form.lora.rule.empty' })
);
}
}
const names = value
.map((it) => it.lora_name?.trim())
.filter(Boolean) as string[];
if (names.length !== new Set(names).size) {
throw new Error(
intl.formatMessage({ id: 'models.form.lora.rule.duplicate' })
);
}
}
}
]}
>
<MetadataList
label="LoRA Adapter"
label={intl.formatMessage({ id: 'models.form.lora.label' })}
dataList={itemList}
btnText="Add LoRA Adapter"
btnText={intl.formatMessage({ id: 'models.form.lora.add' })}
onAdd={handleAdd}
onDelete={handleDelete}
>
@@ -107,6 +160,8 @@ const ModelLoraList = () => {
base={base}
defaultDataList={defaultDataList}
selectedRepoNames={selectedRepoNames}
duplicateNames={duplicateNames}
validated={validated}
onChange={(partial) => handleItemChange(index, partial)}
/>
)}