Skip to content

Commit 6d72b6b

Browse files
committed
feat: Enhance TemplateForm with improved validation and dynamic field rendering; update LabelStudio config validation for camelCase support
1 parent cefb3b1 commit 6d72b6b

File tree

3 files changed

+104
-39
lines changed

3 files changed

+104
-39
lines changed

frontend/src/pages/DataAnnotation/Template/TemplateForm.tsx

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ const TemplateForm: React.FC<TemplateFormProps> = ({
6868
const values = await form.validateFields();
6969
setLoading(true);
7070

71+
console.log("Form values:", values);
72+
7173
const requestData = {
7274
name: values.name,
7375
description: values.description,
@@ -81,6 +83,8 @@ const TemplateForm: React.FC<TemplateFormProps> = ({
8183
},
8284
};
8385

86+
console.log("Request data:", requestData);
87+
8488
let response;
8589
if (mode === "create") {
8690
response = await createAnnotationTemplateUsingPost(requestData);
@@ -287,42 +291,70 @@ const TemplateForm: React.FC<TemplateFormProps> = ({
287291
key={field.key}
288292
size="small"
289293
style={{ marginBottom: 12 }}
290-
title={`控件 ${fields.indexOf(field) + 1}`}
294+
title={
295+
<Space>
296+
<span>控件 {fields.indexOf(field) + 1}</span>
297+
<Form.Item noStyle shouldUpdate>
298+
{() => {
299+
const controlType = form.getFieldValue(["labels", field.name, "type"]);
300+
const fromName = form.getFieldValue(["labels", field.name, "fromName"]);
301+
if (controlType || fromName) {
302+
return (
303+
<span style={{ fontSize: 12, fontWeight: 'normal', color: '#999' }}>
304+
({fromName || '未命名'} - {controlType || '未设置类型'})
305+
</span>
306+
);
307+
}
308+
return null;
309+
}}
310+
</Form.Item>
311+
</Space>
312+
}
291313
extra={
292314
<MinusCircleOutlined
293315
style={{ color: "red" }}
294316
onClick={() => remove(field.name)}
295317
/>
296318
}
297319
>
298-
<Space direction="vertical" style={{ width: "100%" }}>
299-
<Space style={{ width: "100%" }}>
320+
<Space direction="vertical" style={{ width: "100%" }} size="middle">
321+
{/* Row 1: 控件名称, 标注目标对象, 控件类型 */}
322+
<div style={{ display: 'grid', gridTemplateColumns: '180px 220px 1fr auto', gap: 12, alignItems: 'flex-end' }}>
300323
<Form.Item
301324
{...field}
302325
label="来源名称"
303326
name={[field.name, "fromName"]}
304327
rules={[{ required: true, message: "必填" }]}
305-
style={{ marginBottom: 0, width: 150 }}
328+
style={{ marginBottom: 0 }}
329+
tooltip="此控件的唯一标识符"
306330
>
307331
<Input placeholder="例如:choice" />
308332
</Form.Item>
309333

310334
<Form.Item
311335
{...field}
312-
label="目标名称"
336+
label="标注目标对象"
313337
name={[field.name, "toName"]}
314338
rules={[{ required: true, message: "必填" }]}
315-
style={{ marginBottom: 0, width: 150 }}
339+
style={{ marginBottom: 0 }}
340+
tooltip="选择此控件将标注哪个数据对象"
341+
dependencies={['objects']}
316342
>
317-
<Input placeholder="例如:image" />
343+
<Select placeholder="选择数据对象">
344+
{(form.getFieldValue("objects") || []).map((obj: any, idx: number) => (
345+
<Option key={idx} value={obj?.name || ''}>
346+
{obj?.name || `对象 ${idx + 1}`} ({obj?.type || '未知类型'})
347+
</Option>
348+
))}
349+
</Select>
318350
</Form.Item>
319351

320352
<Form.Item
321353
{...field}
322354
label="控件类型"
323355
name={[field.name, "type"]}
324356
rules={[{ required: true, message: "必填" }]}
325-
style={{ marginBottom: 0, width: 250 }}
357+
style={{ marginBottom: 0 }}
326358
>
327359
<Select placeholder="选择控件类型">
328360
{controlTypes.map((t) => (
@@ -335,39 +367,45 @@ const TemplateForm: React.FC<TemplateFormProps> = ({
335367

336368
<Form.Item
337369
{...field}
338-
label="必填"
370+
label=" "
339371
name={[field.name, "required"]}
340372
valuePropName="checked"
341373
style={{ marginBottom: 0 }}
342374
>
343-
<Checkbox />
375+
<Checkbox>必填</Checkbox>
344376
</Form.Item>
345-
</Space>
377+
</div>
346378

379+
{/* Row 2: 取值范围定义(添加选项) - Conditionally rendered based on type */}
347380
<Form.Item
348-
{...field}
349-
label="描述"
350-
name={[field.name, "description"]}
351-
style={{ marginBottom: 0 }}
381+
noStyle
382+
shouldUpdate={(prevValues, currentValues) => {
383+
const prevType = prevValues.labels?.[field.name]?.type;
384+
const currType = currentValues.labels?.[field.name]?.type;
385+
return prevType !== currType;
386+
}}
352387
>
353-
<Input placeholder="标注人员的帮助文本" />
354-
</Form.Item>
388+
{({ getFieldValue }) => {
389+
const controlType = getFieldValue(["labels", field.name, "type"]);
390+
const fieldName = controlType === "Choices" ? "options" : "labels";
355391

356-
<Form.Item noStyle shouldUpdate>
357-
{() => {
358-
const controlType = form.getFieldValue(["labels", field.name, "type"]);
359392
if (needsOptions(controlType)) {
360393
return (
361394
<Form.Item
362395
{...field}
363396
label={controlType === "Choices" ? "选项" : "标签"}
364-
name={[field.name, controlType === "Choices" ? "options" : "labels"]}
397+
name={[field.name, fieldName]}
365398
rules={[{ required: true, message: "至少需要一个选项" }]}
366399
style={{ marginBottom: 0 }}
367400
>
368401
<Select
369402
mode="tags"
370-
placeholder="输入后按回车添加"
403+
open={false}
404+
placeholder={
405+
controlType === "Choices"
406+
? "输入选项内容,按回车添加。例如:是、否、不确定"
407+
: "输入标签名称,按回车添加。例如:人物、车辆、建筑物"
408+
}
371409
style={{ width: "100%" }}
372410
/>
373411
</Form.Item>
@@ -376,6 +414,17 @@ const TemplateForm: React.FC<TemplateFormProps> = ({
376414
return null;
377415
}}
378416
</Form.Item>
417+
418+
{/* Row 3: 描述 */}
419+
<Form.Item
420+
{...field}
421+
label="描述"
422+
name={[field.name, "description"]}
423+
style={{ marginBottom: 0 }}
424+
tooltip="向标注人员显示的帮助信息"
425+
>
426+
<Input placeholder="为标注人员提供此控件的使用说明" maxLength={200} />
427+
</Form.Item>
379428
</Space>
380429
</Card>
381430
))}

runtime/datamate-python/app/module/annotation/service/template.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,23 +91,22 @@ async def create_template(
9191
if not valid:
9292
raise HTTPException(status_code=400, detail=f"Invalid configuration: {error}")
9393

94-
# 生成Label Studio XML配置
94+
# 生成Label Studio XML配置(用于验证,但不存储)
9595
label_config = self.generate_label_studio_config(request.configuration)
9696

9797
# 验证生成的XML
9898
valid, error = LabelStudioConfigValidator.validate_xml(label_config)
9999
if not valid:
100100
raise HTTPException(status_code=400, detail=f"Generated XML is invalid: {error}")
101101

102-
# 创建模板对象
102+
# 创建模板对象(不包含label_config字段)
103103
template = AnnotationTemplate(
104104
id=str(uuid4()),
105105
name=request.name,
106106
description=request.description,
107107
data_type=request.data_type,
108108
labeling_type=request.labeling_type,
109109
configuration=config_dict,
110-
label_config=label_config,
111110
style=request.style,
112111
category=request.category,
113112
built_in=False,
@@ -250,15 +249,15 @@ async def update_template(
250249
if not valid:
251250
raise HTTPException(status_code=400, detail=f"Invalid configuration: {error}")
252251

253-
# 重新生成Label Studio XML配置
252+
# 重新生成Label Studio XML配置(用于验证)
254253
label_config = self.generate_label_studio_config(value)
255254

256255
# 验证生成的XML
257256
valid, error = LabelStudioConfigValidator.validate_xml(label_config)
258257
if not valid:
259258
raise HTTPException(status_code=400, detail=f"Generated XML is invalid: {error}")
260259

261-
setattr(template, 'label_config', label_config)
260+
# 只更新configuration字段,不存储label_config
262261
setattr(template, field, config_dict)
263262
else:
264263
setattr(template, field, value)
@@ -312,8 +311,17 @@ def _to_response(self, template: AnnotationTemplate) -> AnnotationTemplateRespon
312311
Returns:
313312
模板响应对象
314313
"""
314+
# 将配置JSON转换为TemplateConfiguration对象
315+
from typing import cast, Dict, Any
316+
config_dict = cast(Dict[str, Any], template.configuration)
317+
config = TemplateConfiguration(**config_dict)
318+
319+
# 动态生成Label Studio XML配置
320+
label_config = self.generate_label_studio_config(config)
321+
315322
# 使用model_validate从ORM对象创建响应对象
316323
response = AnnotationTemplateResponse.model_validate(template)
317-
# 将配置JSON转换为TemplateConfiguration对象
318-
response.configuration = TemplateConfiguration(**template.configuration) # type: ignore
324+
response.configuration = config
325+
response.label_config = label_config # type: ignore
326+
319327
return response

runtime/datamate-python/app/module/annotation/utils/config_validator.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -172,28 +172,36 @@ def validate_configuration_json(config: Dict) -> Tuple[bool, Optional[str]]:
172172
# 验证toName引用
173173
object_names = {obj['name'] for obj in config['objects']}
174174
for label in config['labels']:
175-
if label.get('toName') not in object_names:
176-
return False, f"Label '{label.get('fromName')}' references unknown object '{label.get('toName')}'"
175+
to_name = label.get('toName') or label.get('to_name')
176+
from_name = label.get('fromName') or label.get('from_name')
177+
if to_name not in object_names:
178+
return False, f"Label '{from_name}' references unknown object '{to_name}'"
177179

178180
return True, None
179181

180182
@staticmethod
181183
def _validate_label_definition(label: Dict) -> Tuple[bool, Optional[str]]:
182184
"""验证标签定义"""
183-
required_fields = ['fromName', 'toName', 'type']
185+
# Support both camelCase and snake_case
186+
from_name = label.get('fromName') or label.get('from_name')
187+
to_name = label.get('toName') or label.get('to_name')
188+
label_type = label.get('type')
184189

185-
for field in required_fields:
186-
if field not in label:
187-
return False, f"Missing required field '{field}'"
190+
if not from_name:
191+
return False, "Missing required field 'fromName'"
192+
if not to_name:
193+
return False, "Missing required field 'toName'"
194+
if not label_type:
195+
return False, "Missing required field 'type'"
188196

189197
# 检查类型是否支持
190-
if label['type'] not in LabelStudioConfigValidator.CONTROL_TYPES:
191-
return False, f"Unsupported control type '{label['type']}'"
198+
if label_type not in LabelStudioConfigValidator.CONTROL_TYPES:
199+
return False, f"Unsupported control type '{label_type}'"
192200

193201
# 检查标签型控件是否有选项或标签
194-
if label['type'] in LabelStudioConfigValidator.LABEL_BASED_CONTROLS:
202+
if label_type in LabelStudioConfigValidator.LABEL_BASED_CONTROLS:
195203
if 'options' not in label and 'labels' not in label:
196-
return False, f"{label['type']} must have 'options' or 'labels' field"
204+
return False, f"{label_type} must have 'options' or 'labels' field"
197205

198206
return True, None
199207

0 commit comments

Comments
 (0)