From a60fd76f0d379d14cac48db5e5f287a5610d648e Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Fri, 4 Dec 2020 20:00:52 +0300 Subject: [PATCH] Label form --- .../components/labels-editor/label-form.tsx | 640 +++++++++--------- cvat-ui/src/components/task-page/details.tsx | 2 +- 2 files changed, 315 insertions(+), 327 deletions(-) diff --git a/cvat-ui/src/components/labels-editor/label-form.tsx b/cvat-ui/src/components/labels-editor/label-form.tsx index c4319e12..9e75a584 100644 --- a/cvat-ui/src/components/labels-editor/label-form.tsx +++ b/cvat-ui/src/components/labels-editor/label-form.tsx @@ -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 { +export default class LabelForm extends React.Component { private continueAfterSubmit: boolean; + private formRef: RefObject; constructor(props: Props) { super(props); this.continueAfterSubmit = false; + this.formRef = React.createRef(); } - 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 ( - - - {form.getFieldDecorator(`attrName[${key}]`, { - initialValue: value, - rules: [ - { - required: true, - message: 'Please specify a name', - }, - { - pattern: patterns.validateAttributeName.pattern, - message: patterns.validateAttributeName.message, - }, - ], - })()} - - + + + ); } - 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 ( - - - - {form.getFieldDecorator(`type[${key}]`, { - initialValue: type, - })( - , - )} - + + + - + ); } - 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 { return ( - - {form.getFieldDecorator(`values[${key}]`, { - initialValue: existedValues, - rules: [ - { - required: true, - message: 'Please specify values', - }, - { - validator, - }, - ], - })( - ); } - 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 ( - - {form.getFieldDecorator(`values[${key}]`, { - initialValue: value, - })( - , - )} + + ); } - 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 { }; return ( - - {form.getFieldDecorator(`values[${key}]`, { - initialValue: value, - rules: [ - { - required: true, - message: 'Please set a range', - }, - { - validator, - }, - ], - })()} + + ); } - 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.getFieldDecorator(`values[${key}]`, { - initialValue: value, - })()} + + ); } - 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.getFieldDecorator(`mutable[${key}]`, { - initialValue: value, - valuePropName: 'checked', - })( - - {' '} - Mutable - {' '} - , - )} - - + + + Mutable + + ); } - 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 ( - - + + - - + + ); } - 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 ( - - - {this.renderAttributeNameInput(key, attr)} - {this.renderAttributeTypeInput(key, attr)} - - {((): 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; - })()} - - {this.renderMutableAttributeInput(key, attr)} - {this.renderDeleteAttributeButton(key, attr)} - + + {() => (( + + {this.renderAttributeNameInput(fieldInstance, attr)} + {this.renderAttributeTypeInput(fieldInstance, attr)} + + {((): 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; + })()} + + {this.renderMutableAttributeInput(fieldInstance, attr)} + {this.renderDeleteAttributeButton(fieldInstance, attr)} + + ))} ); }; 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 ( - - - {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'); - } - }, + { + if (labelNames && labelNames.includes(labelName)) { + callback('Label name must be unique for the task'); + } }, - ], - })()} - - + }, + ] + } + > + + ); } private renderNewAttributeButton(): JSX.Element { return ( - - - - - + + + ); } private renderDoneButton(): JSX.Element { return ( - - - - - + + + ); } - private renderContinueButton(): JSX.Element { + private renderContinueButton(): JSX.Element | null { const { label } = this.props; - return label ? ( -
- ) : ( - - - - - + if (label) return null; + return ( + + + ); } @@ -464,83 +442,93 @@ class LabelForm extends React.PureComponent { const { onSubmit } = this.props; return ( - - - - - + + + ); } private renderChangeColorButton(): JSX.Element { - const { label, form } = this.props; + const { label } = this.props; return ( - - - {form.getFieldDecorator('labelColor', { - initialValue: label && label.color ? label.color : undefined, - })( - - - - - , - )} - - + + + + + + + ); } - 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 ( -
- - {this.renderLabelNameInput()} + + + + {this.renderLabelNameInput()} + - {this.renderChangeColorButton()} + + {this.renderChangeColorButton()} + - {this.renderNewAttributeButton()} + + {this.renderNewAttributeButton()} + - {attributeItems.length > 0 && ( - - - Attributes - - - )} - {attributeItems.reverse()} - - {this.renderDoneButton()} - {this.renderContinueButton()} - {this.renderCancelButton()} + + + + { this.renderAttributes() } + + + + + + {this.renderDoneButton()} + + + {this.renderContinueButton()} + + + {this.renderCancelButton()} + ); } } - -export default Form.create()(LabelForm); diff --git a/cvat-ui/src/components/task-page/details.tsx b/cvat-ui/src/components/task-page/details.tsx index 71052766..e5ba7866 100644 --- a/cvat-ui/src/components/task-page/details.tsx +++ b/cvat-ui/src/components/task-page/details.tsx @@ -298,7 +298,7 @@ export default class DetailsComponent extends React.PureComponent return ( - + label.toJSON())} onSubmit={(labels: any[]): void => {