86 improve uiux on create task page (#7)

* Update label form

Remove Done button
Add closing by Esc
Add continuing by Enter

* disable autocomplete on label form

* Update submit behavior on create task

Change one submit button to two: "submit and open" and "submit and continue"

* Update submit behavior on create project

Change one submit button to two: "submit and open" and "submit and continue"

* fix eslint error

* change tests

* Add changes in changelog

* update version cvat-ui

* move of empty name handler logic when create label

* change handler errors on create project

* change text of buttons

* revert change yarn.lock

* Fixed eslint pipeline

* fix eslint error on test

* fix several tests

* fix several tests

* fix several tests

* fix several tests

Co-authored-by: Boris <sekachev.bs@gmail.com>
Co-authored-by: kirill-sizov <sizow.k.d@gmail.com>
Co-authored-by: Nikita Manovich <nikita@cvat.ai>
main
Aleksey Alekseev 4 years ago committed by GitHub
parent b7e1ebeb53
commit fbcf758674
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -26,7 +26,7 @@ jobs:
done done
if [[ ! -z $CHANGED_FILES ]]; then if [[ ! -z $CHANGED_FILES ]]; then
yarn install --frozen-lockfile yarn install --frozen-lockfile && cd tests && yarn install --frozen-lockfile && cd ..
yarn add eslint-detailed-reporter -D -W yarn add eslint-detailed-reporter -D -W
mkdir -p eslint_report mkdir -p eslint_report

@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Bumped nuclio version to 1.8.14 - Bumped nuclio version to 1.8.14
- Simplified running REST API tests. Extended CI-nightly workflow - Simplified running REST API tests. Extended CI-nightly workflow
- REST API tests are partially moved to Python SDK (`users`, `projects`, `tasks`) - REST API tests are partially moved to Python SDK (`users`, `projects`, `tasks`)
- cvat-ui: Improve UI/UX on label, create task and create project forms (<https://github.com/cvat-ai/cvat/pull/7>)
- Removed link to OpenVINO documentation (<https://github.com/cvat-ai/cvat/pull/35>) - Removed link to OpenVINO documentation (<https://github.com/cvat-ai/cvat/pull/35>)
- Clarified meaning of chunking for videos - Clarified meaning of chunking for videos

@ -1,6 +1,6 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.39.1", "version": "1.40.0",
"description": "CVAT single-page application", "description": "CVAT single-page application",
"main": "src/index.tsx", "main": "src/index.tsx",
"scripts": { "scripts": {

@ -89,7 +89,7 @@ export function getProjectTasksAsync(tasksQuery: Partial<TasksQuery> = {}): Thun
getState().projects.gettingQuery, getState().projects.gettingQuery,
tasksQuery, tasksQuery,
)); ));
const query: Partial<TasksQuery> = { const query: TasksQuery = {
...state.projects.tasksGettingQuery, ...state.projects.tasksGettingQuery,
...tasksQuery, ...tasksQuery,
}; };
@ -149,8 +149,10 @@ export function createProjectAsync(data: any): ThunkAction {
try { try {
const savedProject = await projectInstance.save(); const savedProject = await projectInstance.save();
dispatch(projectActions.createProjectSuccess(savedProject.id)); dispatch(projectActions.createProjectSuccess(savedProject.id));
return savedProject;
} catch (error) { } catch (error) {
dispatch(projectActions.createProjectFailed(error)); dispatch(projectActions.createProjectFailed(error));
throw error;
} }
}; };
} }

@ -345,7 +345,7 @@ function createTaskUpdateStatus(status: string): AnyAction {
} }
export function createTaskAsync(data: any): ThunkAction<Promise<void>, {}, {}, AnyAction> { export function createTaskAsync(data: any): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<any> => {
const description: any = { const description: any = {
name: data.basic.name, name: data.basic.name,
labels: data.labels, labels: data.labels,
@ -417,8 +417,10 @@ export function createTaskAsync(data: any): ThunkAction<Promise<void>, {}, {}, A
dispatch(createTaskUpdateStatus(status + (progress !== null ? ` ${Math.floor(progress * 100)}%` : ''))); dispatch(createTaskUpdateStatus(status + (progress !== null ? ` ${Math.floor(progress * 100)}%` : '')));
}); });
dispatch(createTaskSuccess(savedTask.id)); dispatch(createTaskSuccess(savedTask.id));
return savedTask;
} catch (error) { } catch (error) {
dispatch(createTaskFailed(error)); dispatch(createTaskFailed(error));
throw error;
} }
}; };
} }

@ -3,9 +3,9 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React, { import React, {
RefObject, useContext, useEffect, useRef, useState, RefObject, useContext, useRef, useState, useEffect,
} from 'react'; } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import Switch from 'antd/lib/switch'; import Switch from 'antd/lib/switch';
import Select from 'antd/lib/select'; import Select from 'antd/lib/select';
@ -17,14 +17,16 @@ import Input from 'antd/lib/input';
import notification from 'antd/lib/notification'; import notification from 'antd/lib/notification';
import patterns from 'utils/validation-patterns'; import patterns from 'utils/validation-patterns';
import { CombinedState } from 'reducers/interfaces';
import LabelsEditor from 'components/labels-editor/labels-editor'; import LabelsEditor from 'components/labels-editor/labels-editor';
import { createProjectAsync } from 'actions/projects-actions'; import { createProjectAsync } from 'actions/projects-actions';
import CreateProjectContext from './create-project.context'; import CreateProjectContext from './create-project.context';
const { Option } = Select; const { Option } = Select;
function NameConfigurationForm({ formRef }: { formRef: RefObject<FormInstance> }): JSX.Element { function NameConfigurationForm(
{ formRef, inputRef }:
{ formRef: RefObject<FormInstance>, inputRef: RefObject<Input> },
):JSX.Element {
return ( return (
<Form layout='vertical' ref={formRef}> <Form layout='vertical' ref={formRef}>
<Form.Item <Form.Item
@ -38,7 +40,7 @@ function NameConfigurationForm({ formRef }: { formRef: RefObject<FormInstance> }
}, },
]} ]}
> >
<Input /> <Input ref={inputRef} />
</Form.Item> </Form.Item>
</Form> </Form>
); );
@ -50,7 +52,7 @@ function AdaptiveAutoAnnotationForm({ formRef }: { formRef: RefObject<FormInstan
return ( return (
<Form layout='vertical' ref={formRef}> <Form layout='vertical' ref={formRef}>
<Form.Item name='project_class' hasFeedback label='Class'> <Form.Item name='project_class' hasFeedback label='Class'>
<Select value={projectClass.value} onChange={(v) => projectClass.set(v)}> <Select value={projectClass.value} onChange={(v) => projectClass.set?.(v)}>
<Option value=''>--Not Selected--</Option> <Option value=''>--Not Selected--</Option>
<Option value='OD'>Detection</Option> <Option value='OD'>Detection</Option>
</Select> </Select>
@ -60,7 +62,7 @@ function AdaptiveAutoAnnotationForm({ formRef }: { formRef: RefObject<FormInstan
<Switch <Switch
disabled={!projectClassesForTraining.includes(projectClass.value)} disabled={!projectClassesForTraining.includes(projectClass.value)}
checked={trainingEnabled.value} checked={trainingEnabled.value}
onClick={() => trainingEnabled.set(!trainingEnabled.value)} onClick={() => trainingEnabled.set?.(!trainingEnabled.value)}
/> />
</Form.Item> </Form.Item>
@ -125,64 +127,79 @@ function AdvancedConfigurationForm({ formRef }: { formRef: RefObject<FormInstanc
export default function CreateProjectContent(): JSX.Element { export default function CreateProjectContent(): JSX.Element {
const [projectLabels, setProjectLabels] = useState<any[]>([]); const [projectLabels, setProjectLabels] = useState<any[]>([]);
const shouldShowNotification = useRef(false);
const nameFormRef = useRef<FormInstance>(null); const nameFormRef = useRef<FormInstance>(null);
const nameInputRef = useRef<Input>(null);
const adaptiveAutoAnnotationFormRef = useRef<FormInstance>(null); const adaptiveAutoAnnotationFormRef = useRef<FormInstance>(null);
const advancedFormRef = useRef<FormInstance>(null); const advancedFormRef = useRef<FormInstance>(null);
const dispatch = useDispatch(); const dispatch = useDispatch();
const history = useHistory(); const history = useHistory();
const newProjectId = useSelector((state: CombinedState) => state.projects.activities.creates.id);
const { isTrainingActive } = useContext(CreateProjectContext); const { isTrainingActive } = useContext(CreateProjectContext);
useEffect(() => { const resetForm = (): void => {
if (Number.isInteger(newProjectId) && shouldShowNotification.current) { if (nameFormRef.current) nameFormRef.current.resetFields();
const btn = <Button onClick={() => history.push(`/projects/${newProjectId}`)}>Open project</Button>; if (advancedFormRef.current) advancedFormRef.current.resetFields();
setProjectLabels([]);
};
const focusForm = (): void => {
nameInputRef.current?.focus();
};
const sumbit = async (): Promise<any> => {
try {
let projectData: Record<string, any> = {};
if (nameFormRef.current && advancedFormRef.current) {
const basicValues = await nameFormRef.current.validateFields();
const advancedValues = await advancedFormRef.current.validateFields();
const adaptiveAutoAnnotationValues = await adaptiveAutoAnnotationFormRef.current?.validateFields();
projectData = {
...projectData,
...advancedValues,
name: basicValues.name,
};
if (adaptiveAutoAnnotationValues) {
projectData.training_project = { ...adaptiveAutoAnnotationValues };
}
}
projectData.labels = projectLabels;
const createdProject = await dispatch(createProjectAsync(projectData));
return createdProject;
} catch {
return false;
}
};
// Clear new project forms const onSubmitAndOpen = async (): Promise<void> => {
if (nameFormRef.current) nameFormRef.current.resetFields(); const createdProject = await sumbit();
if (advancedFormRef.current) advancedFormRef.current.resetFields(); if (createdProject) {
setProjectLabels([]); history.push(`/projects/${createdProject.id}`);
}
};
const onSubmitAndContinue = async (): Promise<void> => {
const res = await sumbit();
if (res) {
resetForm();
notification.info({ notification.info({
message: 'The project has been created', message: 'The project has been created',
btn,
className: 'cvat-notification-create-project-success', className: 'cvat-notification-create-project-success',
}); });
focusForm();
} }
shouldShowNotification.current = true;
}, [newProjectId]);
const onSumbit = async (): Promise<void> => {
let projectData: Record<string, any> = {};
if (nameFormRef.current && advancedFormRef.current) {
const basicValues = await nameFormRef.current.validateFields();
const advancedValues = await advancedFormRef.current.validateFields();
const adaptiveAutoAnnotationValues = await adaptiveAutoAnnotationFormRef.current?.validateFields();
projectData = {
...projectData,
...advancedValues,
name: basicValues.name,
};
if (adaptiveAutoAnnotationValues) {
projectData.training_project = { ...adaptiveAutoAnnotationValues };
}
}
projectData.labels = projectLabels;
if (!projectData.name) return;
dispatch(createProjectAsync(projectData));
}; };
useEffect(() => {
focusForm();
}, []);
return ( return (
<Row justify='start' align='middle' className='cvat-create-project-content'> <Row justify='start' align='middle' className='cvat-create-project-content'>
<Col span={24}> <Col span={24}>
<NameConfigurationForm formRef={nameFormRef} /> <NameConfigurationForm formRef={nameFormRef} inputRef={nameInputRef} />
</Col> </Col>
{isTrainingActive.value && ( {isTrainingActive.value && (
<Col span={24}> <Col span={24}>
@ -202,9 +219,18 @@ export default function CreateProjectContent(): JSX.Element {
<AdvancedConfigurationForm formRef={advancedFormRef} /> <AdvancedConfigurationForm formRef={advancedFormRef} />
</Col> </Col>
<Col span={24}> <Col span={24}>
<Button type='primary' onClick={onSumbit}> <Row justify='end' gutter={5}>
Submit <Col>
</Button> <Button type='primary' onClick={onSubmitAndOpen}>
Submit & Open
</Button>
</Col>
<Col>
<Button type='primary' onClick={onSubmitAndContinue}>
Submit & Continue
</Button>
</Col>
</Row>
</Col> </Col>
</Row> </Row>
); );

@ -17,10 +17,12 @@ interface Props {
export default class BasicConfigurationForm extends React.PureComponent<Props> { export default class BasicConfigurationForm extends React.PureComponent<Props> {
private formRef: RefObject<FormInstance>; private formRef: RefObject<FormInstance>;
private inputRef: RefObject<Input>;
public constructor(props: Props) { public constructor(props: Props) {
super(props); super(props);
this.formRef = React.createRef<FormInstance>(); this.formRef = React.createRef<FormInstance>();
this.inputRef = React.createRef<Input>();
} }
public submit(): Promise<void> { public submit(): Promise<void> {
@ -41,6 +43,12 @@ export default class BasicConfigurationForm extends React.PureComponent<Props> {
} }
} }
public focus(): void {
if (this.inputRef.current) {
this.inputRef.current.focus();
}
}
public render(): JSX.Element { public render(): JSX.Element {
return ( return (
<Form ref={this.formRef} layout='vertical'> <Form ref={this.formRef} layout='vertical'>
@ -55,7 +63,7 @@ export default class BasicConfigurationForm extends React.PureComponent<Props> {
}, },
]} ]}
> >
<Input /> <Input ref={this.inputRef} />
</Form.Item> </Form.Item>
</Form> </Form>
); );

@ -85,31 +85,21 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
if (projectId) { if (projectId) {
this.handleProjectIdChange(projectId); this.handleProjectIdChange(projectId);
} }
}
public componentDidUpdate(prevProps: Props): void {
const { status, history, taskId } = this.props;
if (status === 'CREATED' && prevProps.status !== 'CREATED') {
const btn = <Button onClick={() => history.push(`/tasks/${taskId}`)}>Open task</Button>;
notification.info({ this.focusToForm();
message: 'The task has been created', }
btn,
className: 'cvat-notification-create-task-success',
});
this.basicConfigurationComponent.current?.resetFields(); private resetState = (): void => {
this.advancedConfigurationComponent.current?.resetFields(); this.basicConfigurationComponent.current?.resetFields();
this.advancedConfigurationComponent.current?.resetFields();
this.fileManagerContainer.reset(); this.fileManagerContainer.reset();
this.setState((state) => ({ this.setState((state) => ({
...defaultState, ...defaultState,
projectId: state.projectId, projectId: state.projectId,
})); }));
} };
}
private validateLabelsOrProject = (): boolean => { private validateLabelsOrProject = (): boolean => {
const { projectId, labels } = this.state; const { projectId, labels } = this.state;
@ -170,13 +160,42 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
}); });
}; };
private handleSubmitClick = (): void => { private focusToForm = (): void => {
this.basicConfigurationComponent.current?.focus();
};
private handleSubmitAndOpen = (): void => {
const { history } = this.props;
this.handleSubmit()
.then((createdTask) => {
const { id } = createdTask;
history.push(`/tasks/${id}`);
})
.catch(() => {});
};
private handleSubmitAndContinue = (): void => {
this.handleSubmit()
.then(() => {
notification.info({
message: 'The task has been created',
className: 'cvat-notification-create-task-success',
});
})
.then(this.resetState)
.then(this.focusToForm)
.catch(() => {});
};
private handleSubmit = (): Promise<any> => new Promise((resolve, reject) => {
if (!this.validateLabelsOrProject()) { if (!this.validateLabelsOrProject()) {
notification.error({ notification.error({
message: 'Could not create a task', message: 'Could not create a task',
description: 'A task must contain at least one label or belong to some project', description: 'A task must contain at least one label or belong to some project',
className: 'cvat-notification-create-task-fail', className: 'cvat-notification-create-task-fail',
}); });
reject();
return; return;
} }
@ -186,35 +205,43 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
description: 'A task must contain at least one file', description: 'A task must contain at least one file',
className: 'cvat-notification-create-task-fail', className: 'cvat-notification-create-task-fail',
}); });
reject();
return; return;
} }
if (this.basicConfigurationComponent.current) { if (!this.basicConfigurationComponent.current) {
this.basicConfigurationComponent.current reject();
.submit() return;
.then(() => {
if (this.advancedConfigurationComponent.current) {
return this.advancedConfigurationComponent.current.submit();
}
return Promise.resolve();
})
.then((): void => {
const { onCreate } = this.props;
onCreate(this.state);
})
.catch((error: Error | ValidateErrorEntity): void => {
notification.error({
message: 'Could not create a task',
description: (error as ValidateErrorEntity).errorFields ?
(error as ValidateErrorEntity).errorFields
.map((field) => `${field.name} : ${field.errors.join(';')}`)
.map((text: string): JSX.Element => <div>{text}</div>) :
error.toString(),
className: 'cvat-notification-create-task-fail',
});
});
} }
};
this.basicConfigurationComponent.current
.submit()
.then(() => {
if (this.advancedConfigurationComponent.current) {
return this.advancedConfigurationComponent.current.submit();
}
return Promise.resolve();
})
.then((): void => {
const { onCreate } = this.props;
return onCreate(this.state);
})
.then((cratedTask) => {
resolve(cratedTask);
})
.catch((error: Error | ValidateErrorEntity): void => {
notification.error({
message: 'Could not create a task',
description: (error as ValidateErrorEntity).errorFields ?
(error as ValidateErrorEntity).errorFields
.map((field) => `${field.name} : ${field.errors.join(';')}`)
.map((text: string): JSX.Element => <div>{text}</div>) :
error.toString(),
className: 'cvat-notification-create-task-fail',
});
reject(error);
});
});
private renderBasicBlock(): JSX.Element { private renderBasicBlock(): JSX.Element {
return ( return (
@ -332,6 +359,23 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
); );
} }
private renderActions(): JSX.Element {
return (
<Row justify='end' gutter={5}>
<Col>
<Button type='primary' onClick={this.handleSubmitAndOpen}>
Submit & Open
</Button>
</Col>
<Col>
<Button type='primary' onClick={this.handleSubmitAndContinue}>
Submit & Continue
</Button>
</Col>
</Row>
);
}
public render(): JSX.Element { public render(): JSX.Element {
const { status } = this.props; const { status } = this.props;
const loading = !!status && status !== 'CREATED' && status !== 'FAILED'; const loading = !!status && status !== 'CREATED' && status !== 'FAILED';
@ -349,11 +393,8 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
{this.renderFilesBlock()} {this.renderFilesBlock()}
{this.renderAdvancedBlock()} {this.renderAdvancedBlock()}
<Col span={18}>{loading ? <Alert message={status} /> : null}</Col> <Col span={24} className='cvat-create-task-content-footer'>
<Col span={6} className='cvat-create-task-submit-section'> {loading ? <Alert message={status} /> : this.renderActions()}
<Button loading={loading} disabled={loading} type='primary' onClick={this.handleSubmitClick}>
Submit
</Button>
</Col> </Col>
</Row> </Row>
); );

@ -30,11 +30,6 @@
margin-top: 10px; margin-top: 10px;
} }
.cvat-create-task-submit-section > button {
float: right;
width: 120px;
}
.cvat-project-search-field { .cvat-project-search-field {
width: 100%; width: 100%;
} }

@ -9,7 +9,8 @@ import { Label } from './common';
interface Props { interface Props {
labelNames: string[]; labelNames: string[];
onCreate: (label: Label | null) => void; onCreate: (label: Label) => void;
onCancel: () => void;
} }
function compareProps(prevProps: Props, nextProps: Props): boolean { function compareProps(prevProps: Props, nextProps: Props): boolean {
@ -30,10 +31,10 @@ function compareProps(prevProps: Props, nextProps: Props): boolean {
} }
function ConstructorCreator(props: Props): JSX.Element { function ConstructorCreator(props: Props): JSX.Element {
const { onCreate, labelNames } = props; const { onCreate, onCancel, labelNames } = props;
return ( return (
<div className='cvat-label-constructor-creator'> <div className='cvat-label-constructor-creator'>
<LabelForm label={null} onSubmit={onCreate} labelNames={labelNames} /> <LabelForm label={null} onSubmit={onCreate} labelNames={labelNames} onCancel={onCancel} />
</div> </div>
); );
} }

@ -9,15 +9,16 @@ import { Label } from './common';
interface Props { interface Props {
label: Label; label: Label;
onUpdate: (label: Label | null) => void; onUpdate: (label: Label) => void;
onCancel: () => void;
} }
export default function ConstructorUpdater(props: Props): JSX.Element { export default function ConstructorUpdater(props: Props): JSX.Element {
const { label, onUpdate } = props; const { label, onUpdate, onCancel } = props;
return ( return (
<div className='cvat-label-constructor-updater'> <div className='cvat-label-constructor-updater'>
<LabelForm label={label} onSubmit={onUpdate} /> <LabelForm label={label} onSubmit={onUpdate} onCancel={onCancel} />
</div> </div>
); );
} }

@ -17,18 +17,20 @@ interface ConstructorViewerProps {
} }
export default function ConstructorViewer(props: ConstructorViewerProps): JSX.Element { export default function ConstructorViewer(props: ConstructorViewerProps): JSX.Element {
const { onCreate } = props; const {
onCreate, labels, onUpdate, onDelete,
} = props;
const list = [ const list = [
<Button key='create' type='ghost' onClick={onCreate} className='cvat-constructor-viewer-new-item'> <Button key='create' type='ghost' onClick={onCreate} className='cvat-constructor-viewer-new-item'>
Add label Add label
<PlusCircleOutlined /> <PlusCircleOutlined />
</Button>, </Button>,
]; ];
for (const label of props.labels) { for (const label of labels) {
list.push( list.push(
<ConstructorViewerItem <ConstructorViewerItem
onUpdate={props.onUpdate} onUpdate={onUpdate}
onDelete={props.onDelete} onDelete={onDelete}
label={label} label={label}
key={label.id} key={label.id}
color={label.color} color={label.color}

@ -33,21 +33,33 @@ export enum AttributeType {
interface Props { interface Props {
label: Label | null; label: Label | null;
labelNames?: string[]; labelNames?: string[];
onSubmit: (label: Label | null) => void; onSubmit: (label: Label) => void;
onCancel: () => void;
} }
export default class LabelForm extends React.Component<Props> { export default class LabelForm extends React.Component<Props> {
private continueAfterSubmit: boolean;
private formRef: RefObject<FormInstance>; private formRef: RefObject<FormInstance>;
private inputNameRef: RefObject<Input>;
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.continueAfterSubmit = false;
this.formRef = React.createRef<FormInstance>(); this.formRef = React.createRef<FormInstance>();
this.inputNameRef = React.createRef<Input>();
} }
private focus = (): void => {
this.inputNameRef.current?.focus({
cursor: 'end',
});
};
private handleSubmit = (values: Store): void => { private handleSubmit = (values: Store): void => {
const { label, onSubmit } = this.props; const { label, onSubmit, onCancel } = this.props;
if (!values.name) {
onCancel();
return;
}
onSubmit({ onSubmit({
name: values.name, name: values.name,
@ -76,10 +88,10 @@ export default class LabelForm extends React.Component<Props> {
// resetFields does not remove existed attributes // resetFields does not remove existed attributes
this.formRef.current.setFieldsValue({ attributes: undefined }); this.formRef.current.setFieldsValue({ attributes: undefined });
this.formRef.current.resetFields(); this.formRef.current.resetFields();
}
if (!this.continueAfterSubmit) { if (!label) {
onSubmit(null); this.focus();
}
} }
}; };
@ -380,7 +392,7 @@ export default class LabelForm extends React.Component<Props> {
}; };
private renderLabelNameInput(): JSX.Element { private renderLabelNameInput(): JSX.Element {
const { label, labelNames } = this.props; const { label, labelNames, onCancel } = this.props;
const value = label ? label.name : ''; const value = label ? label.name : '';
return ( return (
@ -390,7 +402,7 @@ export default class LabelForm extends React.Component<Props> {
initialValue={value} initialValue={value}
rules={[ rules={[
{ {
required: true, required: !!label,
message: 'Please specify a name', message: 'Please specify a name',
}, },
{ {
@ -407,7 +419,16 @@ export default class LabelForm extends React.Component<Props> {
}, },
]} ]}
> >
<Input placeholder='Label name' /> <Input
ref={this.inputNameRef}
placeholder='Label name'
onKeyUp={(event): void => {
if (event.key === 'Escape' || event.key === 'Esc' || event.keyCode === 27) {
onCancel();
}
}}
autoComplete='off'
/>
</Form.Item> </Form.Item>
); );
} }
@ -423,45 +444,26 @@ export default class LabelForm extends React.Component<Props> {
); );
} }
private renderDoneButton(): JSX.Element { private renderSaveButton(): JSX.Element {
return (
<CVATTooltip title='Save the label and return'>
<Button
style={{ width: '150px' }}
type='primary'
htmlType='submit'
onClick={(): void => {
this.continueAfterSubmit = false;
}}
>
Done
</Button>
</CVATTooltip>
);
}
private renderContinueButton(): JSX.Element | null {
const { label } = this.props; const { label } = this.props;
const tooltipTitle = label ? 'Save the label and return' : 'Save the label and create one more';
const buttonText = label ? 'Done' : 'Continue';
if (label) return null;
return ( return (
<CVATTooltip title='Save the label and create one more'> <CVATTooltip title={tooltipTitle}>
<Button <Button
style={{ width: '150px' }} style={{ width: '150px' }}
type='primary' type='primary'
htmlType='submit' htmlType='submit'
onClick={(): void => {
this.continueAfterSubmit = true;
}}
> >
Continue {buttonText}
</Button> </Button>
</CVATTooltip> </CVATTooltip>
); );
} }
private renderCancelButton(): JSX.Element { private renderCancelButton(): JSX.Element {
const { onSubmit } = this.props; const { onCancel } = this.props;
return ( return (
<CVATTooltip title='Do not save the label and return'> <CVATTooltip title='Do not save the label and return'>
@ -470,7 +472,7 @@ export default class LabelForm extends React.Component<Props> {
danger danger
style={{ width: '150px' }} style={{ width: '150px' }}
onClick={(): void => { onClick={(): void => {
onSubmit(null); onCancel();
}} }}
> >
Cancel Cancel
@ -526,6 +528,8 @@ export default class LabelForm extends React.Component<Props> {
this.formRef.current.setFieldsValue({ attributes: convertedAttributes }); this.formRef.current.setFieldsValue({ attributes: convertedAttributes });
} }
this.focus();
} }
public render(): JSX.Element { public render(): JSX.Element {
@ -546,8 +550,7 @@ export default class LabelForm extends React.Component<Props> {
</Col> </Col>
</Row> </Row>
<Row justify='start' align='middle'> <Row justify='start' align='middle'>
<Col>{this.renderDoneButton()}</Col> <Col>{this.renderSaveButton()}</Col>
<Col offset={1}>{this.renderContinueButton()}</Col>
<Col offset={1}>{this.renderCancelButton()}</Col> <Col offset={1}>{this.renderCancelButton()}</Col>
</Row> </Row>
</Form> </Form>

@ -98,48 +98,45 @@ export default class LabelsEditor extends React.PureComponent<LabelsEditorProps,
this.handleSubmit(savedLabels, unsavedLabels); this.handleSubmit(savedLabels, unsavedLabels);
}; };
private handleCreate = (label: Label | null): void => { private handleCreate = (label: Label): void => {
if (label === null) { const { unsavedLabels, savedLabels } = this.state;
this.setState({ constructorMode: ConstructorMode.SHOW }); const newUnsavedLabels = [
} else { ...unsavedLabels,
const { unsavedLabels, savedLabels } = this.state; {
const newUnsavedLabels = [ ...label,
...unsavedLabels, id: idGenerator(),
{ },
...label, ];
id: idGenerator(),
},
];
this.setState({ unsavedLabels: newUnsavedLabels }); this.setState({ unsavedLabels: newUnsavedLabels });
this.handleSubmit(savedLabels, newUnsavedLabels); this.handleSubmit(savedLabels, newUnsavedLabels);
}
}; };
private handleUpdate = (label: Label | null): void => { private handleUpdate = (label: Label): void => {
const { savedLabels, unsavedLabels } = this.state; const { savedLabels, unsavedLabels } = this.state;
if (label) { const filteredSavedLabels = savedLabels.filter((_label: Label) => _label.id !== label.id);
const filteredSavedLabels = savedLabels.filter((_label: Label) => _label.id !== label.id); const filteredUnsavedLabels = unsavedLabels.filter((_label: Label) => _label.id !== label.id);
const filteredUnsavedLabels = unsavedLabels.filter((_label: Label) => _label.id !== label.id); if (label.id >= 0) {
if (label.id >= 0) { filteredSavedLabels.push(label);
filteredSavedLabels.push(label); this.setState({
this.setState({ savedLabels: filteredSavedLabels,
savedLabels: filteredSavedLabels, constructorMode: ConstructorMode.SHOW,
constructorMode: ConstructorMode.SHOW, });
});
} else {
filteredUnsavedLabels.push(label);
this.setState({
unsavedLabels: filteredUnsavedLabels,
constructorMode: ConstructorMode.SHOW,
});
}
this.handleSubmit(filteredSavedLabels, filteredUnsavedLabels);
} else { } else {
this.setState({ constructorMode: ConstructorMode.SHOW }); filteredUnsavedLabels.push(label);
this.setState({
unsavedLabels: filteredUnsavedLabels,
constructorMode: ConstructorMode.SHOW,
});
} }
this.handleSubmit(filteredSavedLabels, filteredUnsavedLabels);
this.setState({ constructorMode: ConstructorMode.SHOW });
};
private handlerCancel = (): void => {
this.setState({ constructorMode: ConstructorMode.SHOW });
}; };
private handleDelete = (label: Label): void => { private handleDelete = (label: Label): void => {
@ -198,6 +195,7 @@ export default class LabelsEditor extends React.PureComponent<LabelsEditorProps,
const { const {
savedLabels, unsavedLabels, constructorMode, labelForUpdate, savedLabels, unsavedLabels, constructorMode, labelForUpdate,
} = this.state; } = this.state;
const savedAndUnsavedLabels = [...savedLabels, ...unsavedLabels];
return ( return (
<Tabs <Tabs
@ -214,7 +212,7 @@ export default class LabelsEditor extends React.PureComponent<LabelsEditorProps,
)} )}
key='1' key='1'
> >
<RawViewer labels={[...savedLabels, ...unsavedLabels]} onSubmit={this.handleRawSubmit} /> <RawViewer labels={savedAndUnsavedLabels} onSubmit={this.handleRawSubmit} />
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane <Tabs.TabPane
@ -228,7 +226,7 @@ export default class LabelsEditor extends React.PureComponent<LabelsEditorProps,
> >
{constructorMode === ConstructorMode.SHOW && ( {constructorMode === ConstructorMode.SHOW && (
<ConstructorViewer <ConstructorViewer
labels={[...savedLabels, ...unsavedLabels]} labels={savedAndUnsavedLabels}
onUpdate={(label: Label): void => { onUpdate={(label: Label): void => {
this.setState({ this.setState({
constructorMode: ConstructorMode.UPDATE, constructorMode: ConstructorMode.UPDATE,
@ -244,10 +242,18 @@ export default class LabelsEditor extends React.PureComponent<LabelsEditorProps,
/> />
)} )}
{constructorMode === ConstructorMode.UPDATE && labelForUpdate !== null && ( {constructorMode === ConstructorMode.UPDATE && labelForUpdate !== null && (
<ConstructorUpdater label={labelForUpdate} onUpdate={this.handleUpdate} /> <ConstructorUpdater
label={labelForUpdate}
onUpdate={this.handleUpdate}
onCancel={this.handlerCancel}
/>
)} )}
{constructorMode === ConstructorMode.CREATE && ( {constructorMode === ConstructorMode.CREATE && (
<ConstructorCreator labelNames={labels.map((l) => l.name)} onCreate={this.handleCreate} /> <ConstructorCreator
labelNames={labels.map((l) => l.name)}
onCreate={this.handleCreate}
onCancel={this.handlerCancel}
/>
)} )}
</Tabs.TabPane> </Tabs.TabPane>
</Tabs> </Tabs>

@ -23,7 +23,7 @@ interface DispatchToProps {
function mapDispatchToProps(dispatch: any): DispatchToProps { function mapDispatchToProps(dispatch: any): DispatchToProps {
return { return {
onCreate: (data: CreateTaskData): void => dispatch(createTaskAsync(data)), onCreate: (data: CreateTaskData): Promise<any> => dispatch(createTaskAsync(data)),
}; };
} }

@ -71,7 +71,7 @@ context('Creating a project by inserting labels from a task.', { browser: '!fire
cy.contains('button', 'Done').click(); cy.contains('button', 'Done').click();
cy.contains('[role="tab"]', 'Constructor').click(); cy.contains('[role="tab"]', 'Constructor').click();
cy.contains('.cvat-constructor-viewer-item', task.label).should('exist'); cy.contains('.cvat-constructor-viewer-item', task.label).should('exist');
cy.contains('button', 'Submit').click(); cy.contains('button', 'Submit & Continue').click();
cy.get('.cvat-notification-create-project-success').should('exist').find('[data-icon="close"]').click(); cy.get('.cvat-notification-create-project-success').should('exist').find('[data-icon="close"]').click();
cy.goToProjectsList(); cy.goToProjectsList();
cy.openProject(projectName); cy.openProject(projectName);

@ -31,7 +31,7 @@ context('Create more than one task per time when create from project.', () => {
}); });
cy.get('.cvat-constructor-viewer-new-item').should('not.exist'); cy.get('.cvat-constructor-viewer-new-item').should('not.exist');
cy.get('input[type="file"]').attachFile(archiveName, { subjectType: 'drag-n-drop' }); cy.get('input[type="file"]').attachFile(archiveName, { subjectType: 'drag-n-drop' });
cy.contains('button', 'Submit').click(); cy.contains('button', 'Submit & Continue').click();
cy.get('.cvat-notification-create-task-success').should('exist'); cy.get('.cvat-notification-create-task-success').should('exist');
cy.get('.cvat-notification-create-task-fail').should('not.exist'); cy.get('.cvat-notification-create-task-fail').should('not.exist');
} }

@ -44,7 +44,7 @@ context('Create a task with set an issue tracker.', () => {
cy.contains('Advanced configuration').click(); cy.contains('Advanced configuration').click();
cy.get('#bugTracker').type(incorrectBugTrackerUrl); cy.get('#bugTracker').type(incorrectBugTrackerUrl);
cy.contains('URL is not a valid URL').should('exist'); cy.contains('URL is not a valid URL').should('exist');
cy.get('.cvat-create-task-submit-section').click(); cy.contains('button', 'Submit & Continue').click();
cy.get('.cvat-notification-create-task-fail').should('exist').and('be.visible'); cy.get('.cvat-notification-create-task-fail').should('exist').and('be.visible');
cy.closeNotification('.cvat-notification-create-task-fail'); cy.closeNotification('.cvat-notification-create-task-fail');
}); });
@ -52,7 +52,7 @@ context('Create a task with set an issue tracker.', () => {
it('Set correct issue tracker URL. The task created.', () => { it('Set correct issue tracker URL. The task created.', () => {
cy.get('#bugTracker').clear().type(dummyBugTrackerUrl); cy.get('#bugTracker').clear().type(dummyBugTrackerUrl);
cy.contains('URL is not a valid URL').should('not.exist'); cy.contains('URL is not a valid URL').should('not.exist');
cy.get('.cvat-create-task-submit-section').click(); cy.contains('button', 'Submit & Continue').click();
cy.get('.cvat-notification-create-task-fail').should('not.exist'); cy.get('.cvat-notification-create-task-fail').should('not.exist');
cy.get('.cvat-notification-create-task-success').should('exist').and('be.visible'); cy.get('.cvat-notification-create-task-success').should('exist').and('be.visible');
}); });

@ -35,8 +35,7 @@ context('Button "Continue" in label editor.', () => {
cy.get('.cvat-constructor-viewer-new-item').click(); cy.get('.cvat-constructor-viewer-new-item').click();
cy.get('.cvat-label-constructor-creator').within(() => { cy.get('.cvat-label-constructor-creator').within(() => {
cy.contains('button', 'Continue').click(); cy.contains('button', 'Continue').click();
cy.contains('[role="alert"]', 'Please specify a name').should('be.visible'); cy.get('.cvat-label-constructor-creator').should('not.exist');
cy.contains('button', 'Cancel').click();
}); });
}); });
}); });

@ -36,28 +36,28 @@ context('Try to create a task without necessary arguments.', () => {
describe(`Testing "${labelName}"`, () => { describe(`Testing "${labelName}"`, () => {
it('Try to create a task without any fields. A task is not created.', () => { it('Try to create a task without any fields. A task is not created.', () => {
cy.get('.cvat-create-task-submit-section').click(); cy.contains('button', 'Submit & Continue').click();
cy.get('.cvat-notification-create-task-fail').should('exist'); cy.get('.cvat-notification-create-task-fail').should('exist');
cy.closeNotification('.cvat-notification-create-task-fail'); cy.closeNotification('.cvat-notification-create-task-fail');
}); });
it('Input a task name. A task is not created.', () => { it('Input a task name. A task is not created.', () => {
cy.get('[id="name"]').type(taskName); cy.get('[id="name"]').type(taskName);
cy.get('.cvat-create-task-submit-section').click(); cy.contains('button', 'Submit & Continue').click();
cy.get('.cvat-notification-create-task-fail').should('exist'); cy.get('.cvat-notification-create-task-fail').should('exist');
cy.closeNotification('.cvat-notification-create-task-fail'); cy.closeNotification('.cvat-notification-create-task-fail');
}); });
it('Input task labels. A task is not created.', () => { it('Input task labels. A task is not created.', () => {
cy.addNewLabel(labelName); cy.addNewLabel(labelName);
cy.get('.cvat-create-task-submit-section').click(); cy.contains('button', 'Submit & Continue').click();
cy.get('.cvat-notification-create-task-fail').should('exist'); cy.get('.cvat-notification-create-task-fail').should('exist');
cy.closeNotification('.cvat-notification-create-task-fail'); cy.closeNotification('.cvat-notification-create-task-fail');
}); });
it('Add some files. A task created.', () => { it('Add some files. A task created.', () => {
cy.get('input[type="file"]').attachFile(archiveName, { subjectType: 'drag-n-drop' }); cy.get('input[type="file"]').attachFile(archiveName, { subjectType: 'drag-n-drop' });
cy.get('.cvat-create-task-submit-section').click(); cy.contains('button', 'Submit & Continue').click();
cy.get('.cvat-notification-create-task-fail').should('not.exist'); cy.get('.cvat-notification-create-task-fail').should('not.exist');
cy.get('.cvat-notification-create-task-success').should('exist'); cy.get('.cvat-notification-create-task-success').should('exist');
// Check that the interface is prepared for creating the next task. // Check that the interface is prepared for creating the next task.

@ -45,7 +45,8 @@ context('Add/delete labels and attributes.', () => {
cy.get('.cvat-attribute-type-input').click(); cy.get('.cvat-attribute-type-input').click();
cy.get('.cvat-attribute-type-input-text').click(); cy.get('.cvat-attribute-type-input-text').click();
cy.get('.cvat-attribute-values-input').type(textDefaultValue); cy.get('.cvat-attribute-values-input').type(textDefaultValue);
cy.contains('[type="submit"]', 'Done').click(); cy.contains('[type="submit"]', 'Continue').click();
cy.contains('[type="button"]', 'Cancel').click();
cy.get('.cvat-constructor-viewer-item').should('exist'); cy.get('.cvat-constructor-viewer-item').should('exist');
}); });

@ -17,15 +17,18 @@ context('Changing a label name via label constructor.', () => {
}); });
describe(`Testing case "${caseId}"`, () => { describe(`Testing case "${caseId}"`, () => {
it('Set empty label name. Press "Done" button. Alert exist.', () => { it('Set empty label name. Press "Continue" button. Label name is not created. Label constructor is closed.', () => {
cy.get('.cvat-constructor-viewer-new-item').click(); // Open label constructor cy.get('.cvat-constructor-viewer-new-item').click(); // Open label constructor
cy.contains('[type="submit"]', 'Done').click(); cy.contains('[type="submit"]', 'Continue').click();
cy.contains('[role="alert"]', 'Please specify a name').should('exist').and('be.visible'); cy.get('.cvat-label-constructor-creator').should('not.exist');
cy.get('.cvat-constructor-viewer').should('be.visible');
}); });
it('Change label name to any other correct value. Press "Done" button. The label created.', () => { it('Change label name to any other correct value. Press "Continue" button. The label created.', () => {
cy.get('.cvat-constructor-viewer-new-item').click(); // Open label constructor
cy.get('[placeholder="Label name"]').type(firstLabelName); cy.get('[placeholder="Label name"]').type(firstLabelName);
cy.contains('[type="submit"]', 'Done').click({ force: true }); cy.contains('[type="submit"]', 'Continue').click({ force: true });
cy.contains('[type="button"]', 'Cancel').click(); // Close label constructor
cy.get('.cvat-constructor-viewer-item').should('exist').and('have.text', firstLabelName); cy.get('.cvat-constructor-viewer-item').should('exist').and('have.text', firstLabelName);
}); });

@ -45,7 +45,7 @@ context('Try to create a task with an incorrect dataset repository.', () => {
it('Set dummy dataset repository.', () => { it('Set dummy dataset repository.', () => {
cy.get('#repository').type(incorrectDatasetRepoUrlHttps); cy.get('#repository').type(incorrectDatasetRepoUrlHttps);
cy.get('.cvat-create-task-submit-section').click(); cy.contains('button', 'Submit & Continue').click();
cy.get('.cvat-notification-notice-create-task-failed').should('exist'); cy.get('.cvat-notification-notice-create-task-failed').should('exist');
cy.closeNotification('.cvat-notification-notice-create-task-failed'); cy.closeNotification('.cvat-notification-notice-create-task-failed');
cy.get('#repository').clear(); cy.get('#repository').clear();
@ -53,7 +53,7 @@ context('Try to create a task with an incorrect dataset repository.', () => {
it('Set repository with missing access.', () => { it('Set repository with missing access.', () => {
cy.get('#repository').type(repositoryWithMissingAccess); cy.get('#repository').type(repositoryWithMissingAccess);
cy.get('.cvat-create-task-submit-section').click(); cy.contains('button', 'Submit & Continue').click();
cy.get('.cvat-notification-notice-create-task-failed').should('exist'); cy.get('.cvat-notification-notice-create-task-failed').should('exist');
cy.get('.cvat-create-task-clone-repository-fail').should('exist'); cy.get('.cvat-create-task-clone-repository-fail').should('exist');
}); });

@ -34,9 +34,7 @@ context('Connected file share.', () => {
}); });
}); });
}); });
cy.contains('button', 'Submit').click(); cy.contains('button', 'Submit & Open').click();
cy.get('.cvat-notification-create-task-success').should('exist').find('button').click();
cy.get('.cvat-notification-create-task-success').should('exist').find('[data-icon="close"]').click();
cy.get('.cvat-task-details').should('exist'); cy.get('.cvat-task-details').should('exist');
} }

@ -30,14 +30,14 @@ context('Create a task with files from remote sources.', () => {
cy.addNewLabel(labelName); cy.addNewLabel(labelName);
cy.contains('Remote sources').click(); cy.contains('Remote sources').click();
cy.get('.cvat-file-selector-remote').type(wrongUrl); cy.get('.cvat-file-selector-remote').type(wrongUrl);
cy.get('.cvat-create-task-submit-section').click(); cy.contains('button', 'Submit & Continue').click();
cy.get('.cvat-notification-notice-create-task-failed').should('exist'); cy.get('.cvat-notification-notice-create-task-failed').should('exist');
cy.closeNotification('.cvat-notification-notice-create-task-failed'); cy.closeNotification('.cvat-notification-notice-create-task-failed');
}); });
it('Set correct URL to remote file. The task is created.', () => { it('Set correct URL to remote file. The task is created.', () => {
cy.get('.cvat-file-selector-remote').clear().type(correctUrl); cy.get('.cvat-file-selector-remote').clear().type(correctUrl);
cy.get('.cvat-create-task-submit-section').click(); cy.contains('button', 'Submit & Continue').click();
cy.get('.cvat-notification-create-task-success').should('exist'); cy.get('.cvat-notification-create-task-success').should('exist');
cy.goToTaskList(); cy.goToTaskList();
cy.contains('.cvat-item-task-name', taskName).should('exist'); cy.contains('.cvat-item-task-name', taskName).should('exist');

@ -197,7 +197,7 @@ Cypress.Commands.add(
if (multiAttrParams) { if (multiAttrParams) {
cy.updateAttributes(multiAttrParams); cy.updateAttributes(multiAttrParams);
} }
cy.contains('button', 'Done').click(); cy.contains('button', 'Continue').click();
} else { } else {
if (attachToProject) { if (attachToProject) {
cy.get('.cvat-project-search-field').click(); cy.get('.cvat-project-search-field').click();
@ -217,7 +217,7 @@ Cypress.Commands.add(
if (advancedConfigurationParams) { if (advancedConfigurationParams) {
cy.advancedConfiguration(advancedConfigurationParams); cy.advancedConfiguration(advancedConfigurationParams);
} }
cy.contains('button', 'Submit').click(); cy.contains('button', 'Submit & Continue').click();
if (expectedResult === 'success') { if (expectedResult === 'success') {
cy.get('.cvat-notification-create-task-success').should('exist').find('[data-icon="close"]').click(); cy.get('.cvat-notification-create-task-success').should('exist').find('[data-icon="close"]').click();
} }
@ -699,7 +699,8 @@ Cypress.Commands.add('addNewLabel', (newLabelName, additionalAttrs, labelColor)
cy.updateAttributes(additionalAttrs[i]); cy.updateAttributes(additionalAttrs[i]);
} }
} }
cy.contains('button', 'Done').click(); cy.contains('button', 'Continue').click();
cy.contains('button', 'Cancel').click();
cy.get('.cvat-spinner').should('not.exist'); cy.get('.cvat-spinner').should('not.exist');
cy.get('.cvat-constructor-viewer').should('be.visible'); cy.get('.cvat-constructor-viewer').should('be.visible');
cy.contains('.cvat-constructor-viewer-item', new RegExp(`^${newLabelName}$`)).should('exist'); cy.contains('.cvat-constructor-viewer-item', new RegExp(`^${newLabelName}$`)).should('exist');
@ -711,12 +712,9 @@ Cypress.Commands.add('addNewLabelViaContinueButton', (additionalLabels) => {
cy.get('.cvat-constructor-viewer-new-item').click(); cy.get('.cvat-constructor-viewer-new-item').click();
for (let j = 0; j < additionalLabels.length; j++) { for (let j = 0; j < additionalLabels.length; j++) {
cy.get('[placeholder="Label name"]').type(additionalLabels[j]); cy.get('[placeholder="Label name"]').type(additionalLabels[j]);
if (j !== additionalLabels.length - 1) { cy.contains('button', 'Continue').click();
cy.contains('button', 'Continue').click();
} else {
cy.contains('button', 'Done').click();
}
} }
cy.contains('button', 'Cancel').click();
} }
}); });
}); });

@ -26,9 +26,9 @@ Cypress.Commands.add(
if (multiAttrParams) { if (multiAttrParams) {
cy.updateAttributes(multiAttrParams); cy.updateAttributes(multiAttrParams);
} }
cy.contains('button', 'Done').click(); cy.contains('button', 'Continue').click();
cy.get('.cvat-create-project-content').within(() => { cy.get('.cvat-create-project-content').within(() => {
cy.contains('Submit').click(); cy.contains('button', 'Submit & Continue').click();
}); });
if (expectedResult === 'success') { if (expectedResult === 'success') {
cy.get('.cvat-notification-create-project-success').should('exist').find('[data-icon="close"]').click(); cy.get('.cvat-notification-create-project-success').should('exist').find('[data-icon="close"]').click();
@ -184,7 +184,6 @@ Cypress.Commands.add('deleteProjectViaActions', (projectName) => {
Cypress.Commands.add('assignProjectToUser', (user) => { Cypress.Commands.add('assignProjectToUser', (user) => {
cy.get('.cvat-project-details').within(() => { cy.get('.cvat-project-details').within(() => {
cy.get('.cvat-user-search-field').click().type(user); cy.get('.cvat-user-search-field').click().type(user);
cy.wait(300);
}); });
cy.get('.ant-select-dropdown') cy.get('.ant-select-dropdown')
.not('.ant-select-dropdown-hidden') .not('.ant-select-dropdown-hidden')

Loading…
Cancel
Save