You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
527 lines
18 KiB
TypeScript
527 lines
18 KiB
TypeScript
// Copyright (C) 2020 Intel Corporation
|
|
//
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
import React from 'react';
|
|
import { Row, Col } from 'antd/lib/grid';
|
|
import Icon from 'antd/lib/icon';
|
|
import Input from 'antd/lib/input';
|
|
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 'antd/lib/form/Form';
|
|
import Text from 'antd/lib/typography/Text';
|
|
|
|
import patterns from 'utils/validation-patterns';
|
|
import {
|
|
equalArrayHead,
|
|
idGenerator,
|
|
Label,
|
|
Attribute,
|
|
} from './common';
|
|
|
|
|
|
export enum AttributeType {
|
|
SELECT = 'SELECT',
|
|
RADIO = 'RADIO',
|
|
CHECKBOX = 'CHECKBOX',
|
|
TEXT = 'TEXT',
|
|
NUMBER = 'NUMBER',
|
|
}
|
|
|
|
type Props = FormComponentProps & {
|
|
label: Label | null;
|
|
onSubmit: (label: Label | null) => void;
|
|
};
|
|
|
|
class LabelForm extends React.PureComponent<Props, {}> {
|
|
private continueAfterSubmit: boolean;
|
|
|
|
constructor(props: Props) {
|
|
super(props);
|
|
this.continueAfterSubmit = false;
|
|
}
|
|
|
|
private handleSubmit = (e: React.FormEvent): void => {
|
|
e.preventDefault();
|
|
|
|
const {
|
|
form,
|
|
label,
|
|
onSubmit,
|
|
} = this.props;
|
|
|
|
form.validateFields((error, formValues): void => {
|
|
if (!error) {
|
|
onSubmit({
|
|
name: formValues.labelName,
|
|
id: label ? label.id : idGenerator(),
|
|
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 addAttribute = (): void => {
|
|
const { form } = this.props;
|
|
const keys = form.getFieldValue('keys');
|
|
const nextKeys = keys.concat(idGenerator());
|
|
form.setFieldsValue({
|
|
keys: nextKeys,
|
|
});
|
|
};
|
|
|
|
private removeAttribute = (key: number): void => {
|
|
const { form } = this.props;
|
|
const keys = form.getFieldValue('keys');
|
|
form.setFieldsValue({
|
|
keys: keys.filter((_key: number) => _key !== key),
|
|
});
|
|
};
|
|
|
|
private renderAttributeNameInput(key: number, attr: Attribute | null): JSX.Element {
|
|
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 disabled={locked} placeholder='Name' />)}
|
|
</Form.Item>
|
|
</Col>
|
|
);
|
|
}
|
|
|
|
private renderAttributeTypeInput(key: number, attr: Attribute | null): JSX.Element {
|
|
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'>
|
|
{ form.getFieldDecorator(`type[${key}]`, {
|
|
initialValue: type,
|
|
})(
|
|
<Select 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>
|
|
</Form.Item>
|
|
</Col>
|
|
);
|
|
}
|
|
|
|
private renderAttributeValuesInput(key: number, attr: Attribute | null): JSX.Element {
|
|
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) {
|
|
if (!equalArrayHead(existedValues, values)) {
|
|
callback('You can only append new values');
|
|
}
|
|
}
|
|
|
|
for (const value of values) {
|
|
if (!patterns.validateAttributeValue.pattern.test(value)) {
|
|
callback(`Invalid attribute value: "${value}"`);
|
|
}
|
|
}
|
|
|
|
callback();
|
|
};
|
|
|
|
return (
|
|
<Tooltip title='Press enter to add a new value'>
|
|
<Form.Item>
|
|
{ form.getFieldDecorator(`values[${key}]`, {
|
|
initialValue: existedValues,
|
|
rules: [{
|
|
required: true,
|
|
message: 'Please specify values',
|
|
}, {
|
|
validator,
|
|
}],
|
|
})(
|
|
<Select
|
|
mode='tags'
|
|
dropdownMenuStyle={{ display: 'none' }}
|
|
placeholder='Attribute values'
|
|
/>,
|
|
)}
|
|
</Form.Item>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
private renderBooleanValueInput(key: number, attr: Attribute | null): JSX.Element {
|
|
const value = attr ? attr.values[0] : 'false';
|
|
const { form } = this.props;
|
|
|
|
return (
|
|
<Tooltip title='Specify a default value'>
|
|
<Form.Item>
|
|
{ form.getFieldDecorator(`values[${key}]`, {
|
|
initialValue: value,
|
|
})(
|
|
<Select>
|
|
<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 {
|
|
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));
|
|
if (numbers.length !== 3) {
|
|
callback('Three numbers are expected');
|
|
}
|
|
|
|
for (const number of numbers) {
|
|
if (Number.isNaN(number)) {
|
|
callback(`"${number}" is not a number`);
|
|
}
|
|
}
|
|
|
|
const [min, max, step] = numbers;
|
|
|
|
if (min >= max) {
|
|
callback('Minimum must be less than maximum');
|
|
}
|
|
|
|
if (max - min < step) {
|
|
callback('Step must be less than minmax difference');
|
|
}
|
|
|
|
if (step <= 0) {
|
|
callback('Step must be a positive number');
|
|
}
|
|
|
|
callback();
|
|
};
|
|
|
|
return (
|
|
<Form.Item>
|
|
{ form.getFieldDecorator(`values[${key}]`, {
|
|
initialValue: value,
|
|
rules: [{
|
|
required: true,
|
|
message: 'Please set a range',
|
|
}, {
|
|
validator,
|
|
}],
|
|
})(
|
|
<Input disabled={locked} placeholder='min;max;step' />,
|
|
)}
|
|
</Form.Item>
|
|
);
|
|
}
|
|
|
|
private renderDefaultValueInput(key: number, attr: Attribute | null): JSX.Element {
|
|
const value = attr ? attr.values[0] : '';
|
|
const { form } = this.props;
|
|
|
|
return (
|
|
<Form.Item>
|
|
{ form.getFieldDecorator(`values[${key}]`, {
|
|
initialValue: value,
|
|
})(
|
|
<Input placeholder='Default value' />,
|
|
)}
|
|
</Form.Item>
|
|
);
|
|
}
|
|
|
|
private renderMutableAttributeInput(key: number, attr: Attribute | null): JSX.Element {
|
|
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?'>
|
|
{ form.getFieldDecorator(`mutable[${key}]`, {
|
|
initialValue: value,
|
|
valuePropName: 'checked',
|
|
})(
|
|
<Checkbox disabled={locked}> Mutable </Checkbox>,
|
|
)}
|
|
</Tooltip>
|
|
</Form.Item>
|
|
);
|
|
}
|
|
|
|
private renderDeleteAttributeButton(key: number, attr: Attribute | null): JSX.Element {
|
|
const locked = attr ? attr.id >= 0 : false;
|
|
|
|
return (
|
|
<Form.Item>
|
|
<Tooltip title='Delete the attribute'>
|
|
<Button
|
|
type='link'
|
|
className='cvat-delete-attribute-button'
|
|
disabled={locked}
|
|
onClick={(): void => {
|
|
this.removeAttribute(key);
|
|
}}
|
|
>
|
|
<Icon type='close-circle' />
|
|
</Button>
|
|
</Tooltip>
|
|
</Form.Item>
|
|
);
|
|
}
|
|
|
|
private renderAttribute = (key: number, index: number): JSX.Element => {
|
|
const {
|
|
label,
|
|
form,
|
|
} = this.props;
|
|
|
|
const attr = (label && index < label.attributes.length
|
|
? label.attributes[index]
|
|
: null);
|
|
|
|
return (
|
|
<Form.Item key={key}>
|
|
<Row type='flex' justify='space-between' align='middle'>
|
|
{ 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>
|
|
);
|
|
};
|
|
|
|
private renderLabelNameInput(): JSX.Element {
|
|
const {
|
|
label,
|
|
form,
|
|
} = 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,
|
|
}],
|
|
})(<Input disabled={locked} placeholder='Label name' />)}
|
|
</Form.Item>
|
|
</Col>
|
|
);
|
|
}
|
|
|
|
private renderNewAttributeButton(): JSX.Element {
|
|
return (
|
|
<Col span={3}>
|
|
<Form.Item>
|
|
<Button type='ghost' onClick={this.addAttribute}>
|
|
Add an attribute
|
|
<Icon type='plus' />
|
|
</Button>
|
|
</Form.Item>
|
|
</Col>
|
|
);
|
|
}
|
|
|
|
private renderDoneButton(): JSX.Element {
|
|
return (
|
|
<Col>
|
|
<Tooltip title='Save the label and return'>
|
|
<Button
|
|
style={{ width: '150px' }}
|
|
type='primary'
|
|
htmlType='submit'
|
|
onClick={(): void => {
|
|
this.continueAfterSubmit = false;
|
|
}}
|
|
>
|
|
Done
|
|
</Button>
|
|
</Tooltip>
|
|
</Col>
|
|
);
|
|
}
|
|
|
|
private renderContinueButton(): JSX.Element {
|
|
const { label } = this.props;
|
|
|
|
return (
|
|
label ? <div />
|
|
: (
|
|
<Col offset={1}>
|
|
<Tooltip title='Save the label and create one more'>
|
|
<Button
|
|
style={{ width: '150px' }}
|
|
type='primary'
|
|
htmlType='submit'
|
|
onClick={(): void => {
|
|
this.continueAfterSubmit = true;
|
|
}}
|
|
>
|
|
Continue
|
|
</Button>
|
|
</Tooltip>
|
|
</Col>
|
|
)
|
|
);
|
|
}
|
|
|
|
private renderCancelButton(): JSX.Element {
|
|
const { onSubmit } = this.props;
|
|
|
|
return (
|
|
<Col offset={1}>
|
|
<Tooltip title='Do not save the label and return'>
|
|
<Button
|
|
style={{ width: '150px' }}
|
|
type='danger'
|
|
onClick={(): void => {
|
|
onSubmit(null);
|
|
}}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
</Tooltip>
|
|
</Col>
|
|
);
|
|
}
|
|
|
|
public render(): JSX.Element {
|
|
const {
|
|
label,
|
|
form,
|
|
} = this.props;
|
|
|
|
form.getFieldDecorator('keys', {
|
|
initialValue: label
|
|
? label.attributes.map((attr: Attribute): number => attr.id)
|
|
: [],
|
|
});
|
|
|
|
const keys = form.getFieldValue('keys');
|
|
const attributeItems = keys.map(this.renderAttribute);
|
|
|
|
return (
|
|
<Form onSubmit={this.handleSubmit}>
|
|
<Row type='flex' justify='start' align='middle'>
|
|
{ this.renderLabelNameInput() }
|
|
<Col span={1} />
|
|
{ this.renderNewAttributeButton() }
|
|
</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>
|
|
</Form>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default Form.create<Props>()(LabelForm);
|