label-form.tsx 20.5 KB
Newer Older
1
// Copyright (C) 2020-2021 Intel Corporation
B
Boris Sekachev 已提交
2 3 4
//
// SPDX-License-Identifier: MIT

B
Boris Sekachev 已提交
5
import React, { RefObject } from 'react';
6
import { Row, Col } from 'antd/lib/grid';
B
Boris Sekachev 已提交
7
import Icon, { CloseCircleOutlined, PlusOutlined } from '@ant-design/icons';
8 9 10 11
import Input from 'antd/lib/input';
import Button from 'antd/lib/button';
import Checkbox from 'antd/lib/checkbox';
import Select from 'antd/lib/select';
B
Boris Sekachev 已提交
12
import Form, { FormInstance } from 'antd/lib/form';
D
Dmitry Kalinin 已提交
13
import Badge from 'antd/lib/badge';
B
Boris Sekachev 已提交
14
import { Store } from 'antd/lib/form/interface';
15

16
import CVATTooltip from 'components/common/cvat-tooltip';
B
Boris Sekachev 已提交
17
import ColorPicker from 'components/annotation-page/standard-workspace/objects-side-bar/color-picker';
D
Dmitry Kalinin 已提交
18
import { ColorizeIcon } from 'icons';
19
import patterns from 'utils/validation-patterns';
D
Dmitry Kalinin 已提交
20
import consts from 'consts';
21 22 23
import {
    equalArrayHead, idGenerator, Label, Attribute,
} from './common';
24 25 26 27 28 29 30 31 32

export enum AttributeType {
    SELECT = 'SELECT',
    RADIO = 'RADIO',
    CHECKBOX = 'CHECKBOX',
    TEXT = 'TEXT',
    NUMBER = 'NUMBER',
}

B
Boris Sekachev 已提交
33
interface Props {
34
    label: Label | null;
D
Dmitry Kalinin 已提交
35
    labelNames?: string[];
36
    onSubmit: (label: Label | null) => void;
B
Boris Sekachev 已提交
37
}
38

B
Boris Sekachev 已提交
39
export default class LabelForm extends React.Component<Props> {
40
    private continueAfterSubmit: boolean;
B
Boris Sekachev 已提交
41
    private formRef: RefObject<FormInstance>;
42 43 44 45

    constructor(props: Props) {
        super(props);
        this.continueAfterSubmit = false;
B
Boris Sekachev 已提交
46
        this.formRef = React.createRef<FormInstance>();
47 48
    }

B
Boris Sekachev 已提交
49 50 51 52
    private handleSubmit = (values: Store): void => {
        const { label, onSubmit } = this.props;

        onSubmit({
B
Boris Sekachev 已提交
53
            name: values.name,
B
Boris Sekachev 已提交
54
            id: label ? label.id : idGenerator(),
B
Boris Sekachev 已提交
55
            color: values.color,
B
Boris Sekachev 已提交
56
            attributes: values.attributes.map((attribute: Store) => {
B
Boris Sekachev 已提交
57 58 59 60 61 62 63
                let attrValues: string | string[] = attribute.values;
                if (!Array.isArray(attrValues)) {
                    if (attribute.type === AttributeType.NUMBER) {
                        attrValues = attrValues.split(';');
                    } else {
                        attrValues = [attrValues];
                    }
64
                }
B
Boris Sekachev 已提交
65 66 67 68 69
                attrValues = attrValues.map((value: string) => value.trim());

                return {
                    ...attribute,
                    values: attrValues,
B
Boris Sekachev 已提交
70
                    input_type: attribute.type.toLowerCase(),
B
Boris Sekachev 已提交
71 72
                };
            }),
73
        });
B
Boris Sekachev 已提交
74 75 76

        if (this.formRef.current) {
            this.formRef.current.resetFields();
B
Boris Sekachev 已提交
77
            this.formRef.current.setFieldsValue({ attributes: [] });
B
Boris Sekachev 已提交
78 79 80 81 82
        }

        if (!this.continueAfterSubmit) {
            onSubmit(null);
        }
B
Boris Sekachev 已提交
83
    };
84

B
Boris Sekachev 已提交
85
    private addAttribute = (): void => {
B
Boris Sekachev 已提交
86 87 88 89
        if (this.formRef.current) {
            const attributes = this.formRef.current.getFieldValue('attributes');
            this.formRef.current.setFieldsValue({ attributes: [...attributes, { id: idGenerator() }] });
        }
B
Boris Sekachev 已提交
90
    };
91

B
Boris Sekachev 已提交
92
    private removeAttribute = (key: number): void => {
B
Boris Sekachev 已提交
93 94 95 96 97 98
        if (this.formRef.current) {
            const attributes = this.formRef.current.getFieldValue('attributes');
            this.formRef.current.setFieldsValue({
                attributes: attributes.filter((_: any, id: number) => id !== key),
            });
        }
B
Boris Sekachev 已提交
99
    };
100

B
Boris Sekachev 已提交
101 102 103
    /* eslint-disable class-methods-use-this */
    private renderAttributeNameInput(fieldInstance: any, attr: Attribute | null): JSX.Element {
        const { key } = fieldInstance;
104 105 106 107
        const locked = attr ? attr.id >= 0 : false;
        const value = attr ? attr.name : '';

        return (
B
Boris Sekachev 已提交
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
            <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>
126 127 128
        );
    }

B
Boris Sekachev 已提交
129 130
    private renderAttributeTypeInput(fieldInstance: any, attr: Attribute | null): JSX.Element {
        const { key } = fieldInstance;
131
        const locked = attr ? attr.id >= 0 : false;
132
        const type = attr ? attr.input_type.toUpperCase() : AttributeType.SELECT;
133 134

        return (
135
            <CVATTooltip title='An HTML element representing the attribute'>
B
Boris Sekachev 已提交
136 137
                <Form.Item name={[key, 'type']} fieldKey={[fieldInstance.fieldKey, 'type']} initialValue={type}>
                    <Select className='cvat-attribute-type-input' disabled={locked}>
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
                        <Select.Option value={AttributeType.SELECT} className='cvat-attribute-type-input-select'>
                            Select
                        </Select.Option>
                        <Select.Option value={AttributeType.RADIO} className='cvat-attribute-type-input-radio'>
                            Radio
                        </Select.Option>
                        <Select.Option value={AttributeType.CHECKBOX} className='cvat-attribute-type-input-checkbox'>
                            Checkbox
                        </Select.Option>
                        <Select.Option value={AttributeType.TEXT} className='cvat-attribute-type-input-text'>
                            Text
                        </Select.Option>
                        <Select.Option value={AttributeType.NUMBER} className='cvat-attribute-type-input-number'>
                            Number
                        </Select.Option>
B
Boris Sekachev 已提交
153
                    </Select>
154
                </Form.Item>
155
            </CVATTooltip>
156 157 158
        );
    }

B
Boris Sekachev 已提交
159 160
    private renderAttributeValuesInput(fieldInstance: any, attr: Attribute | null): JSX.Element {
        const { key } = fieldInstance;
161 162 163
        const locked = attr ? attr.id >= 0 : false;
        const existedValues = attr ? attr.values : [];

164
        const validator = (_: any, values: string[]): Promise<void> => {
165 166
            if (locked && existedValues) {
                if (!equalArrayHead(existedValues, values)) {
167
                    return Promise.reject(new Error('You can only append new values'));
168 169 170 171 172
                }
            }

            for (const value of values) {
                if (!patterns.validateAttributeValue.pattern.test(value)) {
173
                    return Promise.reject(new Error(`Invalid attribute value: "${value}"`));
174 175 176
                }
            }

177
            return Promise.resolve();
B
Boris Sekachev 已提交
178
        };
179 180

        return (
181
            <CVATTooltip title='Press enter to add a new value'>
B
Boris Sekachev 已提交
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
                <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' }}
                    />
B
Boris Sekachev 已提交
202
                </Form.Item>
203
            </CVATTooltip>
204 205 206
        );
    }

B
Boris Sekachev 已提交
207 208
    private renderBooleanValueInput(fieldInstance: any, attr: Attribute | null): JSX.Element {
        const { key } = fieldInstance;
209 210 211
        const value = attr ? attr.values[0] : 'false';

        return (
212
            <CVATTooltip title='Specify a default value'>
B
Boris Sekachev 已提交
213 214
                <Form.Item name={[key, 'values']} fieldKey={[fieldInstance.fieldKey, 'values']} initialValue={value}>
                    <Select className='cvat-attribute-values-input'>
B
Boris Sekachev 已提交
215 216
                        <Select.Option value='false'>False</Select.Option>
                        <Select.Option value='true'>True</Select.Option>
B
Boris Sekachev 已提交
217
                    </Select>
B
Boris Sekachev 已提交
218
                </Form.Item>
219
            </CVATTooltip>
220 221 222
        );
    }

B
Boris Sekachev 已提交
223 224
    private renderNumberRangeInput(fieldInstance: any, attr: Attribute | null): JSX.Element {
        const { key } = fieldInstance;
225
        const locked = attr ? attr.id >= 0 : false;
226
        const value = attr ? attr.values : '';
227

228
        const validator = (_: any, strNumbers: string): Promise<void> => {
V
Vitaliy Nishukov 已提交
229
            const numbers = strNumbers.split(';').map((number): number => Number.parseFloat(number));
230
            if (numbers.length !== 3) {
231
                return Promise.reject(new Error('Three numbers are expected'));
232 233 234 235
            }

            for (const number of numbers) {
                if (Number.isNaN(number)) {
236
                    return Promise.reject(new Error(`"${number}" is not a number`));
237 238 239
                }
            }

240 241 242
            const [min, max, step] = numbers;

            if (min >= max) {
243
                return Promise.reject(new Error('Minimum must be less than maximum'));
244 245 246
            }

            if (max - min < step) {
247
                return Promise.reject(new Error('Step must be less than minmax difference'));
248 249
            }

250
            if (step <= 0) {
251
                return Promise.reject(new Error('Step must be a positive number'));
252 253
            }

254
            return Promise.resolve();
B
Boris Sekachev 已提交
255
        };
256 257

        return (
B
Boris Sekachev 已提交
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
            <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' />
273 274 275 276
            </Form.Item>
        );
    }

B
Boris Sekachev 已提交
277 278
    private renderDefaultValueInput(fieldInstance: any, attr: Attribute | null): JSX.Element {
        const { key } = fieldInstance;
279 280 281
        const value = attr ? attr.values[0] : '';

        return (
B
Boris Sekachev 已提交
282 283
            <Form.Item name={[key, 'values']} fieldKey={[fieldInstance.fieldKey, 'values']} initialValue={value}>
                <Input className='cvat-attribute-values-input' placeholder='Default value' />
284 285 286 287
            </Form.Item>
        );
    }

B
Boris Sekachev 已提交
288 289
    private renderMutableAttributeInput(fieldInstance: any, attr: Attribute | null): JSX.Element {
        const { key } = fieldInstance;
290 291 292 293
        const locked = attr ? attr.id >= 0 : false;
        const value = attr ? attr.mutable : false;

        return (
294
            <CVATTooltip title='Can this attribute be changed frame to frame?'>
B
Boris Sekachev 已提交
295 296 297 298 299 300 301 302 303
                <Form.Item
                    name={[key, 'mutable']}
                    fieldKey={[fieldInstance.fieldKey, 'mutable']}
                    initialValue={value}
                    valuePropName='checked'
                >
                    <Checkbox className='cvat-attribute-mutable-checkbox' disabled={locked}>
                        Mutable
                    </Checkbox>
B
Boris Sekachev 已提交
304
                </Form.Item>
305
            </CVATTooltip>
306 307 308
        );
    }

B
Boris Sekachev 已提交
309 310
    private renderDeleteAttributeButton(fieldInstance: any, attr: Attribute | null): JSX.Element {
        const { key } = fieldInstance;
311 312 313
        const locked = attr ? attr.id >= 0 : false;

        return (
314
            <CVATTooltip title='Delete the attribute'>
B
Boris Sekachev 已提交
315
                <Form.Item>
316 317 318 319
                    <Button
                        type='link'
                        className='cvat-delete-attribute-button'
                        disabled={locked}
B
Boris Sekachev 已提交
320
                        onClick={(): void => {
321 322 323
                            this.removeAttribute(key);
                        }}
                    >
B
Boris Sekachev 已提交
324
                        <CloseCircleOutlined />
325
                    </Button>
B
Boris Sekachev 已提交
326
                </Form.Item>
327
            </CVATTooltip>
328 329 330
        );
    }

B
Boris Sekachev 已提交
331 332 333 334 335
    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;
336 337

        return (
B
Boris Sekachev 已提交
338 339
            <Form.Item noStyle key={key} shouldUpdate>
                {() => (
B
Boris Sekachev 已提交
340 341
                    <Row
                        justify='space-between'
342
                        align='top'
B
Boris Sekachev 已提交
343
                        cvat-attribute-id={fieldValue.id}
B
Boris Sekachev 已提交
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368
                        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>
B
Boris Sekachev 已提交
369
                )}
370 371
            </Form.Item>
        );
B
Boris Sekachev 已提交
372
    };
373

B
Boris Sekachev 已提交
374
    private renderLabelNameInput(): JSX.Element {
B
Boris Sekachev 已提交
375
        const { label, labelNames } = this.props;
B
Boris Sekachev 已提交
376
        const value = label ? label.name : '';
377 378

        return (
B
Boris Sekachev 已提交
379 380
            <Form.Item
                hasFeedback
B
Boris Sekachev 已提交
381
                name='name'
B
Boris Sekachev 已提交
382
                initialValue={value}
B
Boris Sekachev 已提交
383 384 385 386 387 388 389 390 391 392
                rules={[
                    {
                        required: true,
                        message: 'Please specify a name',
                    },
                    {
                        pattern: patterns.validateAttributeName.pattern,
                        message: patterns.validateAttributeName.message,
                    },
                    {
393
                        validator: (_rule: any, labelName: string) => {
B
Boris Sekachev 已提交
394
                            if (labelNames && labelNames.includes(labelName)) {
395
                                return Promise.reject(new Error('Label name must be unique for the task'));
B
Boris Sekachev 已提交
396
                            }
397
                            return Promise.resolve();
B
Boris Sekachev 已提交
398
                        },
B
Boris Sekachev 已提交
399 400
                    },
                ]}
B
Boris Sekachev 已提交
401
            >
D
Dmitry Kalinin 已提交
402
                <Input placeholder='Label name' />
B
Boris Sekachev 已提交
403
            </Form.Item>
404 405 406
        );
    }

B
Boris Sekachev 已提交
407
    private renderNewAttributeButton(): JSX.Element {
408
        return (
B
Boris Sekachev 已提交
409 410 411 412 413 414
            <Form.Item>
                <Button type='ghost' onClick={this.addAttribute} className='cvat-new-attribute-button'>
                    Add an attribute
                    <PlusOutlined />
                </Button>
            </Form.Item>
415 416 417
        );
    }

B
Boris Sekachev 已提交
418
    private renderDoneButton(): JSX.Element {
419
        return (
420
            <CVATTooltip title='Save the label and return'>
B
Boris Sekachev 已提交
421 422 423 424 425 426 427 428 429 430
                <Button
                    style={{ width: '150px' }}
                    type='primary'
                    htmlType='submit'
                    onClick={(): void => {
                        this.continueAfterSubmit = false;
                    }}
                >
                    Done
                </Button>
431
            </CVATTooltip>
432 433 434
        );
    }

B
Boris Sekachev 已提交
435
    private renderContinueButton(): JSX.Element | null {
B
Boris Sekachev 已提交
436 437
        const { label } = this.props;

B
Boris Sekachev 已提交
438 439
        if (label) return null;
        return (
440
            <CVATTooltip title='Save the label and create one more'>
B
Boris Sekachev 已提交
441 442 443 444 445 446 447 448 449 450
                <Button
                    style={{ width: '150px' }}
                    type='primary'
                    htmlType='submit'
                    onClick={(): void => {
                        this.continueAfterSubmit = true;
                    }}
                >
                    Continue
                </Button>
451
            </CVATTooltip>
B
Boris Sekachev 已提交
452
        );
453 454
    }

B
Boris Sekachev 已提交
455 456 457
    private renderCancelButton(): JSX.Element {
        const { onSubmit } = this.props;

458
        return (
459
            <CVATTooltip title='Do not save the label and return'>
B
Boris Sekachev 已提交
460
                <Button
B
Boris Sekachev 已提交
461
                    type='primary'
B
Boris Sekachev 已提交
462 463 464 465 466 467 468 469
                    danger
                    style={{ width: '150px' }}
                    onClick={(): void => {
                        onSubmit(null);
                    }}
                >
                    Cancel
                </Button>
470
            </CVATTooltip>
471 472 473
        );
    }

D
Dmitry Kalinin 已提交
474
    private renderChangeColorButton(): JSX.Element {
B
Boris Sekachev 已提交
475
        const { label } = this.props;
D
Dmitry Kalinin 已提交
476 477

        return (
B
Boris Sekachev 已提交
478 479 480 481
            <Form.Item noStyle shouldUpdate>
                {() => (
                    <Form.Item name='color' initialValue={label ? label?.color : undefined}>
                        <ColorPicker placement='bottom'>
482
                            <CVATTooltip title='Change color of the label'>
B
Boris Sekachev 已提交
483 484 485 486 487 488 489
                                <Button type='default' className='cvat-change-task-label-color-button'>
                                    <Badge
                                        className='cvat-change-task-label-color-badge'
                                        color={this.formRef.current?.getFieldValue('color') || consts.NEW_LABEL_COLOR}
                                        text={<Icon component={ColorizeIcon} />}
                                    />
                                </Button>
490
                            </CVATTooltip>
B
Boris Sekachev 已提交
491 492 493
                        </ColorPicker>
                    </Form.Item>
                )}
B
Boris Sekachev 已提交
494
            </Form.Item>
D
Dmitry Kalinin 已提交
495 496 497
        );
    }

B
Boris Sekachev 已提交
498 499 500
    private renderAttributes() {
        return (fieldInstances: any[]): JSX.Element[] => fieldInstances.map(this.renderAttribute);
    }
501

B
Boris Sekachev 已提交
502 503 504 505
    // eslint-disable-next-line react/sort-comp
    public componentDidMount(): void {
        const { label } = this.props;
        if (this.formRef.current) {
B
Boris Sekachev 已提交
506 507 508 509
            const convertedAttributes = label ?
                label.attributes.map(
                    (attribute: Attribute): Store => ({
                        ...attribute,
510 511 512 513
                        values:
                              attribute.input_type.toUpperCase() === 'NUMBER' ?
                                  attribute.values.join(';') :
                                  attribute.values,
B
Boris Sekachev 已提交
514 515 516 517 518 519 520 521 522 523
                        type: attribute.input_type.toUpperCase(),
                    }),
                ) :
                [];

            for (const attr of convertedAttributes) {
                delete attr.input_type;
            }

            this.formRef.current.setFieldsValue({ attributes: convertedAttributes });
B
Boris Sekachev 已提交
524 525
        }
    }
526

B
Boris Sekachev 已提交
527
    public render(): JSX.Element {
528
        return (
B
Boris Sekachev 已提交
529
            <Form onFinish={this.handleSubmit} layout='vertical' ref={this.formRef}>
530
                <Row justify='start' align='top'>
B
Boris Sekachev 已提交
531
                    <Col span={10}>{this.renderLabelNameInput()}</Col>
B
Boris Sekachev 已提交
532 533 534 535 536 537
                    <Col span={3} offset={1}>
                        {this.renderChangeColorButton()}
                    </Col>
                    <Col span={6} offset={1}>
                        {this.renderNewAttributeButton()}
                    </Col>
538
                </Row>
539
                <Row justify='start' align='top'>
B
Boris Sekachev 已提交
540
                    <Col span={24}>
B
Boris Sekachev 已提交
541
                        <Form.List name='attributes'>{this.renderAttributes()}</Form.List>
B
Boris Sekachev 已提交
542 543 544
                    </Col>
                </Row>
                <Row justify='start' align='middle'>
B
Boris Sekachev 已提交
545 546 547
                    <Col>{this.renderDoneButton()}</Col>
                    <Col offset={1}>{this.renderContinueButton()}</Col>
                    <Col offset={1}>{this.renderCancelButton()}</Col>
548 549 550 551 552
                </Row>
            </Form>
        );
    }
}