Label form

main
Boris Sekachev 5 years ago
parent ae14894796
commit a60fd76f0d

@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: MIT
import React from 'react';
import React, { RefObject } from 'react';
import { Row, Col } from 'antd/lib/grid';
import Icon, { CloseCircleOutlined, PlusOutlined } from '@ant-design/icons';
import Input from 'antd/lib/input';
@ -10,11 +10,11 @@ import Button from 'antd/lib/button';
import Checkbox from 'antd/lib/checkbox';
import Tooltip from 'antd/lib/tooltip';
import Select from 'antd/lib/select';
import Form, { FormComponentProps } from '@ant-design/compatible/lib/form/Form';
import Text from 'antd/lib/typography/Text';
import Form, { FormInstance } from 'antd/lib/form';
import Badge from 'antd/lib/badge';
import ColorPicker from 'components/annotation-page/standard-workspace/objects-side-bar/color-picker';
import { Store } from 'antd/lib/form/interface';
import ColorPicker from 'components/annotation-page/standard-workspace/objects-side-bar/color-picker';
import { ColorizeIcon } from 'icons';
import patterns from 'utils/validation-patterns';
import consts from 'consts';
@ -30,136 +30,123 @@ export enum AttributeType {
NUMBER = 'NUMBER',
}
type Props = FormComponentProps & {
interface Props {
label: Label | null;
labelNames?: string[];
onSubmit: (label: Label | null) => void;
};
}
class LabelForm extends React.PureComponent<Props, {}> {
export default class LabelForm extends React.Component<Props> {
private continueAfterSubmit: boolean;
private formRef: RefObject<FormInstance>;
constructor(props: Props) {
super(props);
this.continueAfterSubmit = false;
this.formRef = React.createRef<FormInstance>();
}
private handleSubmit = (e: React.FormEvent): void => {
const { form, label, onSubmit } = this.props;
e.preventDefault();
form.validateFields((error, formValues): void => {
if (!error) {
onSubmit({
name: formValues.labelName,
id: label ? label.id : idGenerator(),
color: formValues.labelColor,
attributes: formValues.keys.map(
(key: number, index: number): Attribute => {
let attrValues = formValues.values[key];
if (!Array.isArray(attrValues)) {
if (formValues.type[key] === AttributeType.NUMBER) {
attrValues = attrValues.split(';');
} else {
attrValues = [attrValues];
}
}
attrValues = attrValues.map((value: string) => value.trim());
return {
name: formValues.attrName[key],
input_type: formValues.type[key],
mutable: formValues.mutable[key],
id: label && index < label.attributes.length ? label.attributes[index].id : key,
values: attrValues,
};
},
),
});
form.resetFields();
if (!this.continueAfterSubmit) {
onSubmit(null);
private handleSubmit = (values: Store): void => {
const { label, onSubmit } = this.props;
onSubmit({
name: values.labelName,
id: label ? label.id : idGenerator(),
color: values.labelColor,
attributes: values.attributes.map((attribute: Store) => {
let attrValues: any = attribute.values;
if (attribute.input_type === AttributeType.NUMBER) {
attrValues = attrValues.split(';');
} else {
attrValues = [attrValues];
}
}
attrValues = attrValues.map((value: string) => value.trim());
return {
...attribute,
values: attrValues,
input_type: attribute.type,
};
}),
});
if (this.formRef.current) {
this.formRef.current.resetFields();
}
if (!this.continueAfterSubmit) {
onSubmit(null);
}
};
private addAttribute = (): void => {
const { form } = this.props;
const keys = form.getFieldValue('keys');
const nextKeys = keys.concat(idGenerator());
form.setFieldsValue({
keys: nextKeys,
});
if (this.formRef.current) {
const attributes = this.formRef.current.getFieldValue('attributes');
this.formRef.current.setFieldsValue({ attributes: [...attributes, { id: idGenerator() }] });
}
};
private removeAttribute = (key: number): void => {
const { form } = this.props;
const keys = form.getFieldValue('keys');
form.setFieldsValue({
keys: keys.filter((_key: number) => _key !== key),
});
if (this.formRef.current) {
const attributes = this.formRef.current.getFieldValue('attributes');
this.formRef.current.setFieldsValue({
attributes: attributes.filter((_: any, id: number) => id !== key),
});
}
};
private renderAttributeNameInput(key: number, attr: Attribute | null): JSX.Element {
/* eslint-disable class-methods-use-this */
private renderAttributeNameInput(fieldInstance: any, attr: Attribute | null): JSX.Element {
const { key } = fieldInstance;
const locked = attr ? attr.id >= 0 : false;
const value = attr ? attr.name : '';
const { form } = this.props;
return (
<Col span={5}>
<Form.Item hasFeedback>
{form.getFieldDecorator(`attrName[${key}]`, {
initialValue: value,
rules: [
{
required: true,
message: 'Please specify a name',
},
{
pattern: patterns.validateAttributeName.pattern,
message: patterns.validateAttributeName.message,
},
],
})(<Input className='cvat-attribute-name-input' disabled={locked} placeholder='Name' />)}
</Form.Item>
</Col>
<Form.Item
hasFeedback
name={[key, 'name']}
fieldKey={[fieldInstance.fieldKey, 'name']}
initialValue={value}
rules={[
{
required: true,
message: 'Please specify a name',
},
{
pattern: patterns.validateAttributeName.pattern,
message: patterns.validateAttributeName.message,
},
]}
>
<Input className='cvat-attribute-name-input' disabled={locked} placeholder='Name' />
</Form.Item>
);
}
private renderAttributeTypeInput(key: number, attr: Attribute | null): JSX.Element {
private renderAttributeTypeInput(fieldInstance: any, attr: Attribute | null): JSX.Element {
const { key } = fieldInstance;
const locked = attr ? attr.id >= 0 : false;
const type = attr ? attr.input_type.toUpperCase() : AttributeType.SELECT;
const { form } = this.props;
return (
<Col span={4}>
<Form.Item>
<Tooltip title='An HTML element representing the attribute' mouseLeaveDelay={0}>
{form.getFieldDecorator(`type[${key}]`, {
initialValue: type,
})(
<Select className='cvat-attribute-type-input' disabled={locked}>
<Select.Option value={AttributeType.SELECT}>Select</Select.Option>
<Select.Option value={AttributeType.RADIO}>Radio</Select.Option>
<Select.Option value={AttributeType.CHECKBOX}>Checkbox</Select.Option>
<Select.Option value={AttributeType.TEXT}>Text</Select.Option>
<Select.Option value={AttributeType.NUMBER}>Number</Select.Option>
</Select>,
)}
</Tooltip>
<Tooltip title='An HTML element representing the attribute' mouseLeaveDelay={0}>
<Form.Item name={[key, 'type']} fieldKey={[fieldInstance.fieldKey, 'type']} initialValue={type}>
<Select className='cvat-attribute-type-input' disabled={locked}>
<Select.Option value={AttributeType.SELECT}>Select</Select.Option>
<Select.Option value={AttributeType.RADIO}>Radio</Select.Option>
<Select.Option value={AttributeType.CHECKBOX}>Checkbox</Select.Option>
<Select.Option value={AttributeType.TEXT}>Text</Select.Option>
<Select.Option value={AttributeType.NUMBER}>Number</Select.Option>
</Select>
</Form.Item>
</Col>
</Tooltip>
);
}
private renderAttributeValuesInput(key: number, attr: Attribute | null): JSX.Element {
private renderAttributeValuesInput(fieldInstance: any, attr: Attribute | null): JSX.Element {
const { key } = fieldInstance;
const locked = attr ? attr.id >= 0 : false;
const existedValues = attr ? attr.values : [];
const { form } = this.props;
const validator = (_: any, values: string[], callback: any): void => {
if (locked && existedValues) {
@ -179,55 +166,51 @@ class LabelForm extends React.PureComponent<Props, {}> {
return (
<Tooltip title='Press enter to add a new value' mouseLeaveDelay={0}>
<Form.Item>
{form.getFieldDecorator(`values[${key}]`, {
initialValue: existedValues,
rules: [
{
required: true,
message: 'Please specify values',
},
{
validator,
},
],
})(
<Select
className='cvat-attribute-values-input'
mode='tags'
dropdownMenuStyle={{ display: 'none' }}
placeholder='Attribute values'
/>,
)}
<Form.Item
name={[key, 'values']}
fieldKey={[fieldInstance.fieldKey, 'values']}
initialValue={existedValues}
rules={[
{
required: true,
message: 'Please specify values',
},
{
validator,
},
]}
>
<Select
className='cvat-attribute-values-input'
mode='tags'
placeholder='Attribute values'
dropdownStyle={{ display: 'none' }}
/>
</Form.Item>
</Tooltip>
);
}
private renderBooleanValueInput(key: number, attr: Attribute | null): JSX.Element {
private renderBooleanValueInput(fieldInstance: any, attr: Attribute | null): JSX.Element {
const { key } = fieldInstance;
const value = attr ? attr.values[0] : 'false';
const { form } = this.props;
return (
<Tooltip title='Specify a default value' mouseLeaveDelay={0}>
<Form.Item>
{form.getFieldDecorator(`values[${key}]`, {
initialValue: value,
})(
<Select className='cvat-attribute-values-input'>
<Select.Option value='false'> False </Select.Option>
<Select.Option value='true'> True </Select.Option>
</Select>,
)}
<Form.Item name={[key, 'values']} fieldKey={[fieldInstance.fieldKey, 'values']} initialValue={value}>
<Select className='cvat-attribute-values-input'>
<Select.Option value='false'> False </Select.Option>
<Select.Option value='true'> True </Select.Option>
</Select>
</Form.Item>
</Tooltip>
);
}
private renderNumberRangeInput(key: number, attr: Attribute | null): JSX.Element {
private renderNumberRangeInput(fieldInstance: any, attr: Attribute | null): JSX.Element {
const { key } = fieldInstance;
const locked = attr ? attr.id >= 0 : false;
const value = attr ? attr.values.join(';') : '';
const { form } = this.props;
const validator = (_: any, strNumbers: string, callback: any): void => {
const numbers = strNumbers.split(';').map((number): number => Number.parseFloat(number));
@ -259,65 +242,57 @@ class LabelForm extends React.PureComponent<Props, {}> {
};
return (
<Form.Item>
{form.getFieldDecorator(`values[${key}]`, {
initialValue: value,
rules: [
{
required: true,
message: 'Please set a range',
},
{
validator,
},
],
})(<Input className='cvat-attribute-values-input' disabled={locked} placeholder='min;max;step' />)}
<Form.Item
name={[key, 'values']}
fieldKey={[fieldInstance.fieldKey, 'values']}
initialValue={value}
rules={[
{
required: true,
message: 'Please set a range',
},
{
validator,
},
]}
>
<Input className='cvat-attribute-values-input' disabled={locked} placeholder='min;max;step' />
</Form.Item>
);
}
private renderDefaultValueInput(key: number, attr: Attribute | null): JSX.Element {
private renderDefaultValueInput(fieldInstance: any, attr: Attribute | null): JSX.Element {
const { key } = fieldInstance;
const value = attr ? attr.values[0] : '';
const { form } = this.props;
return (
<Form.Item>
{form.getFieldDecorator(`values[${key}]`, {
initialValue: value,
})(<Input className='cvat-attribute-values-input' placeholder='Default value' />)}
<Form.Item name={[key, 'values']} fieldKey={[fieldInstance.fieldKey, 'values']} initialValue={value}>
<Input className='cvat-attribute-values-input' placeholder='Default value' />
</Form.Item>
);
}
private renderMutableAttributeInput(key: number, attr: Attribute | null): JSX.Element {
private renderMutableAttributeInput(fieldInstance: any, attr: Attribute | null): JSX.Element {
const { key } = fieldInstance;
const locked = attr ? attr.id >= 0 : false;
const value = attr ? attr.mutable : false;
const { form } = this.props;
return (
<Form.Item>
<Tooltip title='Can this attribute be changed frame to frame?' mouseLeaveDelay={0}>
{form.getFieldDecorator(`mutable[${key}]`, {
initialValue: value,
valuePropName: 'checked',
})(
<Checkbox className='cvat-attribute-mutable-checkbox' disabled={locked}>
{' '}
Mutable
{' '}
</Checkbox>,
)}
</Tooltip>
</Form.Item>
<Tooltip title='Can this attribute be changed frame to frame?' mouseLeaveDelay={0}>
<Form.Item name={[key, 'mutable']} fieldKey={[fieldInstance.fieldKey, 'mutable']} initialValue={value} valuePropName='checked'>
<Checkbox className='cvat-attribute-mutable-checkbox' disabled={locked}>Mutable</Checkbox>
</Form.Item>
</Tooltip>
);
}
private renderDeleteAttributeButton(key: number, attr: Attribute | null): JSX.Element {
private renderDeleteAttributeButton(fieldInstance: any, attr: Attribute | null): JSX.Element {
const { key } = fieldInstance;
const locked = attr ? attr.id >= 0 : false;
return (
<Form.Item>
<Tooltip title='Delete the attribute' mouseLeaveDelay={0}>
<Tooltip title='Delete the attribute' mouseLeaveDelay={0}>
<Form.Item>
<Button
type='link'
className='cvat-delete-attribute-button'
@ -328,135 +303,138 @@ class LabelForm extends React.PureComponent<Props, {}> {
>
<CloseCircleOutlined />
</Button>
</Tooltip>
</Form.Item>
</Form.Item>
</Tooltip>
);
}
private renderAttribute = (key: number): JSX.Element => {
const { label, form } = this.props;
const attr = label ? label.attributes.filter((_attr: any): boolean => _attr.id === key)[0] : null;
private renderAttribute = (fieldInstance: any): JSX.Element => {
const { label } = this.props;
const { key } = fieldInstance;
const fieldValue = this.formRef.current?.getFieldValue('attributes')[key];
const attr = label ? label.attributes.filter((_attr: any): boolean => _attr.id === fieldValue.id)[0] : null;
return (
<Form.Item key={key}>
<Row
type='flex'
justify='space-between'
align='middle'
cvat-attribute-id={key}
className='cvat-attribute-inputs-wrapper'
>
{this.renderAttributeNameInput(key, attr)}
{this.renderAttributeTypeInput(key, attr)}
<Col span={6}>
{((): JSX.Element => {
const type = form.getFieldValue(`type[${key}]`);
let element = null;
if ([AttributeType.SELECT, AttributeType.RADIO].includes(type)) {
element = this.renderAttributeValuesInput(key, attr);
} else if (type === AttributeType.CHECKBOX) {
element = this.renderBooleanValueInput(key, attr);
} else if (type === AttributeType.NUMBER) {
element = this.renderNumberRangeInput(key, attr);
} else {
element = this.renderDefaultValueInput(key, attr);
}
return element;
})()}
</Col>
<Col span={5}>{this.renderMutableAttributeInput(key, attr)}</Col>
<Col span={2}>{this.renderDeleteAttributeButton(key, attr)}</Col>
</Row>
<Form.Item
noStyle
key={key}
shouldUpdate
>
{() => ((
<Row
justify='space-between'
align='middle'
cvat-attribute-id={key}
className='cvat-attribute-inputs-wrapper'
>
<Col span={5}>{this.renderAttributeNameInput(fieldInstance, attr)}</Col>
<Col span={4}>{this.renderAttributeTypeInput(fieldInstance, attr)}</Col>
<Col span={6}>
{((): JSX.Element => {
const currentFieldValue = this.formRef.current?.getFieldValue('attributes')[key];
const type = currentFieldValue.type || AttributeType.SELECT;
let element = null;
if ([AttributeType.SELECT, AttributeType.RADIO].includes(type)) {
element = this.renderAttributeValuesInput(fieldInstance, attr);
} else if (type === AttributeType.CHECKBOX) {
element = this.renderBooleanValueInput(fieldInstance, attr);
} else if (type === AttributeType.NUMBER) {
element = this.renderNumberRangeInput(fieldInstance, attr);
} else {
element = this.renderDefaultValueInput(fieldInstance, attr);
}
return element;
})()}
</Col>
<Col span={5}>{this.renderMutableAttributeInput(fieldInstance, attr)}</Col>
<Col span={2}>{this.renderDeleteAttributeButton(fieldInstance, attr)}</Col>
</Row>
))}
</Form.Item>
);
};
private renderLabelNameInput(): JSX.Element {
const { label, form, labelNames } = this.props;
const { label, labelNames } = this.props;
const value = label ? label.name : '';
const locked = label ? label.id >= 0 : false;
return (
<Col span={10}>
<Form.Item hasFeedback>
{form.getFieldDecorator('labelName', {
initialValue: value,
rules: [
{
required: true,
message: 'Please specify a name',
},
{
pattern: patterns.validateAttributeName.pattern,
message: patterns.validateAttributeName.message,
},
{
validator: async (_rule: any, labelName: string, callback: Function) => {
if (labelNames && labelNames.includes(labelName)) {
callback('Label name must be unique for the task');
}
},
<Form.Item
hasFeedback
name='labelName'
initialValue={value}
rules={
[
{
required: true,
message: 'Please specify a name',
},
{
pattern: patterns.validateAttributeName.pattern,
message: patterns.validateAttributeName.message,
},
{
validator: async (_rule: any, labelName: string, callback: Function) => {
if (labelNames && labelNames.includes(labelName)) {
callback('Label name must be unique for the task');
}
},
],
})(<Input disabled={locked} placeholder='Label name' />)}
</Form.Item>
</Col>
},
]
}
>
<Input disabled={locked} placeholder='Label name' />
</Form.Item>
);
}
private renderNewAttributeButton(): JSX.Element {
return (
<Col span={6}>
<Form.Item>
<Button type='ghost' onClick={this.addAttribute} className='cvat-new-attribute-button'>
Add an attribute
<PlusOutlined />
</Button>
</Form.Item>
</Col>
<Form.Item>
<Button type='ghost' onClick={this.addAttribute} className='cvat-new-attribute-button'>
Add an attribute
<PlusOutlined />
</Button>
</Form.Item>
);
}
private renderDoneButton(): JSX.Element {
return (
<Col>
<Tooltip title='Save the label and return' mouseLeaveDelay={0}>
<Button
style={{ width: '150px' }}
type='primary'
htmlType='submit'
onClick={(): void => {
this.continueAfterSubmit = false;
}}
>
Done
</Button>
</Tooltip>
</Col>
<Tooltip title='Save the label and return' mouseLeaveDelay={0}>
<Button
style={{ width: '150px' }}
type='primary'
htmlType='submit'
onClick={(): void => {
this.continueAfterSubmit = false;
}}
>
Done
</Button>
</Tooltip>
);
}
private renderContinueButton(): JSX.Element {
private renderContinueButton(): JSX.Element | null {
const { label } = this.props;
return label ? (
<div />
) : (
<Col offset={1}>
<Tooltip title='Save the label and create one more' mouseLeaveDelay={0}>
<Button
style={{ width: '150px' }}
type='primary'
htmlType='submit'
onClick={(): void => {
this.continueAfterSubmit = true;
}}
>
Continue
</Button>
</Tooltip>
</Col>
if (label) return null;
return (
<Tooltip title='Save the label and create one more' mouseLeaveDelay={0}>
<Button
style={{ width: '150px' }}
type='primary'
htmlType='submit'
onClick={(): void => {
this.continueAfterSubmit = true;
}}
>
Continue
</Button>
</Tooltip>
);
}
@ -464,83 +442,93 @@ class LabelForm extends React.PureComponent<Props, {}> {
const { onSubmit } = this.props;
return (
<Col offset={1}>
<Tooltip title='Do not save the label and return' mouseLeaveDelay={0}>
<Button
danger
style={{ width: '150px' }}
onClick={(): void => {
onSubmit(null);
}}
>
Cancel
</Button>
</Tooltip>
</Col>
<Tooltip title='Do not save the label and return' mouseLeaveDelay={0}>
<Button
danger
style={{ width: '150px' }}
onClick={(): void => {
onSubmit(null);
}}
>
Cancel
</Button>
</Tooltip>
);
}
private renderChangeColorButton(): JSX.Element {
const { label, form } = this.props;
const { label } = this.props;
return (
<Col span={3}>
<Form.Item>
{form.getFieldDecorator('labelColor', {
initialValue: label && label.color ? label.color : undefined,
})(
<ColorPicker placement='bottom'>
<Tooltip title='Change color of the label'>
<Button type='default' className='cvat-change-task-label-color-button'>
<Badge
className='cvat-change-task-label-color-badge'
color={form.getFieldValue('labelColor') || consts.NEW_LABEL_COLOR}
text={<Icon component={ColorizeIcon} />}
/>
</Button>
</Tooltip>
</ColorPicker>,
)}
</Form.Item>
</Col>
<Form.Item name='labelColor' initialValue={label && label.color ? label.color : undefined}>
<ColorPicker placement='bottom'>
<Tooltip title='Change color of the label'>
<Button type='default' className='cvat-change-task-label-color-button'>
<Badge
className='cvat-change-task-label-color-badge'
color={this.formRef.current?.getFieldValue('labelColor') || consts.NEW_LABEL_COLOR}
text={<Icon component={ColorizeIcon} />}
/>
</Button>
</Tooltip>
</ColorPicker>
</Form.Item>
);
}
public render(): JSX.Element {
const { label, form } = this.props;
form.getFieldDecorator('keys', {
initialValue: label ? label.attributes.map((attr: Attribute): number => attr.id) : [],
});
private renderAttributes() {
return (fieldInstances: any[]): JSX.Element[] => fieldInstances.map(this.renderAttribute);
}
const keys = form.getFieldValue('keys');
const attributeItems = keys.map(this.renderAttribute);
// eslint-disable-next-line react/sort-comp
public componentDidMount(): void {
const { label } = this.props;
if (this.formRef.current) {
this.formRef.current.setFieldsValue({
attributes: label ? label.attributes
.map((attribute: Attribute): Store => ({
...attribute,
type: attribute.input_type,
})) : [],
});
}
}
public render(): JSX.Element {
return (
<Form onSubmit={this.handleSubmit}>
<Row type='flex' justify='start' align='middle'>
{this.renderLabelNameInput()}
<Form onFinish={this.handleSubmit} layout='vertical' ref={this.formRef}>
<Row justify='start' align='middle'>
<Col span={10}>
{this.renderLabelNameInput()}
</Col>
<Col span={1} />
{this.renderChangeColorButton()}
<Col span={3}>
{this.renderChangeColorButton()}
</Col>
<Col span={1} />
{this.renderNewAttributeButton()}
<Col span={6}>
{this.renderNewAttributeButton()}
</Col>
</Row>
{attributeItems.length > 0 && (
<Row type='flex' justify='start' align='middle'>
<Col>
<Text>Attributes</Text>
</Col>
</Row>
)}
{attributeItems.reverse()}
<Row type='flex' justify='start' align='middle'>
{this.renderDoneButton()}
{this.renderContinueButton()}
{this.renderCancelButton()}
<Row justify='start' align='middle'>
<Col span={24}>
<Form.List name='attributes'>
{ this.renderAttributes() }
</Form.List>
</Col>
</Row>
<Row justify='start' align='middle'>
<Col>
{this.renderDoneButton()}
</Col>
<Col offset={1}>
{this.renderContinueButton()}
</Col>
<Col offset={1}>
{this.renderCancelButton()}
</Col>
</Row>
</Form>
);
}
}
export default Form.create<Props>()(LabelForm);

@ -298,7 +298,7 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
return (
<Row>
<Col>
<Col span={24}>
<LabelsEditorComponent
labels={taskInstance.labels.map((label: any): string => label.toJSON())}
onSubmit={(labels: any[]): void => {

Loading…
Cancel
Save