diff --git a/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx b/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx index 0f03a570..55a79724 100644 --- a/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx +++ b/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx @@ -2,16 +2,17 @@ // // SPDX-License-Identifier: MIT -import React from 'react'; +import React, { RefObject } from 'react'; import { Row, Col } from 'antd/lib/grid'; import { PercentageOutlined } from '@ant-design/icons'; import Input from 'antd/lib/input'; import Checkbox from 'antd/lib/checkbox'; import Tooltip from 'antd/lib/tooltip'; -import Form, { FormComponentProps } from '@ant-design/compatible/lib/form/Form'; +import Form, { FormInstance, RuleObject, RuleRender } from 'antd/lib/form'; import Text from 'antd/lib/typography/Text'; import patterns from 'utils/validation-patterns'; +import { Store } from 'antd/lib/form/interface'; export interface AdvancedConfiguration { bugTracker?: string; @@ -29,273 +30,266 @@ export interface AdvancedConfiguration { copyData?: boolean; } -type Props = FormComponentProps & { +const initialValues: AdvancedConfiguration = { + imageQuality: 70, + lfs: false, + useZipChunks: true, + useCache: true, + copyData: false, +}; + +interface Props { onSubmit(values: AdvancedConfiguration): void; installedGit: boolean; activeFileManagerTab: string; -}; +} -function isPositiveInteger(_: any, value: any, callback: any): void { - if (!value) { - callback(); - return; +function validateURL(_: RuleObject, value: string): Promise { + if (value && !patterns.validateURL.pattern.test(value)) { + return Promise.reject(new Error('URL is not a valid URL')); } - const intValue = +value; - if (Number.isNaN(intValue) || !Number.isInteger(intValue) || intValue < 1) { - callback('Value must be a positive integer'); - } - - callback(); + return Promise.resolve(); } -function isNonNegativeInteger(_: any, value: any, callback: any): void { - if (!value) { - callback(); - return; +function validateRepositoryPath(_: RuleObject, value: string): Promise { + if (value && !patterns.validatePath.pattern.test(value)) { + return Promise.reject(new Error('Repository path is not a valid path')); } - const intValue = +value; - if (Number.isNaN(intValue) || intValue < 0) { - callback('Value must be a non negative integer'); + return Promise.resolve(); +} + +function validateRepository(_: RuleObject, value: string): Promise<[void, void]> | Promise { + if (value) { + const [url, path] = value.split(/\s+/); + return Promise.all([validateURL(_, url), validateRepositoryPath(_, path)]); } - callback(); + return Promise.resolve(); } -function isIntegerRange(min: number, max: number, _: any, value: any, callback: any): void { - if (!value) { - callback(); - return; +const isInteger = ({ min, max }: { min?: number, max?: number }) => ( + _: RuleObject, value?: number | string, +): Promise => { + if (typeof value === 'undefined' || value === '') { + return Promise.resolve(); } const intValue = +value; - if (Number.isNaN(intValue) || !Number.isInteger(intValue) || intValue < min || intValue > max) { - callback(`Value must be an integer [${min}, ${max}]`); + if (Number.isNaN(intValue) || !Number.isInteger(intValue)) { + return Promise.reject(new Error('Value must be a positive integer')); } - callback(); -} + if (typeof min !== 'undefined' && intValue < min) { + return Promise.reject(new Error(`Value must be more than ${min}`)); + } + + if (typeof max !== 'undefined' && intValue > max) { + return Promise.reject(new Error(`Value must be less than ${max}`)); + } + + return Promise.resolve(); +}; + +const validateOverlapSize: RuleRender = ({ getFieldValue }): RuleObject => ({ + validator(_: RuleObject, value?: string | number): Promise { + if (typeof value !== 'undefined' && value !== '') { + const segmentSize = getFieldValue('segmentSize'); + if (typeof segmentSize !== 'undefined' && segmentSize !== '') { + if (+segmentSize <= +value) { + return Promise.reject(new Error('Segment size must be more than overlap size')); + } + } + } + + return Promise.resolve(); + }, +}); + +const validateStopFrame: RuleRender = ({ getFieldValue }): RuleObject => ({ + validator(_: RuleObject, value?: string | number): Promise { + if (typeof value !== 'undefined' && value !== '') { + const startFrame = getFieldValue('startFrame'); + if (typeof startFrame !== 'undefined' && startFrame !== '') { + if (+startFrame > +value) { + return Promise.reject(new Error('Start frame must not be more than stop frame')); + } + } + } + + return Promise.resolve(); + }, +}); class AdvancedConfigurationForm extends React.PureComponent { + private formRef: RefObject; + + public constructor(props: Props) { + super(props); + this.formRef = React.createRef(); + } + public submit(): Promise { - return new Promise((resolve, reject) => { - const { form, onSubmit } = this.props; - - form.validateFields((error, values): void => { - if (!error) { - const filteredValues = { ...values }; - delete filteredValues.frameStep; - - if (values.overlapSize && +values.segmentSize <= +values.overlapSize) { - reject(new Error('Segment size must be more than overlap size')); - } - - if ( - typeof values.startFrame !== 'undefined' && - typeof values.stopFrame !== 'undefined' && - +values.stopFrame < +values.startFrame - ) { - reject(new Error('Stop frame must be more or equal start frame')); - } - - onSubmit({ - ...values, - frameFilter: values.frameStep ? `step=${values.frameStep}` : undefined, - }); - resolve(); - } else { - reject(); - } + const { onSubmit } = this.props; + if (this.formRef.current) { + this.formRef.current.resetFields(); + return this.formRef.current.validateFields().then((values: Store): Promise => { + const frameFilter = values.frameStep ? `step=${values.frameStep}` : undefined; + const entries = Object.entries(values) + .filter((entry: [string, unknown]): boolean => entry[0] !== frameFilter); + + onSubmit({ + ...Object.fromEntries(entries) as any as AdvancedConfiguration, + frameFilter, + }); + return Promise.resolve(); }); - }); + } + + return Promise.reject(new Error('Form ref is empty')); } public resetFields(): void { - const { form } = this.props; - form.resetFields(); + if (this.formRef.current) { + this.formRef.current.resetFields(); + } } - renderCopyDataChechbox(): JSX.Element { - const { form } = this.props; + /* eslint-disable class-methods-use-this */ + private renderCopyDataChechbox(): JSX.Element { return ( - - - - {form.getFieldDecorator('copyData', { - initialValue: false, - valuePropName: 'checked', - })( - - Copy data into CVAT - , - )} - - - + + + Copy data into CVAT + + ); } private renderImageQuality(): JSX.Element { - const { form } = this.props; - return ( - Image quality}> - - {form.getFieldDecorator('imageQuality', { - initialValue: 70, - rules: [ - { - required: true, - message: 'The field is required.', - }, - { - validator: isIntegerRange.bind(null, 5, 100), - }, - ], - })(} />)} - - + + + } + /> + + ); } private renderOverlap(): JSX.Element { - const { form } = this.props; - return ( - Overlap size}> - - {form.getFieldDecorator('overlapSize', { - rules: [ - { - validator: isNonNegativeInteger, - }, - ], - })()} - - + + + + + ); } private renderSegmentSize(): JSX.Element { - const { form } = this.props; - return ( - Segment size}> - - {form.getFieldDecorator('segmentSize', { - rules: [ - { - validator: isPositiveInteger, - }, - ], - })()} - - + + + + + ); } private renderStartFrame(): JSX.Element { - const { form } = this.props; - return ( - Start frame}> - {form.getFieldDecorator('startFrame', { - rules: [ - { - validator: isNonNegativeInteger, - }, - ], - })()} + + ); } private renderStopFrame(): JSX.Element { - const { form } = this.props; - return ( - Stop frame}> - {form.getFieldDecorator('stopFrame', { - rules: [ - { - validator: isNonNegativeInteger, - }, - ], - })()} + + ); } private renderFrameStep(): JSX.Element { - const { form } = this.props; - return ( - Frame step}> - {form.getFieldDecorator('frameStep', { - rules: [ - { - validator: isPositiveInteger, - }, - ], - })()} + + ); } private renderGitLFSBox(): JSX.Element { - const { form } = this.props; - return ( - - {form.getFieldDecorator('lfs', { - valuePropName: 'checked', - initialValue: false, - })( - - Use LFS (Large File Support): - , - )} + + + Use LFS (Large File Support): + ); } private renderGitRepositoryURL(): JSX.Element { - const { form } = this.props; - return ( Dataset repository URL} + name='repository' + label='Dataset repository URL' extra='Attach a repository to store annotations there' + rules={[{ validator: validateRepository }]} > - {form.getFieldDecorator('repository', { - rules: [ - { - validator: (_, value, callback): void => { - if (!value) { - callback(); - } else { - const [url, path] = value.split(/\s+/); - if (!patterns.validateURL.pattern.test(url)) { - callback('Git URL is not a valid'); - } - - if (path && !patterns.validatePath.pattern.test(path)) { - callback('Git path is not a valid'); - } - - callback(); - } - }, - }, - ], - })( - , - )} + ); } @@ -304,124 +298,106 @@ class AdvancedConfigurationForm extends React.PureComponent { return ( <> - {this.renderGitRepositoryURL()} + {this.renderGitRepositoryURL()} - {this.renderGitLFSBox()} + {this.renderGitLFSBox()} ); } private renderBugTracker(): JSX.Element { - const { form } = this.props; - return ( Issue tracker} + name='bugTracker' + label='Issue tracker' extra='Attach issue tracker where the task is described' + rules={[{ validator: validateURL }]} > - {form.getFieldDecorator('bugTracker', { - rules: [ - { - validator: (_, value, callback): void => { - if (value && !patterns.validateURL.pattern.test(value)) { - callback('Issue tracker must be URL'); - } else { - callback(); - } - }, - }, - ], - })()} + ); } private renderUzeZipChunks(): JSX.Element { - const { form } = this.props; return ( - - {form.getFieldDecorator('useZipChunks', { - initialValue: true, - valuePropName: 'checked', - })( - - Use zip chunks - , - )} + + + Use zip chunks + ); } private renderCreateTaskMethod(): JSX.Element { - const { form } = this.props; return ( - - {form.getFieldDecorator('useCache', { - initialValue: true, - valuePropName: 'checked', - })( - - Use cache - , - )} + + + Use cache + ); } private renderChunkSize(): JSX.Element { - const { form } = this.props; - return ( - Chunk size}> - - Defines a number of frames to be packed in a chunk when send from client to server. Server - defines automatically if empty. -
- Recommended values: -
- 1080p or less: 36 -
- 2k or less: 8 - 16 -
- 4k or less: 4 - 8 -
- More: 1 - 4 - - )} - mouseLeaveDelay={0} + + Defines a number of frames to be packed in a chunk when send from client to server. Server + defines automatically if empty. +
+ Recommended values: +
+ 1080p or less: 36 +
+ 2k or less: 8 - 16 +
+ 4k or less: 4 - 8 +
+ More: 1 - 4 + + )} + mouseLeaveDelay={0} + > + - {form.getFieldDecorator('dataChunkSize', { - rules: [ - { - validator: isPositiveInteger, - }, - ], - })()} -
-
+ +
+ ); } public render(): JSX.Element { const { installedGit, activeFileManagerTab } = this.props; return ( -
- - {activeFileManagerTab === 'share' ? this.renderCopyDataChechbox() : null} - + + {activeFileManagerTab === 'share' ? ( + + + { this.renderCopyDataChechbox() } + + + ) : null} {this.renderUzeZipChunks()} - {this.renderCreateTaskMethod()} - {this.renderImageQuality()} @@ -449,11 +425,11 @@ class AdvancedConfigurationForm extends React.PureComponent { {installedGit ? this.renderGit() : null} - {this.renderBugTracker()} + {this.renderBugTracker()} ); } } -export default Form.create()(AdvancedConfigurationForm); +export default AdvancedConfigurationForm; diff --git a/cvat-ui/src/components/create-task-page/basic-configuration-form.tsx b/cvat-ui/src/components/create-task-page/basic-configuration-form.tsx index 1be17aa9..ac5cf7bb 100644 --- a/cvat-ui/src/components/create-task-page/basic-configuration-form.tsx +++ b/cvat-ui/src/components/create-task-page/basic-configuration-form.tsx @@ -2,60 +2,62 @@ // // SPDX-License-Identifier: MIT -import React from 'react'; +import React, { RefObject } from 'react'; import Input from 'antd/lib/input'; -import Form, { FormComponentProps } from '@ant-design/compatible/lib/form/Form'; +import Form, { FormInstance } from 'antd/lib/form'; +import { Store } from 'antd/lib/form/interface'; export interface BaseConfiguration { name: string; } -type Props = FormComponentProps & { +interface Props { onSubmit(values: BaseConfiguration): void; -}; +} + +export default class BasicConfigurationForm extends React.PureComponent { + private formRef: RefObject; + + public constructor(props: Props) { + super(props); + this.formRef = React.createRef(); + } -class BasicConfigurationForm extends React.PureComponent { public submit(): Promise { - return new Promise((resolve, reject) => { - const { form, onSubmit } = this.props; - - form.validateFields((error, values): void => { - if (!error) { - onSubmit({ - name: values.name, - }); - resolve(); - } else { - reject(); - } + const { onSubmit } = this.props; + if (this.formRef.current) { + return this.formRef.current.validateFields().then((values: Store): Promise => { + onSubmit({ name: values.name }); + return Promise.resolve(); }); - }); + } + + return Promise.reject(new Error('Form ref is empty')); } public resetFields(): void { - const { form } = this.props; - form.resetFields(); + if (this.formRef.current) { + this.formRef.current.resetFields(); + } } public render(): JSX.Element { - const { form } = this.props; - const { getFieldDecorator } = form; - return ( -
e.preventDefault()}> - Name}> - {getFieldDecorator('name', { - rules: [ - { - required: true, - message: 'Please, specify a name', - }, - ], - })()} + + Name} + rules={[ + { + required: true, + message: 'Task name cannot be empty', + }, + ]} + > + ); } } - -export default Form.create()(BasicConfigurationForm); diff --git a/cvat-ui/src/components/create-task-page/create-task-content.tsx b/cvat-ui/src/components/create-task-page/create-task-content.tsx index 7c76aba4..c5a3b6a3 100644 --- a/cvat-ui/src/components/create-task-page/create-task-content.tsx +++ b/cvat-ui/src/components/create-task-page/create-task-content.tsx @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MIT -import React from 'react'; +import React, { RefObject } from 'react'; import { RouteComponentProps } from 'react-router'; import { withRouter } from 'react-router-dom'; import { Row, Col } from 'antd/lib/grid'; @@ -58,15 +58,15 @@ const defaultState = { }; class CreateTaskContent extends React.PureComponent { - private basicConfigurationComponent: any; - - private advancedConfigurationComponent: any; - + private basicConfigurationComponent: RefObject; + private advancedConfigurationComponent: RefObject; private fileManagerContainer: any; public constructor(props: Props & RouteComponentProps) { super(props); this.state = { ...defaultState }; + this.basicConfigurationComponent = React.createRef(); + this.advancedConfigurationComponent = React.createRef(); } public componentDidMount(): void { @@ -88,9 +88,11 @@ class CreateTaskContent extends React.PureComponent { - if (this.advancedConfigurationComponent) { - return this.advancedConfigurationComponent.submit(); - } - - return new Promise((resolve): void => { - resolve(); + if (this.basicConfigurationComponent.current) { + this.basicConfigurationComponent.current.submit() + .then(() => { + if (this.advancedConfigurationComponent.current) { + return this.advancedConfigurationComponent.current.submit(); + } + + return new Promise((resolve): void => { + resolve(); + }); + }).then((): void => { + const { onCreate } = this.props; + onCreate(this.state); + }) + .catch((error: Error): void => { + notification.error({ + message: 'Could not create a task', + description: error.toString(), + }); }); - }) - .then((): void => { - const { onCreate } = this.props; - onCreate(this.state); - }) - .catch((error: Error): void => { - notification.error({ - message: 'Could not create a task', - description: error.toString(), - }); - }); + } }; private renderBasicBlock(): JSX.Element { return ( { - this.basicConfigurationComponent = component; - }} + ref={this.basicConfigurationComponent} onSubmit={this.handleSubmitBasicConfiguration} /> @@ -260,16 +260,15 @@ class CreateTaskContent extends React.PureComponent Advanced configuration}> { - this.advancedConfigurationComponent = component; - }} + activeFileManagerTab={activeFileManagerTab} + ref={this.advancedConfigurationComponent} onSubmit={this.handleSubmitAdvancedConfiguration} /> @@ -283,7 +282,7 @@ class CreateTaskContent extends React.PureComponent + Basic configuration