feat(web): form add rules

This commit is contained in:
zhaoying
2026-03-02 14:41:58 +08:00
parent 1270b7cdd8
commit 62b2ecdfc2
18 changed files with 165 additions and 28 deletions

View File

@@ -41,6 +41,8 @@ interface SearchInputProps {
className?: string;
/** Input size */
size?: InputProps['size']
/** Maximum length of the input value */
maxLength?: number;
}
/** Search input component with debounce and throttle support */

View File

@@ -453,6 +453,8 @@ export const en = {
prevStep: 'Previous Step',
exportSuccess: 'Export successful',
recommend: 'Recommend',
logoTip: `Supported image formats: JPG, PNG \n Suggested size: square ratio \n Maximum size: ≤ 2MB`,
imageSquareRequired: 'Please upload a square image',
},
model: {
searchPlaceholder: 'search model…',
@@ -542,7 +544,8 @@ export const en = {
ollama: "Ollama",
xinference: "Xinference",
gpustack: "Gpustack",
bedrock: "Bedrock"
bedrock: "Bedrock",
nameInvalid: 'Model name can only contain letters, numbers, underscores and spaces, cannot be empty or pure whitespace',
},
modelNew: {
group: 'Model Group',

View File

@@ -1029,6 +1029,8 @@ export const zh = {
prevStep: '上一步',
exportSuccess: '导出成功',
recommend: '推荐',
logoTip: `支持图片格式JPG、PNG\n 尺寸:正方形比例 \n 文件大小限制:≤ 2MB`,
imageSquareRequired: '请上传正方形比例图片',
},
model: {
searchPlaceholder: '搜索模型…',
@@ -1176,7 +1178,8 @@ export const zh = {
ollama: "Ollama",
xinference: "Xinference",
gpustack: "Gpustack",
bedrock: "Bedrock"
bedrock: "Bedrock",
nameInvalid: '模型名称只能包含字母、数字、下划线和空格, 不能为空或纯空格',
},
timezones: {
'Asia/Shanghai': '中国标准时间 (UTC+8)',

View File

@@ -0,0 +1,50 @@
/*
* @Author: ZhaoYing
* @Date: 2026-03-02 13:46:53
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-02 14:38:33
*/
/**
* Form validation utilities
*/
interface UploadFile {
originFileObj: Blob;
[key: string]: unknown;
}
/**
* Validate if uploaded image is square (width === height)
* @param errorMessage - Error message to display when validation fails
* @returns Ant Design form validator
*/
export const validateSquareImage = (errorMessage: string = 'Image must be square') => {
return (_: unknown, value: UploadFile | UploadFile[] | undefined) => {
if (!value || (Array.isArray(value) && value.length === 0)) {
return Promise.resolve();
}
const file = Array.isArray(value) ? value[0] : value;
if (file?.originFileObj) {
return new Promise<void>((resolve, reject) => {
const img = new Image();
img.onload = () => {
if (img.width === img.height) {
resolve();
} else {
reject(new Error(errorMessage));
}
};
img.onerror = () => reject(new Error('Failed to load image'));
img.src = URL.createObjectURL(file.originFileObj);
});
}
return Promise.resolve();
};
};
// - Cannot be empty or pure whitespace
// - Cannot start with a space
export const stringRegExp = /^[a-zA-Z0-9\u4e00-\u9fa5][a-zA-Z0-9\u4e00-\u9fa5\s]*$/

View File

@@ -12,6 +12,7 @@ import dayjs from 'dayjs'
import type { ApiKey, ApiKeyModalRef } from '../types';
import RbModal from '@/components/RbModal'
import { createApiKey, updateApiKey } from '@/api/apiKey';
import { stringRegExp } from '@/utils/validator';
const FormItem = Form.Item;
@@ -78,7 +79,7 @@ const ApiKeyModal = forwardRef<ApiKeyModalRef, CreateModalProps>(({
form.validateFields()
.then((values) => {
const { memory, rag, expires_at, ...rest } = values
let scopes = []
const scopes = []
if (memory) {
scopes.push('memory')
@@ -130,7 +131,11 @@ const ApiKeyModal = forwardRef<ApiKeyModalRef, CreateModalProps>(({
<FormItem
name="name"
label={t('apiKey.name')}
rules={[{ required: true, message: t('common.pleaseEnter') }]}
rules={[
{ required: true, message: t('common.pleaseEnter') },
{ max: 50 },
{ pattern: stringRegExp, message: t('common.nameInvalid') },
]}
>
<Input placeholder={t('common.enter')} />
</FormItem>
@@ -138,6 +143,7 @@ const ApiKeyModal = forwardRef<ApiKeyModalRef, CreateModalProps>(({
<FormItem
name="description"
label={t('apiKey.description')}
rules={[{ max: 500 }]}
>
<Input.TextArea placeholder={t('common.pleaseEnter')} rows={3} />
</FormItem>

View File

@@ -169,8 +169,8 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
getApplicationConfig(id as string).then(res => {
const response = res as Config
const { skills, variables } = response
let allSkills = Array.isArray(skills?.skill_ids) ? skills?.skill_ids.map(vo => ({ id: vo })) : []
let allTools = Array.isArray(response.tools) ? response.tools : []
const allSkills = Array.isArray(skills?.skill_ids) ? skills?.skill_ids.map(vo => ({ id: vo })) : []
const allTools = Array.isArray(response.tools) ? response.tools : []
const memoryContent = response.memory?.memory_config_id
const parsedMemoryContent = memoryContent === null || memoryContent === ''
? undefined
@@ -431,7 +431,11 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
</Button>
</div>
<Form.Item name="system_prompt" className="rb:mb-0!">
<Form.Item
name="system_prompt"
className="rb:mb-0!"
rules={[{ max: 10000 }]}
>
<Input.TextArea
placeholder={t('application.promptPlaceholder')}
styles={{

View File

@@ -21,6 +21,7 @@ import WorkflowIcon from '@/assets/images/application/workflow.svg'
import type { ApplicationModalData, ApplicationModalRef, Application } from '../types'
import RbModal from '@/components/RbModal'
import { addApplication, updateApplication } from '@/api/application'
import { stringRegExp } from '@/utils/validator';
const FormItem = Form.Item;
@@ -131,13 +132,18 @@ const ApplicationModal = forwardRef<ApplicationModalRef, ApplicationModalProps>(
<FormItem
name="name"
label={t('application.applicationName')}
rules={[{ required: true, message: t('common.pleaseEnter') }]}
rules={[
{ required: true, message: t('common.pleaseEnter') },
{ max: 50 },
{ pattern: stringRegExp, message: t('common.nameInvalid') },
]}
>
<Input placeholder={t('common.enter')} />
</FormItem>
<FormItem
name="description"
label={t('application.description')}
rules={[{ max: 500 }]}
>
<Input.TextArea placeholder={t('common.enter')} />
</FormItem>

View File

@@ -152,7 +152,11 @@ const MemberModal = forwardRef<MemberModalRef, MemberModalProps>(({
<FormItem
name="email"
label={t('member.email')}
rules={[{ required: true, message: t('common.pleaseEnter') }]}
rules={[
{ required: true, message: t('common.pleaseEnter') },
{ type: 'email' },
{ max: 100 },
]}
>
<Input placeholder={t('common.enterPlaceholder', { title: t('member.email') })} disabled={!!editingUser} />
</FormItem>

View File

@@ -18,6 +18,7 @@ import RbModal from '@/components/RbModal'
import { createMemoryConfig, updateMemoryConfig } from '@/api/memory'
import { getOntologyScenesSimpleUrl } from '@/api/ontology'
import CustomSelect from '@/components/CustomSelect';
import { stringRegExp } from '@/utils/validator';
const FormItem = Form.Item;
@@ -110,7 +111,11 @@ const MemoryForm = forwardRef<MemoryFormRef, MemoryFormProps>(({
<FormItem
name="config_name"
label={t('memory.configurationName')}
rules={[{ required: true, message: t('common.pleaseEnter') }]}
rules={[
{ required: true, message: t('common.pleaseEnter') },
{ max: 50 },
{ pattern: stringRegExp, message: t('common.nameInvalid') },
]}
>
<Input placeholder={t('common.pleaseEnter')} />
</FormItem>
@@ -118,6 +123,7 @@ const MemoryForm = forwardRef<MemoryFormRef, MemoryFormProps>(({
<FormItem
name="config_desc"
label={t('memory.desc')}
rules={[{ max: 500 }]}
>
<Input.TextArea placeholder={t('common.pleaseEnter')} />
</FormItem>

View File

@@ -20,6 +20,7 @@ import CustomSelect from '@/components/CustomSelect'
import UploadImages from '@/components/Upload/UploadImages'
import { updateCustomModel, addCustomModel, modelTypeUrl, modelProviderUrl } from '@/api/models'
import { getFileLink } from '@/api/fileStorage'
import { validateSquareImage, stringRegExp } from '@/utils/validator'
/**
* Custom model modal component
@@ -65,7 +66,7 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
const res = isEdit ? updateCustomModel(model.id, rest) : addCustomModel(data)
res.then(() => {
refresh && refresh(isEdit)
refresh?.(isEdit)
handleClose()
message.success(isEdit ? t('common.updateSuccess') : t('common.createSuccess'))
})
@@ -79,7 +80,7 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
.validateFields()
.then((values) => {
const { logo, ...rest } = values;
let formData: CustomModelForm = {
const formData: CustomModelForm = {
...rest
}
@@ -125,14 +126,22 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
name="logo"
label={t('modelNew.logo')}
valuePropName="fileList"
rules={[{ required: true, message: t('common.pleaseSelect') }]}
rules={[
{ required: true, message: t('common.pleaseSelect') },
{ validator: validateSquareImage(t('common.imageSquareRequired')) }
]}
extra={t('common.logoTip')?.split('\n').map((vo, index) => <div key={index}>{vo}</div>)}
>
<UploadImages />
<UploadImages fileSize={2} />
</Form.Item>
<Form.Item
name="name"
label={t('modelNew.name')}
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.name') }) }]}
rules={[
{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.name') }) },
{ max: 50 },
{ pattern: stringRegExp, message: t('common.nameInvalid') },
]}
>
<Input placeholder={t('common.pleaseEnter')} />
</Form.Item>
@@ -166,6 +175,7 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
<Form.Item
name="description"
label={t('modelNew.description')}
rules={[{ max: 500 }]}
>
<Input.TextArea placeholder={t('common.pleaseEnter')} />
</Form.Item>

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:49:33
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:49:33
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-02 12:23:13
*/
/**
* Group Model Modal
@@ -21,6 +21,7 @@ import { updateCompositeModel, modelTypeUrl, addCompositeModel } from '@/api/mod
import UploadImages from '@/components/Upload/UploadImages'
import ModelImplement from './ModelImplement'
import { getFileLink } from '@/api/fileStorage'
import { validateSquareImage, stringRegExp } from '@/utils/validator'
/**
* Group model modal component
@@ -133,15 +134,26 @@ const GroupModelModal = forwardRef<GroupModelModalRef, GroupModelModalProps>(({
name="logo"
label={t('modelNew.logo')}
valuePropName="fileList"
rules={[{ required: true, message: t('common.pleaseSelect') }]}
rules={[
{ required: true, message: t('common.pleaseSelect') },
{ validator: validateSquareImage(t('common.imageSquareRequired')) }
]}
extra={t('common.logoTip')?.split('\n').map((vo, index) => <div key={index}>{vo}</div>)}
>
<UploadImages />
<UploadImages
fileSize={2}
fileType={['png', 'jpg']}
/>
</Form.Item>
<Form.Item
name="name"
label={t('modelNew.name')}
rules={[{ required: true, message: t('common.pleaseEnter') }]}
rules={[
{ required: true, message: t('common.pleaseEnter') },
{ max: 50 },
{ pattern: stringRegExp, message: t('common.nameInvalid') },
]}
>
<Input placeholder={t('common.pleaseEnter')} />
</Form.Item>
@@ -165,6 +177,7 @@ const GroupModelModal = forwardRef<GroupModelModalRef, GroupModelModalProps>(({
<Form.Item
name="description"
label={t('modelNew.description')}
rules={[{ max: 500 }]}
>
<Input.TextArea placeholder={t('common.pleaseEnter')} />
</Form.Item>

View File

@@ -121,6 +121,7 @@ const tabKeys = ['group', 'list', 'square']
{activeTab !== 'list' &&
<Form.Item name="search" noStyle>
<SearchInput
maxLength={50}
placeholder={t(`modelNew.${activeTab}SearchPlaceholder`)}
className="rb:w-70!"
/>

View File

@@ -182,7 +182,10 @@ const OntologyClassExtractModal = forwardRef<OntologyClassExtractModalRef, Ontol
<FormItem
name="scenario"
label={t('ontology.scenario')}
rules={[{ required: true, message: t('common.pleaseEnter') }]}
rules={[
{ required: true, message: t('common.pleaseEnter') },
{ max: 2000 },
]}
>
<Input.TextArea placeholder={t('ontology.scenarioPlaceholder')} />
</FormItem>

View File

@@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next';
import type { AddClassItem, OntologyClassModalRef } from '../types'
import RbModal from '@/components/RbModal'
import { createOntologyClass } from '@/api/ontology'
import { stringRegExp } from '@/utils/validator';
const FormItem = Form.Item;
@@ -105,7 +106,11 @@ const OntologyClassModal = forwardRef<OntologyClassModalRef, OntologyClassModalP
<FormItem
name="class_name"
label={t('ontology.class_name')}
rules={[{ required: true, message: t('common.pleaseEnter') }]}
rules={[
{ required: true, message: t('common.pleaseEnter') },
{ max: 50 },
{ pattern: stringRegExp, message: t('common.nameInvalid') },
]}
>
<Input placeholder={t('common.enter')} />
</FormItem>
@@ -113,6 +118,7 @@ const OntologyClassModal = forwardRef<OntologyClassModalRef, OntologyClassModalP
<FormItem
name="class_description"
label={t('ontology.class_description')}
rules={[{ max: 500 }]}
>
<Input.TextArea placeholder={t('ontology.classDescriptionPlaceholder')} />
</FormItem>

View File

@@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next';
import type { OntologyItem, OntologyModalData, OntologyModalRef } from '../types'
import RbModal from '@/components/RbModal'
import { createOntologyScene, updateOntologyScene } from '@/api/ontology'
import { stringRegExp } from '@/utils/validator';
const FormItem = Form.Item;
@@ -109,7 +110,11 @@ const OntologyModal = forwardRef<OntologyModalRef, OntologyModalProps>(({
<FormItem
name="scene_name"
label={t('ontology.scene_name')}
rules={[{ required: true, message: t('common.pleaseEnter') }]}
rules={[
{ required: true, message: t('common.pleaseEnter') },
{ max: 50 },
{ pattern: stringRegExp, message: t('common.nameInvalid') },
]}
>
<Input placeholder={t('common.enter')} />
</FormItem>
@@ -117,6 +122,7 @@ const OntologyModal = forwardRef<OntologyModalRef, OntologyModalProps>(({
<FormItem
name="scene_description"
label={t('ontology.scene_description')}
rules={[{ max: 500 }]}
>
<Input.TextArea placeholder={t('ontology.descriptionPlaceholder')} />
</FormItem>

View File

@@ -17,6 +17,7 @@ import type { AiPromptModalRef } from '@/views/ApplicationConfig/types'
import exitIcon from '@/assets/images/knowledgeBase/exit.png';
import type { SkillFormData } from '../types'
import { getSkillDetail, createSkill, updateSkill } from '@/api/skill'
import { stringRegExp } from '@/utils/validator';
/**
* Skill Configuration Page Component
@@ -110,7 +111,7 @@ const SkillConfig: FC = () => {
// Format tools data for API
const formData = {
...rest,
tools: tools?.map((item: any) => ({
tools: tools?.map((item) => ({
tool_id: item.tool_id,
operation: item.operation
}))
@@ -144,13 +145,18 @@ const SkillConfig: FC = () => {
<Form.Item
name="name"
label={t('skills.name')}
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('skills.name') }) }]}
rules={[
{ required: true, message: t('common.inputPlaceholder', { title: t('skills.name') }) },
{ max: 50 },
{ pattern: stringRegExp, message: t('common.nameInvalid') },
]}
>
<Input placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item
name="description"
label={t('skills.description')}
rules={[{ max: 500 }]}
>
<Input.TextArea placeholder={t('skills.descriptionPlaceholder')} />
</Form.Item>

View File

@@ -17,6 +17,8 @@ export interface SkillFormData {
tools: Array<{
/** Tool identifier */
tool_id: string;
/** Tool operation/action */
operation?: string;
}>;
/** Skill configuration settings */
config: {

View File

@@ -23,6 +23,7 @@ import UploadImages from '@/components/Upload/UploadImages'
import { getFileLink } from '@/api/fileStorage'
import ragIcon from '@/assets/images/space/rag.png'
import neo4jIcon from '@/assets/images/space/neo4j.png'
import { stringRegExp } from '@/utils/validator';
const FormItem = Form.Item;
@@ -91,7 +92,7 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
setCurrentStep(1)
} else {
const { icon, ...rest } = values
let formData: SpaceModalData = {
const formData: SpaceModalData = {
...rest
}
if (icon?.response?.data.file_id) {
@@ -164,14 +165,19 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
valuePropName="fileList"
hidden={currentStep === 1}
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('space.spaceIcon') }) }]}
extra={t('common.logoTip')?.split('\n').map((vo, index) => <div key={index}>{vo}</div>)}
>
<UploadImages />
<UploadImages fileSize={2} />
</Form.Item>
<FormItem
name="name"
label={t('space.spaceName')}
hidden={currentStep === 1}
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('space.spaceName') }) }]}
rules={[
{ required: true, message: t('common.inputPlaceholder', { title: t('space.spaceName') }) },
{ max: 50 },
{ pattern: stringRegExp, message: t('common.nameInvalid') },
]}
>
<Input placeholder={t('common.inputPlaceholder', { title: t('space.spaceName') })} />
</FormItem>