Annotations filters new UI (#2871)
* Filters new UI implemented * Build fix * ESLint unmached pattern error fix * ESLint unmached pattern error fix in cvat-ui * ESLint unmached pattern error fix in github action workflow * Old test exclude from jest scope * Build fix * Build fix 1 * Build fix 2 * Tests failure fix * Review comments fix 1 and lock-hide test fix * lock-hide test fix * packages fix for cor & ui * Review comments fix * Top bar right group layout justify fix * Annotation page header responsive fix * Filters modal layout fix * Build fix. E2E case 13 workaround * Linters fix * Recently used empty rows fix * Comparable fields config fix * Build linters fix * Minor fixes * Fixed broken navigation * Fixed createObjectURL * Removed extra import * Fixed issues with attributes * Extra line were removed * Fixed typos * All renamed clientID -> objectID * Fixed small issues * Code refactoring * Fixed dot-contained names * Reordered import Co-authored-by: Boris Sekachev <boris.sekachev@intel.com>main
parent
ce1666f6f8
commit
11d818dac2
@ -1,121 +0,0 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Setup mock for a server
|
||||
jest.mock('../../src/server-proxy', () => {
|
||||
const mock = require('../mocks/server-proxy.mock');
|
||||
return mock;
|
||||
});
|
||||
|
||||
const AnnotationsFilter = require('../../src/annotations-filter');
|
||||
// Initialize api
|
||||
window.cvat = require('../../src/api');
|
||||
|
||||
// Test cases
|
||||
describe('Feature: toJSONQuery', () => {
|
||||
test('convert filters to a json query', () => {
|
||||
const annotationsFilter = new AnnotationsFilter();
|
||||
const [groups, query] = annotationsFilter.toJSONQuery([]);
|
||||
expect(Array.isArray(groups)).toBeTruthy();
|
||||
expect(typeof query).toBe('string');
|
||||
});
|
||||
|
||||
test('convert empty fitlers to a json query', () => {
|
||||
const annotationsFilter = new AnnotationsFilter();
|
||||
const [, query] = annotationsFilter.toJSONQuery([]);
|
||||
expect(query).toBe('$.objects[*].clientID');
|
||||
});
|
||||
|
||||
test('convert wrong fitlers (empty string) to a json query', () => {
|
||||
const annotationsFilter = new AnnotationsFilter();
|
||||
expect(() => {
|
||||
annotationsFilter.toJSONQuery(['']);
|
||||
}).toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('convert wrong fitlers (wrong number argument) to a json query', () => {
|
||||
const annotationsFilter = new AnnotationsFilter();
|
||||
expect(() => {
|
||||
annotationsFilter.toJSONQuery(1);
|
||||
}).toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('convert wrong fitlers (wrong array argument) to a json query', () => {
|
||||
const annotationsFilter = new AnnotationsFilter();
|
||||
expect(() => {
|
||||
annotationsFilter.toJSONQuery(['clientID ==6', 1]);
|
||||
}).toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('convert wrong filters (wrong expression) to a json query', () => {
|
||||
const annotationsFilter = new AnnotationsFilter();
|
||||
expect(() => {
|
||||
annotationsFilter.toJSONQuery(['clientID=5']);
|
||||
}).toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('convert filters to a json query', () => {
|
||||
const annotationsFilter = new AnnotationsFilter();
|
||||
const [groups, query] = annotationsFilter.toJSONQuery(['clientID==5 & shape=="rectangle" & label==["car"]']);
|
||||
expect(groups).toEqual([['clientID==5', '&', 'shape=="rectangle"', '&', 'label==["car"]']]);
|
||||
expect(query).toBe('$.objects[?((@.clientID==5&@.shape=="rectangle"&@.label==["car"]))].clientID');
|
||||
});
|
||||
|
||||
test('convert filters to a json query', () => {
|
||||
const annotationsFilter = new AnnotationsFilter();
|
||||
const [groups, query] = annotationsFilter.toJSONQuery(['label=="car" | width >= height & type=="track"']);
|
||||
expect(groups).toEqual([['label=="car"', '|', 'width >= height', '&', 'type=="track"']]);
|
||||
expect(query).toBe('$.objects[?((@.label=="car"|@.width>=@.height&@.type=="track"))].clientID');
|
||||
});
|
||||
|
||||
test('convert filters to a json query', () => {
|
||||
const annotationsFilter = new AnnotationsFilter();
|
||||
const [groups, query] = annotationsFilter.toJSONQuery([
|
||||
'label=="person" & attr["Attribute 1"] ==attr["Attribute 2"]',
|
||||
]);
|
||||
expect(groups).toEqual([['label=="person"', '&', 'attr["Attribute 1"] ==attr["Attribute 2"]']]);
|
||||
expect(query).toBe('$.objects[?((@.label=="person"&@.attr["Attribute 1"]==@.attr["Attribute 2"]))].clientID');
|
||||
});
|
||||
|
||||
test('convert filters to a json query', () => {
|
||||
const annotationsFilter = new AnnotationsFilter();
|
||||
const [groups, query] = annotationsFilter.toJSONQuery([
|
||||
'label=="car" & attr["parked"]==true',
|
||||
'label=="pedestrian" & width > 150',
|
||||
]);
|
||||
expect(groups).toEqual([
|
||||
['label=="car"', '&', 'attr["parked"]==true'],
|
||||
'|',
|
||||
['label=="pedestrian"', '&', 'width > 150'],
|
||||
]);
|
||||
expect(query).toBe(
|
||||
'$.objects[?((@.label=="car"&@.attr["parked"]==true)|(@.label=="pedestrian"&@.width>150))].clientID',
|
||||
);
|
||||
});
|
||||
|
||||
test('convert filters to a json query', () => {
|
||||
const annotationsFilter = new AnnotationsFilter();
|
||||
const [groups, query] = annotationsFilter.toJSONQuery([
|
||||
// eslint-disable-next-line
|
||||
'(( label==["car \\"mazda\\""]) & (attr["sunglass ( help ) es"]==true | (width > 150 | height > 150 & (clientID == serverID))))) ',
|
||||
]);
|
||||
expect(groups).toEqual([
|
||||
[
|
||||
[
|
||||
['label==["car `mazda`"]'],
|
||||
'&',
|
||||
[
|
||||
'attr["sunglass ( help ) es"]==true',
|
||||
'|',
|
||||
['width > 150', '|', 'height > 150', '&', ['clientID == serverID']],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
expect(query).toBe(
|
||||
// eslint-disable-next-line
|
||||
'$.objects[?((((@.label==["car `mazda`"])&(@.attr["sunglass ( help ) es"]==true|(@.width>150|@.height>150&(@.clientID==serverID))))))].clientID',
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -1,183 +0,0 @@
|
||||
// Copyright (C) 2020-2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Select, { SelectValue, LabeledValue } from 'antd/lib/select';
|
||||
import Title from 'antd/lib/typography/Title';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
import Paragraph from 'antd/lib/typography/Paragraph';
|
||||
import Modal from 'antd/lib/modal';
|
||||
import { FilterOutlined } from '@ant-design/icons';
|
||||
|
||||
import {
|
||||
changeAnnotationsFilters as changeAnnotationsFiltersAction,
|
||||
fetchAnnotationsAsync,
|
||||
} from 'actions/annotation-actions';
|
||||
import CVATTooltip from 'components/common/cvat-tooltip';
|
||||
import { CombinedState } from 'reducers/interfaces';
|
||||
|
||||
interface StateToProps {
|
||||
annotationsFilters: string[];
|
||||
annotationsFiltersHistory: string[];
|
||||
searchForwardShortcut: string;
|
||||
searchBackwardShortcut: string;
|
||||
}
|
||||
|
||||
interface DispatchToProps {
|
||||
changeAnnotationsFilters(value: SelectValue): void;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: CombinedState): StateToProps {
|
||||
const {
|
||||
annotation: {
|
||||
annotations: { filters: annotationsFilters, filtersHistory: annotationsFiltersHistory },
|
||||
},
|
||||
shortcuts: { normalizedKeyMap },
|
||||
} = state;
|
||||
|
||||
return {
|
||||
annotationsFilters,
|
||||
annotationsFiltersHistory,
|
||||
searchForwardShortcut: normalizedKeyMap.SEARCH_FORWARD,
|
||||
searchBackwardShortcut: normalizedKeyMap.SEARCH_BACKWARD,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
||||
return {
|
||||
changeAnnotationsFilters(value: SelectValue) {
|
||||
if (typeof value === 'string') {
|
||||
dispatch(changeAnnotationsFiltersAction([value]));
|
||||
dispatch(fetchAnnotationsAsync());
|
||||
} else if (
|
||||
Array.isArray(value) &&
|
||||
!value.some((element: string | number | LabeledValue): boolean => typeof element !== 'string')
|
||||
) {
|
||||
dispatch(changeAnnotationsFiltersAction(value as string[]));
|
||||
dispatch(fetchAnnotationsAsync());
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function filtersHelpModalContent(searchForwardShortcut: string, searchBackwardShortcut: string): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Paragraph>
|
||||
<Title level={3}>General</Title>
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
You can use filters to display only subset of objects on a frame or to search objects that satisfy the
|
||||
filters using hotkeys
|
||||
<Text strong>{` ${searchForwardShortcut} `}</Text>
|
||||
and
|
||||
<Text strong>{` ${searchBackwardShortcut} `}</Text>
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Text strong>Supported properties: </Text>
|
||||
width, height, label, serverID, clientID, type, shape, occluded
|
||||
<br />
|
||||
<Text strong>Supported operators: </Text>
|
||||
==, !=, >, >=, <, <=, (), & and |
|
||||
<br />
|
||||
<Text strong>
|
||||
If you have double quotes in your query string, please escape them using back slash: \" (see
|
||||
the latest example)
|
||||
</Text>
|
||||
<br />
|
||||
All properties and values are case-sensitive. CVAT uses json queries to perform search.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Title level={3}>Examples</Title>
|
||||
<ul>
|
||||
<li>label=="car" | label==["road sign"]</li>
|
||||
<li>shape == "polygon"</li>
|
||||
<li>width >= height</li>
|
||||
<li>attr["Attribute 1"] == attr["Attribute 2"]</li>
|
||||
<li>clientID == 50</li>
|
||||
<li>
|
||||
(label=="car" & attr["parked"]==true) | (label=="pedestrian"
|
||||
& width > 150)
|
||||
</li>
|
||||
<li>
|
||||
(( label==["car \"mazda\""]) & (attr["sunglasses"]==true |
|
||||
(width > 150 | height > 150 & (clientID == serverID)))))
|
||||
</li>
|
||||
</ul>
|
||||
</Paragraph>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AnnotationsFiltersInput(props: StateToProps & DispatchToProps): JSX.Element {
|
||||
const {
|
||||
annotationsFilters,
|
||||
annotationsFiltersHistory,
|
||||
searchForwardShortcut,
|
||||
searchBackwardShortcut,
|
||||
changeAnnotationsFilters,
|
||||
} = props;
|
||||
|
||||
const [underCursor, setUnderCursor] = useState<boolean>(false);
|
||||
const [dropdownVisible, setDropdownVisible] = useState<boolean>(true);
|
||||
|
||||
return (
|
||||
<Select
|
||||
className='cvat-annotations-filters-input'
|
||||
allowClear
|
||||
value={annotationsFilters}
|
||||
mode='tags'
|
||||
disabled={!dropdownVisible}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={
|
||||
underCursor ? (
|
||||
<>
|
||||
<CVATTooltip title='Click to open help'>
|
||||
<FilterOutlined
|
||||
style={{ pointerEvents: 'all' }}
|
||||
onClick={() => {
|
||||
// also opens the select dropdown
|
||||
// looks like it is done on capturing state
|
||||
// so we cannot cancel it somehow via the event handling
|
||||
// to avoid it we use also onMouseEnter, onMouseLeave below
|
||||
Modal.info({
|
||||
width: 700,
|
||||
title: 'How to use filters?',
|
||||
content: filtersHelpModalContent(searchForwardShortcut, searchBackwardShortcut),
|
||||
className: 'cvat-annotations-filters-help-modal-window',
|
||||
});
|
||||
}}
|
||||
onMouseEnter={() => setDropdownVisible(false)}
|
||||
onMouseLeave={() => setDropdownVisible(true)}
|
||||
/>
|
||||
</CVATTooltip>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FilterOutlined style={{ transform: 'scale(0.9)' }} />
|
||||
<span style={{ marginLeft: 5 }}>Annotations filters</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
onChange={changeAnnotationsFilters}
|
||||
onMouseEnter={() => setUnderCursor(true)}
|
||||
onMouseLeave={() => setUnderCursor(false)}
|
||||
>
|
||||
{annotationsFiltersHistory.map(
|
||||
(element: string): JSX.Element => (
|
||||
<Select.Option
|
||||
key={element}
|
||||
value={element}
|
||||
className='cvat-annotations-filters-input-history-element'
|
||||
>
|
||||
{element}
|
||||
</Select.Option>
|
||||
),
|
||||
)}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AnnotationsFiltersInput);
|
||||
@ -0,0 +1,288 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
Builder, Config, ImmutableTree, JsonLogicTree, Query, Utils as QbUtils,
|
||||
} from 'react-awesome-query-builder';
|
||||
import AntdWidgets from 'react-awesome-query-builder/lib/components/widgets/antd';
|
||||
import AntdConfig from 'react-awesome-query-builder/lib/config/antd';
|
||||
import 'react-awesome-query-builder/lib/css/styles.css';
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import { Dropdown, Menu } from 'antd';
|
||||
import Button from 'antd/lib/button';
|
||||
import Modal from 'antd/lib/modal';
|
||||
import { omit } from 'lodash';
|
||||
|
||||
import { CombinedState } from 'reducers/interfaces';
|
||||
import { changeAnnotationsFilters, fetchAnnotationsAsync, showFilters } from 'actions/annotation-actions';
|
||||
|
||||
const { FieldDropdown } = AntdWidgets;
|
||||
|
||||
const FILTERS_HISTORY = 'annotationFiltersHistory';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
interface StoredFilter {
|
||||
id: string;
|
||||
logic: JsonLogicTree;
|
||||
}
|
||||
|
||||
export default function FiltersModalComponent(props: Props): JSX.Element {
|
||||
const { visible } = props;
|
||||
const { labels } = useSelector((state: CombinedState) => state.annotation.job);
|
||||
const { filters: activeFilters } = useSelector((state: CombinedState) => state.annotation.annotations);
|
||||
|
||||
const getConvertedInputType = (inputType: string): string => {
|
||||
switch (inputType) {
|
||||
case 'checkbox':
|
||||
return 'boolean';
|
||||
case 'radio':
|
||||
return 'select';
|
||||
default:
|
||||
return inputType;
|
||||
}
|
||||
};
|
||||
|
||||
const adjustName = (name: string): string => name.replaceAll('.', '\u2219');
|
||||
|
||||
const getAttributesSubfields = (): Record<string, any> => {
|
||||
const subfields: Record<string, any> = {};
|
||||
labels.forEach((label: any): void => {
|
||||
const adjustedLabelName = adjustName(label.name);
|
||||
subfields[adjustedLabelName] = {
|
||||
type: '!struct', // nested complex field
|
||||
label: label.name,
|
||||
subfields: {},
|
||||
};
|
||||
|
||||
const labelSubfields = subfields[adjustedLabelName].subfields;
|
||||
label.attributes.forEach((attr: any): void => {
|
||||
const adjustedAttrName = adjustName(attr.name);
|
||||
labelSubfields[adjustedAttrName] = {
|
||||
label: attr.name,
|
||||
type: getConvertedInputType(attr.inputType),
|
||||
};
|
||||
if (labelSubfields[adjustedAttrName].type === 'select') {
|
||||
labelSubfields[adjustedAttrName] = {
|
||||
...labelSubfields[adjustedAttrName],
|
||||
fieldSettings: {
|
||||
listValues: attr.values,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return subfields;
|
||||
};
|
||||
|
||||
const config: Config = {
|
||||
...AntdConfig,
|
||||
fields: {
|
||||
label: {
|
||||
label: 'Label',
|
||||
type: 'select',
|
||||
valueSources: ['value'],
|
||||
fieldSettings: {
|
||||
listValues: labels.map((label: any) => label.name),
|
||||
},
|
||||
},
|
||||
type: {
|
||||
label: 'Type',
|
||||
type: 'select',
|
||||
fieldSettings: {
|
||||
listValues: [
|
||||
{ value: 'shape', title: 'Shape' },
|
||||
{ value: 'track', title: 'Track' },
|
||||
{ value: 'tag', title: 'Tag' },
|
||||
],
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
label: 'Shape',
|
||||
type: 'select',
|
||||
fieldSettings: {
|
||||
listValues: [
|
||||
{ value: 'rectangle', title: 'Rectangle' },
|
||||
{ value: 'points', title: 'Points' },
|
||||
{ value: 'polyline', title: 'Polyline' },
|
||||
{ value: 'polygon', title: 'Polygon' },
|
||||
{ value: 'cuboids', title: 'Cuboids' },
|
||||
],
|
||||
},
|
||||
},
|
||||
occluded: {
|
||||
label: 'Occluded',
|
||||
type: 'boolean',
|
||||
},
|
||||
width: {
|
||||
label: 'Width',
|
||||
type: 'number',
|
||||
fieldSettings: { min: 0 },
|
||||
},
|
||||
height: {
|
||||
label: 'Height',
|
||||
type: 'number',
|
||||
fieldSettings: { min: 0 },
|
||||
},
|
||||
objectID: {
|
||||
label: 'ObjectID',
|
||||
type: 'number',
|
||||
hideForCompare: true,
|
||||
fieldSettings: { min: 0 },
|
||||
},
|
||||
serverID: {
|
||||
label: 'ServerID',
|
||||
type: 'number',
|
||||
hideForCompare: true,
|
||||
fieldSettings: { min: 0 },
|
||||
},
|
||||
attr: {
|
||||
label: 'Attributes',
|
||||
type: '!struct',
|
||||
subfields: getAttributesSubfields(),
|
||||
fieldSettings: {
|
||||
treeSelectOnlyLeafs: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
...AntdConfig.settings,
|
||||
renderField: (_props: any) => (
|
||||
<FieldDropdown {...omit(_props)} customProps={omit(_props.customProps, 'showSearch')} />
|
||||
),
|
||||
// using FieldDropdown because we cannot use antd because of antd-related bugs
|
||||
// https://github.com/ukrbublik/react-awesome-query-builder/issues/224
|
||||
},
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
tree: QbUtils.checkTree(
|
||||
QbUtils.loadTree({ id: QbUtils.uuid(), type: 'group' }),
|
||||
config as Config,
|
||||
) as ImmutableTree,
|
||||
config,
|
||||
};
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [state, setState] = useState(initialState);
|
||||
const [filters, setFilters] = useState([] as StoredFilter[]);
|
||||
|
||||
useEffect(() => {
|
||||
const filtersHistory = window.localStorage.getItem(FILTERS_HISTORY)?.trim() || '[]';
|
||||
try {
|
||||
setFilters(JSON.parse(filtersHistory));
|
||||
} catch (_) {
|
||||
setFilters([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.localStorage.setItem(FILTERS_HISTORY, JSON.stringify(filters));
|
||||
}, [filters]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
const treeFromActiveFilters = activeFilters.length ?
|
||||
QbUtils.checkTree(QbUtils.loadFromJsonLogic(activeFilters[0], config), config) :
|
||||
null;
|
||||
setState({
|
||||
tree: treeFromActiveFilters || initialState.tree,
|
||||
config,
|
||||
});
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const applyFilters = (filtersData: any[]): void => {
|
||||
dispatch(changeAnnotationsFilters(filtersData));
|
||||
dispatch(fetchAnnotationsAsync());
|
||||
dispatch(showFilters(false));
|
||||
};
|
||||
|
||||
const confirmModal = (): void => {
|
||||
const currentFilter: StoredFilter = {
|
||||
id: QbUtils.uuid(),
|
||||
logic: QbUtils.jsonLogicFormat(state.tree, config).logic || {},
|
||||
};
|
||||
const updatedFilters = filters.filter(
|
||||
(filter) => JSON.stringify(filter.logic) !== JSON.stringify(currentFilter.logic),
|
||||
);
|
||||
setFilters([currentFilter, ...updatedFilters].slice(0, 10));
|
||||
applyFilters([QbUtils.jsonLogicFormat(state.tree, config).logic]);
|
||||
};
|
||||
|
||||
const isModalConfirmable = (): boolean =>
|
||||
QbUtils.queryString(state.tree, config)?.trim().length > 0 && QbUtils.isValidTree(state.tree);
|
||||
|
||||
const renderBuilder = (builderProps: any): JSX.Element => (
|
||||
<div className='query-builder-container'>
|
||||
<div className='query-builder qb-lite'>
|
||||
<Builder {...builderProps} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const onChange = (tree: ImmutableTree): void => {
|
||||
setState({ tree, config });
|
||||
};
|
||||
|
||||
const menu = (
|
||||
<Menu>
|
||||
{filters
|
||||
.filter((filter: StoredFilter) => {
|
||||
const tree = QbUtils.loadFromJsonLogic(filter.logic, config);
|
||||
return !!QbUtils.queryString(tree, config);
|
||||
})
|
||||
.map((filter: StoredFilter) => {
|
||||
const tree = QbUtils.loadFromJsonLogic(filter.logic, config);
|
||||
return (
|
||||
<Menu.Item key={filter.id} onClick={() => setState({ tree, config })}>
|
||||
{QbUtils.queryString(tree, config)}
|
||||
</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className='cvat-filters-modal'
|
||||
visible={visible}
|
||||
closable={false}
|
||||
width={800}
|
||||
centered
|
||||
onCancel={() => dispatch(showFilters(false))}
|
||||
footer={[
|
||||
<Button key='clear' disabled={!activeFilters.length} onClick={() => applyFilters([])}>
|
||||
Clear filters
|
||||
</Button>,
|
||||
<Button key='cancel' onClick={() => dispatch(showFilters(false))}>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button key='submit' type='primary' disabled={!isModalConfirmable()} onClick={confirmModal}>
|
||||
Submit
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<div
|
||||
key='used'
|
||||
className='recently-used-wrapper'
|
||||
style={{ display: filters.length ? 'inline-block' : 'none' }}
|
||||
>
|
||||
<Dropdown overlay={menu}>
|
||||
<Button type='text'>
|
||||
Recently used
|
||||
{' '}
|
||||
<DownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<Query {...config} value={state.tree} onChange={onChange} renderBuilder={renderBuilder} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { CombinedState } from 'reducers/interfaces';
|
||||
import FiltersModalComponent from 'components/annotation-page/top-bar/filters-modal';
|
||||
|
||||
interface StateToProps {
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: CombinedState): StateToProps => {
|
||||
const {
|
||||
annotation: { filtersPanelVisible: visible },
|
||||
} = state;
|
||||
return { visible };
|
||||
};
|
||||
|
||||
function FiltersModalContainer(props: StateToProps): JSX.Element {
|
||||
const { visible } = props;
|
||||
return <FiltersModalComponent visible={visible} />;
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(FiltersModalContainer);
|
||||
@ -1,183 +1,185 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
// Copyright (C) 2020-2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/// <reference types="cypress" />
|
||||
// /// <reference types="cypress" />
|
||||
|
||||
import { taskName } from '../../support/const';
|
||||
// import { taskName } from '../../support/const';
|
||||
|
||||
context('Filters functionality.', () => {
|
||||
const caseId = '18';
|
||||
const labelShape = 'shape 3 points';
|
||||
const additionalAttrsLabelShape = [
|
||||
{ additionalAttrName: 'type', additionalValue: 'shape', typeAttribute: 'Text' },
|
||||
{ additionalAttrName: 'count points', additionalValue: '3', typeAttribute: 'Text' },
|
||||
{ additionalAttrName: 'polygon', additionalValue: 'True', typeAttribute: 'Checkbox' },
|
||||
];
|
||||
const labelTrack = 'track 4 points';
|
||||
const additionalAttrsLabelTrack = [
|
||||
{ additionalAttrName: 'type', additionalValue: 'track', typeAttribute: 'Text' },
|
||||
{ additionalAttrName: 'polygon', additionalValue: 'True', typeAttribute: 'Checkbox' },
|
||||
{ additionalAttrName: 'count points', additionalValue: '4', typeAttribute: 'Text' },
|
||||
];
|
||||
// TODO: Update with new filters UI
|
||||
|
||||
const createPolygonShape = {
|
||||
reDraw: false,
|
||||
type: 'Shape',
|
||||
labelName: labelShape,
|
||||
pointsMap: [
|
||||
{ x: 200, y: 200 },
|
||||
{ x: 250, y: 200 },
|
||||
{ x: 250, y: 240 },
|
||||
],
|
||||
complete: true,
|
||||
numberOfPoints: null,
|
||||
};
|
||||
const createRectangleTrack2Points = {
|
||||
points: 'By 2 Points',
|
||||
type: 'Track',
|
||||
labelName: labelTrack,
|
||||
firstX: 260,
|
||||
firstY: 200,
|
||||
secondX: 360,
|
||||
secondY: 250,
|
||||
};
|
||||
const createRectangleShape4Points = {
|
||||
points: 'By 4 Points',
|
||||
type: 'Shape',
|
||||
labelName: labelShape,
|
||||
firstX: 550,
|
||||
firstY: 350,
|
||||
secondX: 650,
|
||||
secondY: 350,
|
||||
thirdX: 650,
|
||||
thirdY: 450,
|
||||
fourthX: 550,
|
||||
fourthY: 450,
|
||||
};
|
||||
const createPolygonTrack = {
|
||||
reDraw: false,
|
||||
type: 'Track',
|
||||
labelName: labelTrack,
|
||||
pointsMap: [
|
||||
{ x: 700, y: 350 },
|
||||
{ x: 850, y: 350 },
|
||||
{ x: 850, y: 450 },
|
||||
{ x: 700, y: 450 },
|
||||
],
|
||||
numberOfPoints: 4,
|
||||
};
|
||||
// context('Filters functionality.', () => {
|
||||
// const caseId = '18';
|
||||
// const labelShape = 'shape 3 points';
|
||||
// const additionalAttrsLabelShape = [
|
||||
// { additionalAttrName: 'type', additionalValue: 'shape', typeAttribute: 'Text' },
|
||||
// { additionalAttrName: 'count points', additionalValue: '3', typeAttribute: 'Text' },
|
||||
// { additionalAttrName: 'polygon', additionalValue: 'True', typeAttribute: 'Checkbox' },
|
||||
// ];
|
||||
// const labelTrack = 'track 4 points';
|
||||
// const additionalAttrsLabelTrack = [
|
||||
// { additionalAttrName: 'type', additionalValue: 'track', typeAttribute: 'Text' },
|
||||
// { additionalAttrName: 'polygon', additionalValue: 'True', typeAttribute: 'Checkbox' },
|
||||
// { additionalAttrName: 'count points', additionalValue: '4', typeAttribute: 'Text' },
|
||||
// ];
|
||||
|
||||
let cvatCanvasShapeList = [];
|
||||
let cvatFiltesList = [];
|
||||
// const createPolygonShape = {
|
||||
// reDraw: false,
|
||||
// type: 'Shape',
|
||||
// labelName: labelShape,
|
||||
// pointsMap: [
|
||||
// { x: 200, y: 200 },
|
||||
// { x: 250, y: 200 },
|
||||
// { x: 250, y: 240 },
|
||||
// ],
|
||||
// complete: true,
|
||||
// numberOfPoints: null,
|
||||
// };
|
||||
// const createRectangleTrack2Points = {
|
||||
// points: 'By 2 Points',
|
||||
// type: 'Track',
|
||||
// labelName: labelTrack,
|
||||
// firstX: 260,
|
||||
// firstY: 200,
|
||||
// secondX: 360,
|
||||
// secondY: 250,
|
||||
// };
|
||||
// const createRectangleShape4Points = {
|
||||
// points: 'By 4 Points',
|
||||
// type: 'Shape',
|
||||
// labelName: labelShape,
|
||||
// firstX: 550,
|
||||
// firstY: 350,
|
||||
// secondX: 650,
|
||||
// secondY: 350,
|
||||
// thirdX: 650,
|
||||
// thirdY: 450,
|
||||
// fourthX: 550,
|
||||
// fourthY: 450,
|
||||
// };
|
||||
// const createPolygonTrack = {
|
||||
// reDraw: false,
|
||||
// type: 'Track',
|
||||
// labelName: labelTrack,
|
||||
// pointsMap: [
|
||||
// { x: 700, y: 350 },
|
||||
// { x: 850, y: 350 },
|
||||
// { x: 850, y: 450 },
|
||||
// { x: 700, y: 450 },
|
||||
// ],
|
||||
// numberOfPoints: 4,
|
||||
// };
|
||||
|
||||
function checkingFilterApplication(ids) {
|
||||
for (let i = 0; i < cvatCanvasShapeList.length; i++) {
|
||||
if (ids.indexOf(cvatCanvasShapeList[i]) > -1) {
|
||||
cy.get(`#cvat_canvas_shape_${cvatCanvasShapeList[i]}`).should('exist');
|
||||
cy.get(`#cvat-objects-sidebar-state-item-${cvatCanvasShapeList[i]}`).should('exist');
|
||||
} else {
|
||||
cy.get(`#cvat_canvas_shape_${cvatCanvasShapeList[i]}`).should('not.exist');
|
||||
cy.get(`#cvat-objects-sidebar-state-item-${cvatCanvasShapeList[i]}`).should('not.exist');
|
||||
}
|
||||
}
|
||||
}
|
||||
// let cvatCanvasShapeList = [];
|
||||
// let cvatFiltesList = [];
|
||||
|
||||
before(() => {
|
||||
cy.openTask(taskName);
|
||||
cy.addNewLabel(labelShape, additionalAttrsLabelShape);
|
||||
cy.addNewLabel(labelTrack, additionalAttrsLabelTrack);
|
||||
cy.openJob();
|
||||
});
|
||||
// function checkingFilterApplication(ids) {
|
||||
// for (let i = 0; i < cvatCanvasShapeList.length; i++) {
|
||||
// if (ids.indexOf(cvatCanvasShapeList[i]) > -1) {
|
||||
// cy.get(`#cvat_canvas_shape_${cvatCanvasShapeList[i]}`).should('exist');
|
||||
// cy.get(`#cvat-objects-sidebar-state-item-${cvatCanvasShapeList[i]}`).should('exist');
|
||||
// } else {
|
||||
// cy.get(`#cvat_canvas_shape_${cvatCanvasShapeList[i]}`).should('not.exist');
|
||||
// cy.get(`#cvat-objects-sidebar-state-item-${cvatCanvasShapeList[i]}`).should('not.exist');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
describe(`Testing case "${caseId}"`, () => {
|
||||
it('Draw several objects (different shapes, tracks, labels)', () => {
|
||||
cy.createPolygon(createPolygonShape);
|
||||
cy.createRectangle(createRectangleTrack2Points);
|
||||
cy.createRectangle(createRectangleShape4Points);
|
||||
cy.createPolygon(createPolygonTrack);
|
||||
cy.get('.cvat_canvas_shape').then(($cvatCanvasShapeList) => {
|
||||
for (let i = 0; i < $cvatCanvasShapeList.length; i++) {
|
||||
cvatCanvasShapeList.push(Number($cvatCanvasShapeList[i].id.match(/\d+$/)));
|
||||
}
|
||||
});
|
||||
});
|
||||
it('Filter: shape=="polygon". Only the polygon exist.', () => {
|
||||
const textFilter = 'shape=="polygon"';
|
||||
cvatFiltesList.push(textFilter);
|
||||
cy.writeFilterValue(false, textFilter); // #cvat_canvas_shape_1,4, #cvat-objects-sidebar-state-item-1,4
|
||||
checkingFilterApplication([1, 4]);
|
||||
});
|
||||
it('Filter: shape=="polygon" | shape=="rectangle". Only the rectangle and polygon exist.', () => {
|
||||
const textFilter = 'shape=="polygon" | shape=="rectangle"';
|
||||
cvatFiltesList.push(textFilter);
|
||||
cy.writeFilterValue(true, textFilter); // #cvat_canvas_shape_1,2,3,4, #cvat-objects-sidebar-state-item-1,2,3,4
|
||||
checkingFilterApplication([1, 2, 3, 4]);
|
||||
});
|
||||
it('Filter: type=="shape". Only the objects with shape type exist.', () => {
|
||||
const textFilter = 'type=="shape"';
|
||||
cvatFiltesList.push(textFilter);
|
||||
cy.writeFilterValue(true, textFilter); // #cvat_canvas_shape_1,3, #cvat-objects-sidebar-state-item-1,3
|
||||
checkingFilterApplication([1, 3]);
|
||||
});
|
||||
it('Filter: label=="track 4 points". Only the polygon exist.', () => {
|
||||
const textFilter = `label=="${labelTrack}"`;
|
||||
cvatFiltesList.push(textFilter);
|
||||
cy.writeFilterValue(true, textFilter); // #cvat_canvas_shape_2,4, #cvat-objects-sidebar-state-item-2,4
|
||||
checkingFilterApplication([2, 4]);
|
||||
});
|
||||
it('Filter: attr["count points"] == "4". Only the objects with same attr exist.', () => {
|
||||
const textFilter = 'attr["count points"] == "4"';
|
||||
cvatFiltesList.push(textFilter);
|
||||
cy.writeFilterValue(true, textFilter); // #cvat_canvas_shape_2,4, #cvat-objects-sidebar-state-item-2,4
|
||||
checkingFilterApplication([2, 4]);
|
||||
});
|
||||
it('Filter: width >= height. All objects exist.', () => {
|
||||
const textFilter = 'width >= height';
|
||||
cvatFiltesList.push(textFilter);
|
||||
cy.writeFilterValue(true, textFilter); // #cvat_canvas_shape_1,2,3,4, #cvat-objects-sidebar-state-item-1,2,3,4
|
||||
checkingFilterApplication([1, 2, 3, 4]);
|
||||
});
|
||||
it('Filter: clientID == 4. Only the objects with same id exist (polygon track).', () => {
|
||||
const textFilter = 'clientID == 4';
|
||||
cvatFiltesList.push(textFilter);
|
||||
cy.writeFilterValue(true, textFilter); // #cvat_canvas_shape_7, #cvat-objects-sidebar-state-item-4
|
||||
checkingFilterApplication([4]);
|
||||
});
|
||||
it('Filter: (label=="shape 3 points" & attr["polylines"]==true) | (label=="track 4 points" & width > 60). Only the objects polygon and rectangle exist.', () => {
|
||||
const textFilter =
|
||||
'(label=="shape 3 points" & attr["polylines"]==true) | (label=="track 4 points" & width > 60)';
|
||||
cvatFiltesList.push(textFilter);
|
||||
cy.writeFilterValue(true, textFilter); // #cvat_canvas_shape_2,4, #cvat-objects-sidebar-state-item-2,4
|
||||
checkingFilterApplication([2, 4]);
|
||||
});
|
||||
it('Filter: (( label==["shape 3 points"]) | (attr["type"]=="shape" & width > 50)) & (height > 50 & (clientID == serverID))). All objects not exist.', () => {
|
||||
const textFilter =
|
||||
'(( label==["points shape"]) | (attr["type"]=="shape" & width > 50)) & (height > 50 & (clientID == serverID)))';
|
||||
cvatFiltesList.push(textFilter);
|
||||
cy.writeFilterValue(true, textFilter);
|
||||
checkingFilterApplication([]);
|
||||
});
|
||||
it('Verify to show all filters', () => {
|
||||
cvatFiltesList.forEach(function (filterValue) {
|
||||
cy.contains('.cvat-annotations-filters-input-history-element', filterValue);
|
||||
});
|
||||
});
|
||||
it('Select filter: type=="shape"', () => {
|
||||
cy.selectFilterValue(true, 'type=="shape"'); // #cvat_canvas_shape_1,3, #cvat-objects-sidebar-state-item-1,3
|
||||
checkingFilterApplication([1, 3]);
|
||||
});
|
||||
it('Select filter: clientID == 4', () => {
|
||||
cy.selectFilterValue(true, 'clientID == 4'); // #cvat_canvas_shape_7, #cvat-objects-sidebar-state-item-4
|
||||
checkingFilterApplication([4]);
|
||||
});
|
||||
it('Select two filters', () => {
|
||||
const textFirstFilter =
|
||||
'(label=="shape 3 points" & attr["polylines"]==true) | (label=="track 4 points" & width > 60)'; // #cvat_canvas_shape_2,4, #cvat-objects-sidebar-state-item-2,4
|
||||
const textSecondFilter = 'shape=="polygon"'; // #cvat_canvas_shape_1,4, #cvat-objects-sidebar-state-item-1,4
|
||||
cy.selectFilterValue(true, textFirstFilter);
|
||||
cy.selectFilterValue(false, textSecondFilter);
|
||||
checkingFilterApplication([1, 2, 4]);
|
||||
});
|
||||
});
|
||||
});
|
||||
// before(() => {
|
||||
// cy.openTask(taskName);
|
||||
// cy.addNewLabel(labelShape, additionalAttrsLabelShape);
|
||||
// cy.addNewLabel(labelTrack, additionalAttrsLabelTrack);
|
||||
// cy.openJob();
|
||||
// });
|
||||
|
||||
// describe(`Testing case "${caseId}"`, () => {
|
||||
// it('Draw several objects (different shapes, tracks, labels)', () => {
|
||||
// cy.createPolygon(createPolygonShape);
|
||||
// cy.createRectangle(createRectangleTrack2Points);
|
||||
// cy.createRectangle(createRectangleShape4Points);
|
||||
// cy.createPolygon(createPolygonTrack);
|
||||
// cy.get('.cvat_canvas_shape').then(($cvatCanvasShapeList) => {
|
||||
// for (let i = 0; i < $cvatCanvasShapeList.length; i++) {
|
||||
// cvatCanvasShapeList.push(Number($cvatCanvasShapeList[i].id.match(/\d+$/)));
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// it('Filter: shape=="polygon". Only the polygon exist.', () => {
|
||||
// const textFilter = 'shape=="polygon"';
|
||||
// cvatFiltesList.push(textFilter);
|
||||
// cy.writeFilterValue(false, textFilter); // #cvat_canvas_shape_1,4, #cvat-objects-sidebar-state-item-1,4
|
||||
// checkingFilterApplication([1, 4]);
|
||||
// });
|
||||
// it('Filter: shape=="polygon" | shape=="rectangle". Only the rectangle and polygon exist.', () => {
|
||||
// const textFilter = 'shape=="polygon" | shape=="rectangle"';
|
||||
// cvatFiltesList.push(textFilter);
|
||||
// cy.writeFilterValue(true, textFilter); // #cvat_canvas_shape_1,2,3,4, #cvat-objects-sidebar-state-item-1,2,3,4
|
||||
// checkingFilterApplication([1, 2, 3, 4]);
|
||||
// });
|
||||
// it('Filter: type=="shape". Only the objects with shape type exist.', () => {
|
||||
// const textFilter = 'type=="shape"';
|
||||
// cvatFiltesList.push(textFilter);
|
||||
// cy.writeFilterValue(true, textFilter); // #cvat_canvas_shape_1,3, #cvat-objects-sidebar-state-item-1,3
|
||||
// checkingFilterApplication([1, 3]);
|
||||
// });
|
||||
// it('Filter: label=="track 4 points". Only the polygon exist.', () => {
|
||||
// const textFilter = `label=="${labelTrack}"`;
|
||||
// cvatFiltesList.push(textFilter);
|
||||
// cy.writeFilterValue(true, textFilter); // #cvat_canvas_shape_2,4, #cvat-objects-sidebar-state-item-2,4
|
||||
// checkingFilterApplication([2, 4]);
|
||||
// });
|
||||
// it('Filter: attr["count points"] == "4". Only the objects with same attr exist.', () => {
|
||||
// const textFilter = 'attr["count points"] == "4"';
|
||||
// cvatFiltesList.push(textFilter);
|
||||
// cy.writeFilterValue(true, textFilter); // #cvat_canvas_shape_2,4, #cvat-objects-sidebar-state-item-2,4
|
||||
// checkingFilterApplication([2, 4]);
|
||||
// });
|
||||
// it('Filter: width >= height. All objects exist.', () => {
|
||||
// const textFilter = 'width >= height';
|
||||
// cvatFiltesList.push(textFilter);
|
||||
// cy.writeFilterValue(true, textFilter); // #cvat_canvas_shape_1,2,3,4, #cvat-objects-sidebar-state-item-1,2,3,4
|
||||
// checkingFilterApplication([1, 2, 3, 4]);
|
||||
// });
|
||||
// it('Filter: clientID == 4. Only the objects with same id exist (polygon track).', () => {
|
||||
// const textFilter = 'clientID == 4';
|
||||
// cvatFiltesList.push(textFilter);
|
||||
// cy.writeFilterValue(true, textFilter); // #cvat_canvas_shape_7, #cvat-objects-sidebar-state-item-4
|
||||
// checkingFilterApplication([4]);
|
||||
// });
|
||||
// it('Filter: (label=="shape 3 points" & attr["polylines"]==true) | (label=="track 4 points" & width > 60). Only the objects polygon and rectangle exist.', () => {
|
||||
// const textFilter =
|
||||
// '(label=="shape 3 points" & attr["polylines"]==true) | (label=="track 4 points" & width > 60)';
|
||||
// cvatFiltesList.push(textFilter);
|
||||
// cy.writeFilterValue(true, textFilter); // #cvat_canvas_shape_2,4, #cvat-objects-sidebar-state-item-2,4
|
||||
// checkingFilterApplication([2, 4]);
|
||||
// });
|
||||
// it('Filter: (( label==["shape 3 points"]) | (attr["type"]=="shape" & width > 50)) & (height > 50 & (clientID == serverID))). All objects not exist.', () => {
|
||||
// const textFilter =
|
||||
// '(( label==["points shape"]) | (attr["type"]=="shape" & width > 50)) & (height > 50 & (clientID == serverID)))';
|
||||
// cvatFiltesList.push(textFilter);
|
||||
// cy.writeFilterValue(true, textFilter);
|
||||
// checkingFilterApplication([]);
|
||||
// });
|
||||
// it('Verify to show all filters', () => {
|
||||
// cvatFiltesList.forEach(function (filterValue) {
|
||||
// cy.contains('.cvat-annotations-filters-input-history-element', filterValue);
|
||||
// });
|
||||
// });
|
||||
// it('Select filter: type=="shape"', () => {
|
||||
// cy.selectFilterValue(true, 'type=="shape"'); // #cvat_canvas_shape_1,3, #cvat-objects-sidebar-state-item-1,3
|
||||
// checkingFilterApplication([1, 3]);
|
||||
// });
|
||||
// it('Select filter: clientID == 4', () => {
|
||||
// cy.selectFilterValue(true, 'clientID == 4'); // #cvat_canvas_shape_7, #cvat-objects-sidebar-state-item-4
|
||||
// checkingFilterApplication([4]);
|
||||
// });
|
||||
// it('Select two filters', () => {
|
||||
// const textFirstFilter =
|
||||
// '(label=="shape 3 points" & attr["polylines"]==true) | (label=="track 4 points" & width > 60)'; // #cvat_canvas_shape_2,4, #cvat-objects-sidebar-state-item-2,4
|
||||
// const textSecondFilter = 'shape=="polygon"'; // #cvat_canvas_shape_1,4, #cvat-objects-sidebar-state-item-1,4
|
||||
// cy.selectFilterValue(true, textFirstFilter);
|
||||
// cy.selectFilterValue(false, textSecondFilter);
|
||||
// checkingFilterApplication([1, 2, 4]);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
Loading…
Reference in New Issue