// Copyright (C) 2020-2021 Intel Corporation // // SPDX-License-Identifier: MIT 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'; import Button from 'antd/lib/button'; import Checkbox from 'antd/lib/checkbox'; import Select from 'antd/lib/select'; import Form, { FormInstance } from 'antd/lib/form'; import Badge from 'antd/lib/badge'; import { Store } from 'antd/lib/form/interface'; import CVATTooltip from 'components/common/cvat-tooltip'; 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'; import { equalArrayHead, idGenerator, Label, Attribute, } from './common'; export enum AttributeType { SELECT = 'SELECT', RADIO = 'RADIO', CHECKBOX = 'CHECKBOX', TEXT = 'TEXT', NUMBER = 'NUMBER', } interface Props { label: Label | null; labelNames?: string[]; onSubmit: (label: Label | null) => void; } 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 = (values: Store): void => { const { label, onSubmit } = this.props; onSubmit({ name: values.name, id: label ? label.id : idGenerator(), color: values.color, attributes: values.attributes.map((attribute: Store) => { let attrValues: string | string[] = attribute.values; if (!Array.isArray(attrValues)) { if (attribute.type === AttributeType.NUMBER) { attrValues = attrValues.split(';'); } else { attrValues = [attrValues]; } } attrValues = attrValues.map((value: string) => value.trim()); return { ...attribute, values: attrValues, input_type: attribute.type.toLowerCase(), }; }), }); if (this.formRef.current) { this.formRef.current.resetFields(); this.formRef.current.setFieldsValue({ attributes: [] }); } if (!this.continueAfterSubmit) { onSubmit(null); } }; private addAttribute = (): void => { if (this.formRef.current) { const attributes = this.formRef.current.getFieldValue('attributes'); this.formRef.current.setFieldsValue({ attributes: [...attributes, { id: idGenerator() }] }); } }; private removeAttribute = (key: number): void => { if (this.formRef.current) { const attributes = this.formRef.current.getFieldValue('attributes'); this.formRef.current.setFieldsValue({ attributes: attributes.filter((_: any, id: number) => id !== key), }); } }; /* 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 : ''; return ( ); } 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; return ( ); } 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 validator = (_: any, values: string[]): Promise => { if (locked && existedValues) { if (!equalArrayHead(existedValues, values)) { return Promise.reject(new Error('You can only append new values')); } } for (const value of values) { if (!patterns.validateAttributeValue.pattern.test(value)) { return Promise.reject(new Error(`Invalid attribute value: "${value}"`)); } } return Promise.resolve(); }; return ( False True ); } private renderNumberRangeInput(fieldInstance: any, attr: Attribute | null): JSX.Element { const { key } = fieldInstance; const locked = attr ? attr.id >= 0 : false; const value = attr ? attr.values : ''; const validator = (_: any, strNumbers: string): Promise => { const numbers = strNumbers.split(';').map((number): number => Number.parseFloat(number)); if (numbers.length !== 3) { return Promise.reject(new Error('Three numbers are expected')); } for (const number of numbers) { if (Number.isNaN(number)) { return Promise.reject(new Error(`"${number}" is not a number`)); } } const [min, max, step] = numbers; if (min >= max) { return Promise.reject(new Error('Minimum must be less than maximum')); } if (max - min < step) { return Promise.reject(new Error('Step must be less than minmax difference')); } if (step <= 0) { return Promise.reject(new Error('Step must be a positive number')); } return Promise.resolve(); }; return ( ); } private renderDefaultValueInput(fieldInstance: any, attr: Attribute | null): JSX.Element { const { key } = fieldInstance; const value = attr ? attr.values[0] : ''; return ( ); } 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; return ( Mutable ); } private renderDeleteAttributeButton(fieldInstance: any, attr: Attribute | null): JSX.Element { const { key } = fieldInstance; const locked = attr ? attr.id >= 0 : false; return ( ); } 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(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, labelNames } = this.props; const value = label ? label.name : ''; return ( { if (labelNames && labelNames.includes(labelName)) { return Promise.reject(new Error('Label name must be unique for the task')); } return Promise.resolve(); }, }, ]} > ); } private renderNewAttributeButton(): JSX.Element { return ( ); } private renderDoneButton(): JSX.Element { return ( ); } private renderContinueButton(): JSX.Element | null { const { label } = this.props; if (label) return null; return ( ); } private renderCancelButton(): JSX.Element { const { onSubmit } = this.props; return ( ); } private renderChangeColorButton(): JSX.Element { const { label } = this.props; return ( {() => ( )} ); } private renderAttributes() { return (fieldInstances: any[]): JSX.Element[] => fieldInstances.map(this.renderAttribute); } // eslint-disable-next-line react/sort-comp public componentDidMount(): void { const { label } = this.props; if (this.formRef.current) { const convertedAttributes = label ? label.attributes.map( (attribute: Attribute): Store => ({ ...attribute, values: attribute.input_type.toUpperCase() === 'NUMBER' ? attribute.values.join(';') : attribute.values, type: attribute.input_type.toUpperCase(), }), ) : []; for (const attr of convertedAttributes) { delete attr.input_type; } this.formRef.current.setFieldsValue({ attributes: convertedAttributes }); } } public render(): JSX.Element { return (
{this.renderLabelNameInput()} {this.renderChangeColorButton()} {this.renderNewAttributeButton()} {this.renderAttributes()} {this.renderDoneButton()} {this.renderContinueButton()} {this.renderCancelButton()}
); } }