fix: add form validation
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user