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
Vitaliy Nishukov 5 years ago committed by GitHub
parent ce1666f6f8
commit 11d818dac2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [Backup/Restore guide](cvat/apps/documentation/backup_guide.md) (<https://github.com/openvinotoolkit/cvat/pull/2964>)
- Label deletion from tasks and projects (<https://github.com/openvinotoolkit/cvat/pull/2881>)
- [Market-1501](https://www.aitribune.com/dataset/2018051063) format support (<https://github.com/openvinotoolkit/cvat/pull/2869>)
- Annotations filters UI using react-awesome-query-builder (https://github.com/openvinotoolkit/cvat/issues/1418)
### Changed

@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "3.11.0",
"version": "3.12.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -5051,8 +5051,8 @@
"cvat-data": {
"version": "file:../cvat-data",
"requires": {
"async-mutex": "^0.2.6",
"jszip": "3.5.0"
"async-mutex": "^0.3.0",
"jszip": "3.6.0"
},
"dependencies": {
"@babel/cli": {
@ -14078,7 +14078,8 @@
"ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"dev": true
},
"inquirer": {
"version": "6.5.2",
@ -18342,6 +18343,11 @@
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA=="
},
"json-logic-js": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/json-logic-js/-/json-logic-js-2.0.0.tgz",
"integrity": "sha512-cQBDOXgFtFladCg99wnQ7YfN+nv1+Sznj4K6bp3CTgDJNJKgEXJE2VCXzVBjEU2e1UagDHSek52IQk5Ha38n7Q=="
},
"json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
@ -18397,23 +18403,6 @@
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=",
"dev": true
},
"jsonpath": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.0.tgz",
"integrity": "sha512-CZHwa1sZOf42qkIyK7evwToFXeTB4iplbl6Z9CVwU0wmBQPffL6gtYJXCoeciJoZENMuzaidPjhp2iOLRei4wQ==",
"requires": {
"esprima": "1.2.2",
"static-eval": "2.0.2",
"underscore": "1.7.0"
},
"dependencies": {
"esprima": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz",
"integrity": "sha1-dqD9Zvz+FU/SkmZ9wmQBl1CxZXs="
}
}
},
"jsonpointer": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.1.0.tgz",

@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "3.11.0",
"version": "3.12.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js",
"scripts": {
@ -40,8 +40,8 @@
"error-stack-parser": "^2.0.2",
"form-data": "^2.5.0",
"jest-config": "^26.6.3",
"json-logic-js": "^2.0.0",
"js-cookie": "^2.2.0",
"jsonpath": "^1.1.0",
"platform": "^1.3.5",
"quickhull": "^1.0.3",
"store": "^2.0.12",

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -213,13 +213,9 @@
visible.data.push(stateData);
}
const [, query] = this.annotationsFilter.toJSONQuery(filters);
let filtered = [];
if (filters.length) {
filtered = this.annotationsFilter.filter(visible.data, query);
}
const objectStates = [];
const filtered = this.annotationsFilter.filter(visible.data, filters);
visible.data.forEach((stateData, idx) => {
if (!filters.length || filtered.includes(stateData.clientID)) {
const model = visible.models[idx];
@ -777,6 +773,7 @@
}
// Add constructed objects to a collection
// eslint-disable-next-line no-unsanitized/method
const imported = this.import(constructed);
const importedArray = imported.tags.concat(imported.tracks).concat(imported.shapes);
@ -865,13 +862,9 @@
}
search(filters, frameFrom, frameTo) {
const [groups, query] = this.annotationsFilter.toJSONQuery(filters);
const sign = Math.sign(frameTo - frameFrom);
const flattenedQuery = groups.flat(Number.MAX_SAFE_INTEGER);
const containsDifficultProperties = flattenedQuery.some(
(fragment) => fragment.match(/^width/) || fragment.match(/^height/),
);
const filtersStr = JSON.stringify(filters);
const containsDifficultProperties = filtersStr.match(/"var":"width"/) || filtersStr.match(/"var":"height"/);
const deepSearch = (deepSearchFrom, deepSearchTo) => {
// deepSearchFrom is expected to be a frame that doesn't satisfy a filter
@ -882,7 +875,7 @@
while (!(Math.abs(prev - next) === 1)) {
const middle = next + Math.floor((prev - next) / 2);
const shapesData = this.tracks.map((track) => track.get(middle));
const filtered = this.annotationsFilter.filter(shapesData, query);
const filtered = this.annotationsFilter.filter(shapesData, filters);
if (filtered.length) {
next = middle;
} else {
@ -919,7 +912,7 @@
}
// Filtering
const filtered = this.annotationsFilter.filter(statesData, query);
const filtered = this.annotationsFilter.filter(statesData, filters);
// Now we are checking whether we need deep search or not
// Deep search is needed in some difficult cases

@ -1,143 +1,15 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
const jsonpath = require('jsonpath');
const jsonLogic = require('json-logic-js');
const { AttributeType, ObjectType } = require('./enums');
const { ArgumentError } = require('./exceptions');
class AnnotationsFilter {
constructor() {
// eslint-disable-next-line security/detect-unsafe-regex
this.operatorRegex = /(==|!=|<=|>=|>|<)(?=(?:[^"]*(["])[^"]*\2)*[^"]*$)/g;
}
// Method splits expression by operators that are outside of any brackets
_splitWithOperator(container, expression) {
const operators = ['|', '&'];
const splitted = [];
let nestedCounter = 0;
let isQuotes = false;
let start = -1;
for (let i = 0; i < expression.length; i++) {
if (expression[i] === '"') {
// all quotes inside other quotes must
// be escaped by a user and changed to ` above
isQuotes = !isQuotes;
}
// We don't split with operator inside brackets
// It will be done later in recursive call
if (!isQuotes && expression[i] === '(') {
nestedCounter++;
}
if (!isQuotes && expression[i] === ')') {
nestedCounter--;
}
if (operators.includes(expression[i])) {
if (!nestedCounter) {
const subexpression = expression.substr(start + 1, i - start - 1).trim();
splitted.push(subexpression);
splitted.push(expression[i]);
start = i;
}
}
}
const subexpression = expression.substr(start + 1).trim();
splitted.push(subexpression);
splitted.forEach((internalExpression) => {
if (internalExpression === '|' || internalExpression === '&') {
container.push(internalExpression);
} else {
this._groupByBrackets(container, internalExpression);
}
});
}
// Method groups bracket containings to nested arrays of container
_groupByBrackets(container, expression) {
if (!(expression.startsWith('(') && expression.endsWith(')'))) {
container.push(expression);
}
let nestedCounter = 0;
let startBracket = null;
let endBracket = null;
let isQuotes = false;
for (let i = 0; i < expression.length; i++) {
if (expression[i] === '"') {
// all quotes inside other quotes must
// be escaped by a user and changed to ` above
isQuotes = !isQuotes;
}
if (!isQuotes && expression[i] === '(') {
nestedCounter++;
if (startBracket === null) {
startBracket = i;
}
}
if (!isQuotes && expression[i] === ')') {
nestedCounter--;
if (!nestedCounter) {
endBracket = i;
const subcontainer = [];
const subexpression = expression.substr(startBracket + 1, endBracket - 1 - startBracket);
this._splitWithOperator(subcontainer, subexpression);
container.push(subcontainer);
startBracket = null;
endBracket = null;
}
}
}
if (startBracket !== null) {
throw Error('Extra opening bracket found');
}
if (endBracket !== null) {
throw Error('Extra closing bracket found');
}
}
_parse(expression) {
const groups = [];
this._splitWithOperator(groups, expression);
}
_join(groups) {
let expression = '';
for (const group of groups) {
if (Array.isArray(group)) {
expression += `(${this._join(group)})`;
} else if (typeof group === 'string') {
// it can be operator or expression
if (group === '|' || group === '&') {
expression += group;
} else {
let [field, operator, , value] = group.split(this.operatorRegex);
field = `@.${field.trim()}`;
operator = operator.trim();
value = value.trim();
if (value === 'width' || value === 'height' || value.startsWith('attr')) {
value = `@.${value}`;
}
expression += [field, operator, value].join('');
}
}
}
return expression;
}
function adjustName(name) {
return name.replaceAll('.', '\u2219');
}
class AnnotationsFilter {
_convertObjects(statesData) {
const objects = statesData.map((state) => {
const labelAttributes = state.label.attributes.reduce((acc, attr) => {
@ -169,63 +41,38 @@ class AnnotationsFilter {
const attributes = {};
Object.keys(state.attributes).reduce((acc, key) => {
const attr = labelAttributes[key];
let value = state.attributes[key].replace(/\\"/g, '`');
let value = state.attributes[key];
if (attr.inputType === AttributeType.NUMBER) {
value = +value;
} else if (attr.inputType === AttributeType.CHECKBOX) {
value = value === 'true';
}
acc[attr.name] = value;
acc[adjustName(attr.name)] = value;
return acc;
}, attributes);
return {
width,
height,
attr: attributes,
label: state.label.name.replace(/\\"/g, '`'),
attr: Object.fromEntries([[adjustName(state.label.name), attributes]]),
label: state.label.name,
serverID: state.serverID,
clientID: state.clientID,
objectID: state.clientID,
type: state.objectType,
shape: state.shapeType,
occluded: state.occluded,
};
});
return {
objects,
};
}
toJSONQuery(filters) {
try {
if (!Array.isArray(filters) || filters.some((value) => typeof value !== 'string')) {
throw Error('Argument must be an array of strings');
}
if (!filters.length) {
return [[], '$.objects[*].clientID'];
}
const groups = [];
const expression = filters
.map((filter) => `(${filter})`)
.join('|')
.replace(/\\"/g, '`');
this._splitWithOperator(groups, expression);
return [groups, `$.objects[?(${this._join(groups)})].clientID`];
} catch (error) {
throw new ArgumentError(`Wrong filter expression. ${error.toString()}`);
}
return objects;
}
filter(statesData, query) {
try {
const objects = this._convertObjects(statesData);
return jsonpath.query(objects, query);
} catch (error) {
throw new ArgumentError(`Could not apply the filter. ${error.toString()}`);
}
filter(statesData, filters) {
if (!filters.length) return statesData;
const converted = this._convertObjects(statesData);
return converted
.map((state) => state.objectID)
.filter((_, index) => jsonLogic.apply(filters[0], converted[index]));
}
}

@ -379,7 +379,7 @@
* @param {integer} frame get objects from the frame
* @param {boolean} allTracks show all tracks
* even if they are outside and not keyframe
* @param {string[]} [filters = []]
* @param {any[]} [filters = []]
* get only objects that satisfied to specific filters
* @returns {module:API.cvat.classes.ObjectState[]}
* @memberof Session.annotations
@ -1743,8 +1743,8 @@
// TODO: Check filter for annotations
Job.prototype.annotations.get.implementation = async function (frame, allTracks, filters) {
if (!Array.isArray(filters) || filters.some((filter) => typeof filter !== 'string')) {
throw new ArgumentError('The filters argument must be an array of strings');
if (!Array.isArray(filters)) {
throw new ArgumentError('Filters must be an array');
}
if (!Number.isInteger(frame)) {
@ -1760,8 +1760,8 @@
};
Job.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo) {
if (!Array.isArray(filters) || filters.some((filter) => typeof filter !== 'string')) {
throw new ArgumentError('The filters argument must be an array of strings');
if (!Array.isArray(filters)) {
throw new ArgumentError('Filters must be an array');
}
if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) {

@ -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,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.15.5",
"version": "1.16.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -1094,6 +1094,19 @@
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.4.0.tgz",
"integrity": "sha512-JZButFdZ1+/xAfpguQHoabIXkcqRRKpMrWKBkpEZZyxfY9C1DpADFB8PEqGSTeFr135SaTRfKqGKx5xSCLI7ZQ=="
},
"@date-io/core": {
"version": "1.3.13",
"resolved": "https://registry.npmjs.org/@date-io/core/-/core-1.3.13.tgz",
"integrity": "sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA=="
},
"@date-io/moment": {
"version": "1.3.13",
"resolved": "https://registry.npmjs.org/@date-io/moment/-/moment-1.3.13.tgz",
"integrity": "sha512-3kJYusJtQuOIxq6byZlzAHoW/18iExJer9qfRF5DyyzdAk074seTuJfdofjz4RFfTd/Idk8WylOQpWtERqvFuQ==",
"requires": {
"@date-io/core": "^1.3.13"
}
},
"@eslint/eslintrc": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.3.tgz",
@ -3382,6 +3395,11 @@
}
}
},
"clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18="
},
"clone-deep": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
@ -12923,7 +12941,7 @@
"form-data": "^2.5.0",
"jest-config": "^26.6.3",
"js-cookie": "^2.2.0",
"jsonpath": "^1.1.0",
"json-logic-js": "^2.0.0",
"platform": "^1.3.5",
"quickhull": "^1.0.3",
"store": "^2.0.12",
@ -15410,7 +15428,8 @@
"cvat-data": {
"version": "file:../cvat-data",
"requires": {
"jszip": "3.5.0"
"async-mutex": "^0.3.1",
"jszip": "3.6.0"
},
"dependencies": {
"ajv": {
@ -15430,11 +15449,11 @@
"integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ=="
},
"async-mutex": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.2.6.tgz",
"integrity": "sha512-Hs4R+4SPgamu6rSGW8C7cV9gaWUKEHykfzCCvIRuaVv636Ju10ZdeUbvb4TBEW0INuq2DHZqXbK4Nd3yG4RaRw==",
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.1.tgz",
"integrity": "sha512-vRfQwcqBnJTLzVQo72Sf7KIUbcSUP5hNchx6udI1U6LuPQpfePgdjJzlCe76yFZ8pxlLjn9lwcl/Ya0TSOv0Tw==",
"requires": {
"tslib": "^2.0.0"
"tslib": "^2.1.0"
}
},
"big.js": {
@ -15491,9 +15510,9 @@
}
},
"jszip": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.5.0.tgz",
"integrity": "sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.6.0.tgz",
"integrity": "sha512-jgnQoG9LKnWO3mnVNBnfhkh0QknICd1FGSrXcgrl67zioyJ4wgx25o9ZqwNtrROSflGBCGYnJfjrIyRIby1OoQ==",
"requires": {
"lie": "~3.3.0",
"pako": "~1.0.2",
@ -18357,6 +18376,11 @@
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA=="
},
"json-logic-js": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/json-logic-js/-/json-logic-js-2.0.0.tgz",
"integrity": "sha512-cQBDOXgFtFladCg99wnQ7YfN+nv1+Sznj4K6bp3CTgDJNJKgEXJE2VCXzVBjEU2e1UagDHSek52IQk5Ha38n7Q=="
},
"json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
@ -24749,6 +24773,11 @@
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
"dev": true
},
"immutable": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
"integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM="
},
"import-cwd": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
@ -28860,6 +28889,23 @@
"prop-types": "^15.6.2"
}
},
"react-awesome-query-builder": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/react-awesome-query-builder/-/react-awesome-query-builder-3.0.0.tgz",
"integrity": "sha512-EWnbqbLZUqpR60fypLQBuko++2JOQkDzqtszZlVWC6IXIyrYM2GOgP+YdQod01SKIV1QWNCyUWjQlTOCVt96FA==",
"requires": {
"@date-io/moment": "^1.3.13",
"classnames": "^2.2.5",
"clone": "^2.1.1",
"immutable": "^3.7.6",
"lodash": "^4.17.20",
"moment": "^2.28.0",
"prop-types": "^15.6.0",
"react-redux": "^7.2.1",
"redux": "^4.0.5",
"sqlstring": "^2.3.1"
}
},
"react-color": {
"version": "2.19.3",
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz",
@ -30324,6 +30370,11 @@
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
"dev": true
},
"sqlstring": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.2.tgz",
"integrity": "sha512-vF4ZbYdKS8OnoJAWBmMxCQDkiEBkGQYU7UZPtL8flbDRSNkhaXvRJ279ZtI6M+zDaQovVU4tuRgzK5fVhvFAhg=="
},
"sshpk": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",

@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.15.5",
"version": "1.16.0",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
@ -73,6 +73,7 @@
"platform": "^1.3.6",
"prop-types": "^15.7.2",
"react": "^16.14.0",
"react-awesome-query-builder": "^3.0.0",
"react-color": "^2.19.3",
"react-cookie": "^4.0.3",
"react-dom": "^16.14.0",

@ -4,30 +4,28 @@
import { MutableRefObject } from 'react';
import {
AnyAction, Dispatch, ActionCreator, Store,
ActionCreator, AnyAction, Dispatch, Store,
} from 'redux';
import { ThunkAction } from 'utils/redux';
import { RectDrawingMethod } from 'cvat-canvas-wrapper';
import getCore from 'cvat-core-wrapper';
import logger, { LogType } from 'cvat-logger';
import { getCVATStore } from 'cvat-store';
import {
CombinedState,
ActiveControl,
ShapeType,
ObjectType,
Task,
FrameSpeed,
Rotation,
CombinedState,
ContextMenuType,
Workspace,
Model,
DimensionType,
FrameSpeed,
Model,
ObjectType,
OpenCVTool,
Rotation,
ShapeType,
Task,
Workspace,
} from 'reducers/interfaces';
import getCore from 'cvat-core-wrapper';
import logger, { LogType } from 'cvat-logger';
import { RectDrawingMethod } from 'cvat-canvas-wrapper';
import { getCVATStore } from 'cvat-store';
interface AnnotationsParameters {
filters: string[];
frame: number;
@ -161,6 +159,7 @@ export enum AnnotationActionTypes {
PROPAGATE_OBJECT_FAILED = 'PROPAGATE_OBJECT_FAILED',
CHANGE_PROPAGATE_FRAMES = 'CHANGE_PROPAGATE_FRAMES',
SWITCH_SHOWING_STATISTICS = 'SWITCH_SHOWING_STATISTICS',
SWITCH_SHOWING_FILTERS = 'SWITCH_SHOWING_FILTERS',
COLLECT_STATISTICS = 'COLLECT_STATISTICS',
COLLECT_STATISTICS_SUCCESS = 'COLLECT_STATISTICS_SUCCESS',
COLLECT_STATISTICS_FAILED = 'COLLECT_STATISTICS_FAILED',
@ -276,24 +275,10 @@ export function fetchAnnotationsAsync(): ThunkAction {
};
}
export function changeAnnotationsFilters(filters: string[]): AnyAction {
const state: CombinedState = getStore().getState();
const { filtersHistory, filters: oldFilters } = state.annotation.annotations;
filters.forEach((element: string) => {
if (!(filtersHistory.includes(element) || oldFilters.includes(element))) {
filtersHistory.push(element);
}
});
window.localStorage.setItem('filtersHistory', JSON.stringify(filtersHistory.slice(-10)));
export function changeAnnotationsFilters(filters: any[]): AnyAction {
return {
type: AnnotationActionTypes.CHANGE_ANNOTATIONS_FILTERS,
payload: {
filters,
filtersHistory: filtersHistory.slice(-10),
},
payload: { filters },
};
}
@ -443,6 +428,14 @@ export function showStatistics(visible: boolean): AnyAction {
},
};
}
export function showFilters(visible: boolean): AnyAction {
return {
type: AnnotationActionTypes.SWITCH_SHOWING_FILTERS,
payload: {
visible,
},
};
}
export function propagateObjectAsync(sessionInstance: any, objectState: any, from: number, to: number): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
@ -876,7 +869,7 @@ export function closeJob(): ThunkAction {
};
}
export function getJobAsync(tid: number, jid: number, initialFrame: number, initialFilters: string[]): ThunkAction {
export function getJobAsync(tid: number, jid: number, initialFrame: number, initialFilters: object[]): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const state: CombinedState = getStore().getState();

@ -2,25 +2,26 @@
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React, { useEffect } from 'react';
import { useHistory } from 'react-router';
import Layout from 'antd/lib/layout';
import Spin from 'antd/lib/spin';
import Result from 'antd/lib/result';
import Spin from 'antd/lib/spin';
import notification from 'antd/lib/notification';
import { Workspace } from 'reducers/interfaces';
import { usePrevious } from 'utils/hooks';
import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar';
import StatisticsModalContainer from 'containers/annotation-page/top-bar/statistics-modal';
import StandardWorkspaceComponent from 'components/annotation-page/standard-workspace/standard-workspace';
import AttributeAnnotationWorkspace from 'components/annotation-page/attribute-annotation-workspace/attribute-annotation-workspace';
import TagAnnotationWorkspace from 'components/annotation-page/tag-annotation-workspace/tag-annotation-workspace';
import ReviewAnnotationsWorkspace from 'components/annotation-page/review-workspace/review-workspace';
import SubmitAnnotationsModal from 'components/annotation-page/request-review-modal';
import ReviewAnnotationsWorkspace from 'components/annotation-page/review-workspace/review-workspace';
import SubmitReviewModal from 'components/annotation-page/review/submit-review-modal';
import StandardWorkspaceComponent from 'components/annotation-page/standard-workspace/standard-workspace';
import StandardWorkspace3DComponent from 'components/annotation-page/standard3D-workspace/standard3D-workspace';
import TagAnnotationWorkspace from 'components/annotation-page/tag-annotation-workspace/tag-annotation-workspace';
import FiltersModalContainer from 'containers/annotation-page/top-bar/filters-modal';
import StatisticsModalContainer from 'containers/annotation-page/top-bar/statistics-modal';
import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar';
import { Workspace } from 'reducers/interfaces';
import { usePrevious } from 'utils/hooks';
import './styles.scss';
interface Props {
job: any | null | undefined;
@ -130,6 +131,7 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
<ReviewAnnotationsWorkspace />
</Layout.Content>
)}
<FiltersModalContainer visible={false} />
<StatisticsModalContainer />
<SubmitAnnotationsModal />
<SubmitReviewModal />

@ -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>
==, !=, &gt;, &gt;=, &lt;, &lt;=, (), &amp; and |
<br />
<Text strong>
If you have double quotes in your query string, please escape them using back slash: \&quot; (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==&quot;car&quot; | label==[&quot;road sign&quot;]</li>
<li>shape == &quot;polygon&quot;</li>
<li>width &gt;= height</li>
<li>attr[&quot;Attribute 1&quot;] == attr[&quot;Attribute 2&quot;]</li>
<li>clientID == 50</li>
<li>
(label==&quot;car&quot; &amp; attr[&quot;parked&quot;]==true) | (label==&quot;pedestrian&quot;
&amp; width &gt; 150)
</li>
<li>
(( label==[&quot;car \&quot;mazda\&quot;&quot;]) &amp; (attr[&quot;sunglasses&quot;]==true |
(width &gt; 150 | height &gt; 150 &amp; (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);

@ -2,32 +2,29 @@
//
// SPDX-License-Identifier: MIT
import React, { useState, useEffect } from 'react';
import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import Layout, { SiderProps } from 'antd/lib/layout';
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
import { SelectValue } from 'antd/lib/select';
import { Row, Col } from 'antd/lib/grid';
import Layout, { SiderProps } from 'antd/lib/layout';
import Text from 'antd/lib/typography/Text';
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
import { ThunkDispatch } from 'utils/redux';
import { Canvas } from 'cvat-canvas-wrapper';
import { LogType } from 'cvat-logger';
import {
activateObject as activateObjectAction,
updateAnnotationsAsync,
changeFrameAsync,
updateAnnotationsAsync,
} from 'actions/annotation-actions';
import { CombinedState, ObjectType } from 'reducers/interfaces';
import AnnotationsFiltersInput from 'components/annotation-page/annotations-filters-input';
import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
import { ThunkDispatch } from 'utils/redux';
import AppearanceBlock from 'components/annotation-page/appearance-block';
import ObjectButtonsContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-buttons';
import ObjectSwitcher from './object-switcher';
import { CombinedState, ObjectType } from 'reducers/interfaces';
import AttributeEditor from './attribute-editor';
import AttributeSwitcher from './attribute-switcher';
import ObjectBasicsEditor from './object-basics-edtior';
import AttributeEditor from './attribute-editor';
import ObjectSwitcher from './object-switcher';
interface StateToProps {
activatedStateID: number | null;
@ -296,11 +293,7 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.
{sidebarCollapsed ? <MenuFoldOutlined title='Show' /> : <MenuUnfoldOutlined title='Hide' />}
</span>
<GlobalHotKeys keyMap={subKeyMap} handlers={handlers} />
<Row>
<Col span={24}>
<AnnotationsFiltersInput />
</Col>
</Row>
<div className='cvat-sidebar-collapse-button-spacer' />
<ObjectSwitcher
currentLabel={activeObjectState.label.name}
clientID={activeObjectState.clientID}
@ -375,11 +368,7 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.
>
{sidebarCollapsed ? <MenuFoldOutlined title='Show' /> : <MenuUnfoldOutlined title='Hide' />}
</span>
<Row>
<Col span={24}>
<AnnotationsFiltersInput />
</Col>
</Row>
<div className='cvat-sidebar-collapse-button-spacer' />
<div className='attribute-annotations-sidebar-not-found-wrapper'>
<Text strong>No objects found</Text>
</div>

@ -63,3 +63,7 @@
align-items: center;
justify-content: space-around;
}
.cvat-sidebar-collapse-button-spacer {
height: 32px;
}

@ -231,7 +231,7 @@ export default function ItemMenu(props: Props): JSX.Element {
} = props;
return (
<Menu className='cvat-object-item-menu'>
<Menu className='cvat-object-item-menu' selectable={false}>
<CreateURLItem toolProps={props} />
{!readonly && <MakeCopyItem toolProps={props} />}
{!readonly && <PropagateItem toolProps={props} />}

@ -3,19 +3,18 @@
// SPDX-License-Identifier: MIT
import React from 'react';
import { Row, Col } from 'antd/lib/grid';
import {
LockFilled,
UnlockOutlined,
EyeInvisibleFilled,
EyeOutlined,
CaretDownOutlined,
CaretUpFilled,
EyeInvisibleFilled,
EyeOutlined,
LockFilled,
UnlockOutlined,
} from '@ant-design/icons';
import { Col, Row } from 'antd/lib/grid';
import CVATTooltip from 'components/common/cvat-tooltip';
import AnnotationsFiltersInput from 'components/annotation-page/annotations-filters-input';
import StatesOrderingSelector from 'components/annotation-page/standard-workspace/objects-side-bar/states-ordering-selector';
import CVATTooltip from 'components/common/cvat-tooltip';
import { StatesOrdering } from 'reducers/interfaces';
interface Props {
@ -85,11 +84,6 @@ function ObjectListHeader(props: Props): JSX.Element {
return (
<div className='cvat-objects-sidebar-states-header'>
<Row>
<Col span={24}>
<AnnotationsFiltersInput />
</Col>
</Row>
<Row justify='space-between' align='middle'>
{!readonly && (
<>

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -18,7 +18,7 @@ function StatesOrderingSelectorComponent(props: StatesOrderingSelectorComponentP
const { statesOrdering, changeStatesOrdering } = props;
return (
<Col span={16}>
<Col>
<Text strong>Sort by</Text>
<Select
className='cvat-objects-sidebar-ordering-selector'

@ -130,13 +130,8 @@
.cvat-objects-sidebar-states-header {
background: $objects-bar-tabs-color;
padding: 5px;
height: 80px;
> div:nth-child(1) > div:nth-child(1) {
height: 32px;
}
> div:nth-child(2) {
> div:nth-child(1) {
margin-top: $grid-unit-size;
> div {
@ -161,7 +156,7 @@
.cvat-objects-sidebar-states-list {
background-color: $background-color-2;
height: calc(100% - 80px);
height: calc(100% - 50px);
overflow-y: auto;
}

@ -18,9 +18,17 @@
border-bottom: 1px solid $border-color-1;
height: 54px;
padding: 0;
> div:first-child {
display: flex;
flex: 1;
flex-flow: unset;
}
}
.cvat-annotation-header-left-group {
display: flex;
> button:first-child {
filter: invert(0.9);
background: $background-color-1;
@ -33,7 +41,6 @@
padding: 0;
width: 54px;
height: 54px;
float: left;
text-align: center;
user-select: none;
color: $text-color;
@ -64,6 +71,14 @@
display: block;
line-height: 0;
}
&.filters-armed {
color: $info-icon-color;
path {
fill: $info-icon-color;
}
}
}
.cvat-annotation-disabled-header-button {
@ -76,6 +91,7 @@
.cvat-annotation-header-player-group > div {
height: 54px;
line-height: 0;
flex-wrap: nowrap;
}
.cvat-player-buttons {
@ -146,8 +162,10 @@
}
.cvat-annotation-header-right-group {
display: flex;
justify-content: flex-end;
> div {
float: left;
display: block;
height: 54px;
margin-right: 15px;
@ -407,3 +425,28 @@
}
}
}
.cvat-filters-modal {
.ant-modal-body {
padding: 1px;
.recently-used-wrapper {
padding-top: $grid-unit-size * 2;
}
.query-builder-container {
.query-builder > :first-child {
padding-right: 0;
}
.group-or-rule {
border-radius: $grid-unit-size / 4;
}
.group {
background: rgba(216, 233, 250, 0.5);
border: 1px solid #d3e0ec;
}
}
}
}

@ -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>
);
}

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -7,22 +7,26 @@ import { Col } from 'antd/lib/grid';
import Icon from '@ant-design/icons';
import Select from 'antd/lib/select';
import Button from 'antd/lib/button';
import { useSelector } from 'react-redux';
import { DimensionType, Workspace } from 'reducers/interfaces';
import { InfoIcon, FullscreenIcon } from 'icons';
import { FilterIcon, FullscreenIcon, InfoIcon } from 'icons';
import { CombinedState, DimensionType, Workspace } from 'reducers/interfaces';
interface Props {
workspace: Workspace;
showStatistics(): void;
showFilters(): void;
changeWorkspace(workspace: Workspace): void;
jobInstance: any;
}
function RightGroup(props: Props): JSX.Element {
const {
showStatistics, changeWorkspace, workspace, jobInstance,
showFilters, showStatistics, changeWorkspace, workspace, jobInstance,
} = props;
const filters = useSelector((state: CombinedState) => state.annotation.annotations.filters);
return (
<Col className='cvat-annotation-header-right-group'>
<Button
@ -45,6 +49,14 @@ function RightGroup(props: Props): JSX.Element {
<Icon component={InfoIcon} />
Info
</Button>
<Button
type='link'
className={`cvat-annotation-header-button ${filters.length ? 'filters-armed' : ''}`}
onClick={showFilters}
>
<Icon component={FilterIcon} />
Filters
</Button>
<div>
<Select
dropdownClassName='cvat-workspace-selector-dropdown'

@ -3,15 +3,14 @@
// SPDX-License-Identifier: MIT
import React from 'react';
import { Row, Col } from 'antd/lib/grid';
import Input from 'antd/lib/input';
import { Col, Row } from 'antd/lib/grid';
import { Workspace } from 'reducers/interfaces';
import LeftGroup from './left-group';
import RightGroup from './right-group';
import PlayerNavigation from './player-navigation';
import PlayerButtons from './player-buttons';
import PlayerNavigation from './player-navigation';
import RightGroup from './right-group';
interface Props {
playing: boolean;
@ -38,6 +37,7 @@ interface Props {
focusFrameInputShortcut: string;
changeWorkspace(workspace: Workspace): void;
showStatistics(): void;
showFilters(): void;
onSwitchPlay(): void;
onSaveAnnotation(): void;
onPrevFrame(): void;
@ -82,6 +82,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
nextButtonType,
focusFrameInputShortcut,
showStatistics,
showFilters,
changeWorkspace,
onSwitchPlay,
onSaveAnnotation,
@ -154,6 +155,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
workspace={workspace}
changeWorkspace={changeWorkspace}
showStatistics={showStatistics}
showFilters={showFilters}
/>
</Row>
);

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -31,7 +31,7 @@ export default function LabelSelector(props: Props): JSX.Element {
showSearch
filterOption={(input: string, option?: OptionData | OptionGroupData) => {
if (option) {
const { children } = option;
const { children } = option.props;
if (typeof children === 'string') {
return children.toLowerCase().includes(input.toLowerCase());
}

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -50,7 +50,7 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps {
const taskID = +params.tid;
const jobID = +params.jid;
const searchParams = new URLSearchParams(window.location.search);
const initialFilters: string[] = [];
const initialFilters: object[] = [];
let initialFrame = 0;
if (searchParams.has('frame')) {
@ -64,7 +64,9 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps {
const serverID = searchParams.get('serverID');
const type = searchParams.get('type');
if (serverID && !Number.isNaN(+serverID)) {
initialFilters.push(`serverID==${serverID} & type=="${type}"`);
initialFilters.push({
and: [{ '==': [{ var: 'serverID' }, serverID] }, { '==': [{ var: 'type' }, type] }],
});
}
}

@ -34,13 +34,12 @@ interface StateToProps {
statesCollapsedAll: boolean;
collapsedStates: Record<number, boolean>;
objectStates: any[];
annotationsFilters: string[];
annotationsFilters: any[];
colors: string[];
colorBy: ColorBy;
activatedStateID: number | null;
minZLayer: number;
maxZLayer: number;
annotationsFiltersHistory: string[];
keyMap: KeyMap;
normalizedKeyMap: Record<string, string>;
canvasInstance: Canvas;
@ -62,7 +61,6 @@ function mapStateToProps(state: CombinedState): StateToProps {
annotations: {
states: objectStates,
filters: annotationsFilters,
filtersHistory: annotationsFiltersHistory,
collapsed,
collapsedAll,
activatedStateID,
@ -110,7 +108,6 @@ function mapStateToProps(state: CombinedState): StateToProps {
activatedStateID,
minZLayer,
maxZLayer,
annotationsFiltersHistory,
keyMap,
normalizedKeyMap,
canvasInstance,

@ -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);

@ -3,31 +3,30 @@
// SPDX-License-Identifier: MIT
import React from 'react';
import copy from 'copy-to-clipboard';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import { RouteComponentProps } from 'react-router-dom';
import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
import Input from 'antd/lib/input';
import copy from 'copy-to-clipboard';
import {
activateObject,
changeFrameAsync,
switchPlay,
saveAnnotationsAsync,
changeWorkspace as changeWorkspaceAction,
collectStatisticsAsync,
showStatistics as showStatisticsAction,
undoActionAsync,
redoActionAsync,
saveAnnotationsAsync,
searchAnnotationsAsync,
searchEmptyFrameAsync,
changeWorkspace as changeWorkspaceAction,
activateObject,
setForceExitAnnotationFlag as setForceExitAnnotationFlagAction,
showFilters as showFiltersAction,
showStatistics as showStatisticsAction,
switchPlay,
undoActionAsync,
} from 'actions/annotation-actions';
import { Canvas } from 'cvat-canvas-wrapper';
import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar';
import { Canvas } from 'cvat-canvas-wrapper';
import { CombinedState, FrameSpeed, Workspace } from 'reducers/interfaces';
import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
interface StateToProps {
jobInstance: any;
@ -56,6 +55,7 @@ interface DispatchToProps {
onSwitchPlay(playing: boolean): void;
onSaveAnnotation(sessionInstance: any): void;
showStatistics(sessionInstance: any): void;
showFilters(sessionInstance: any): void;
undo(sessionInstance: any, frameNumber: any): void;
redo(sessionInstance: any, frameNumber: any): void;
searchAnnotations(sessionInstance: any, frameFrom: number, frameTo: number): void;
@ -124,6 +124,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
dispatch(collectStatisticsAsync(sessionInstance));
dispatch(showStatisticsAction(true));
},
showFilters(): void {
dispatch(showFiltersAction(true));
},
undo(sessionInstance: any, frameNumber: any): void {
dispatch(undoActionAsync(sessionInstance, frameNumber));
},
@ -274,6 +277,12 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
showStatistics(jobInstance);
};
private showFilters = (): void => {
const { jobInstance, showFilters } = this.props;
showFilters(jobInstance);
};
private onSwitchPlay = (): void => {
const {
frameNumber, jobInstance, onSwitchPlay, playing,
@ -587,6 +596,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
<GlobalHotKeys keyMap={subKeyMap} handlers={handlers} />
<AnnotationTopBarComponent
showStatistics={this.showStatistics}
showFilters={this.showFilters}
onSwitchPlay={this.onSwitchPlay}
onSaveAnnotation={this.onSaveAnnotation}
onPrevFrame={this.onPrevFrame}

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -48,6 +48,7 @@ import SVGResetPerspectiveIcon from './assets/reset-perspective.svg';
import SVGColorizeIcon from './assets/colorize-icon.svg';
import SVGAITools from './assets/ai-tools-icon.svg';
import SVGOpenCV from './assets/opencv.svg';
import SVGFilterIcon from './assets/object-filter-icon.svg';
export const CVATLogo = React.memo((): JSX.Element => <SVGCVATLogo />);
export const AccountIcon = React.memo((): JSX.Element => <SVGAccountIcon />);
@ -93,3 +94,4 @@ export const ResetPerspectiveIcon = React.memo((): JSX.Element => <SVGResetPersp
export const AIToolsIcon = React.memo((): JSX.Element => <SVGAITools />);
export const ColorizeIcon = React.memo((): JSX.Element => <SVGColorizeIcon />);
export const OpenCVIcon = React.memo((): JSX.Element => <SVGOpenCV />);
export const FilterIcon = React.memo((): JSX.Element => <SVGFilterIcon />);

@ -4,21 +4,20 @@
import React from 'react';
import { AnyAction } from 'redux';
import { Canvas, CanvasMode } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper';
import { AnnotationActionTypes } from 'actions/annotation-actions';
import { AuthActionTypes } from 'actions/auth-actions';
import { BoundariesActionTypes } from 'actions/boundaries-actions';
import { Canvas, CanvasMode } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper';
import {
AnnotationState,
ActiveControl,
ShapeType,
ObjectType,
AnnotationState,
ContextMenuType,
Workspace,
TaskStatus,
DimensionType,
ObjectType,
ShapeType,
TaskStatus,
Workspace,
} from './interfaces';
const defaultState: AnnotationState = {
@ -81,7 +80,6 @@ const defaultState: AnnotationState = {
collapsedAll: true,
states: [],
filters: [],
filtersHistory: JSON.parse(window.localStorage.getItem('filtersHistory') || '[]'),
resetGroupFlag: false,
history: {
undo: [],
@ -106,6 +104,7 @@ const defaultState: AnnotationState = {
colors: [],
sidebarCollapsed: false,
appearanceCollapsed: false,
filtersPanelVisible: false,
requestReviewDialogVisible: false,
submitReviewDialogVisible: false,
tabContentHeight: 0,
@ -806,6 +805,14 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
},
};
}
case AnnotationActionTypes.SWITCH_SHOWING_FILTERS: {
const { visible } = action.payload;
return {
...state,
filtersPanelVisible: visible,
};
}
case AnnotationActionTypes.COLLECT_STATISTICS: {
return {
...state,
@ -976,13 +983,11 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
};
}
case AnnotationActionTypes.CHANGE_ANNOTATIONS_FILTERS: {
const { filters, filtersHistory } = action.payload;
const { filters } = action.payload;
return {
...state,
annotations: {
...state.annotations,
filtersHistory,
filters,
},
};

@ -3,8 +3,8 @@
// SPDX-License-Identifier: MIT
import { MutableRefObject } from 'react';
import { Canvas, RectDrawingMethod } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d/src/typescript/canvas3d';
import { Canvas, RectDrawingMethod } from 'cvat-canvas-wrapper';
import { IntelligentScissors } from 'utils/opencv-wrapper/intelligent-scissors';
import { KeyMap } from 'utils/mousetrap-react';
@ -428,8 +428,7 @@ export interface AnnotationState {
collapsed: Record<number, boolean>;
collapsedAll: boolean;
states: any[];
filters: string[];
filtersHistory: string[];
filters: any[];
resetGroupFlag: boolean;
history: {
undo: [string, number][];
@ -456,6 +455,7 @@ export interface AnnotationState {
data: any;
};
colors: any[];
filtersPanelVisible: boolean;
requestReviewDialogVisible: boolean;
submitReviewDialogVisible: boolean;
sidebarCollapsed: boolean;

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -129,6 +129,7 @@ context('Merge/split features', () => {
cy.get('.cvat-split-track-control').click();
// A single click does not reproduce the split a track scenario in cypress test.
cy.get('#cvat_canvas_shape_3').click().click();
cy.get('#cvat_canvas_shape_3').click(); // TODO: workaraund. Need to figure out and make it work with just single click
cy.get('#cvat_canvas_shape_4').should('exist').and('be.hidden');
cy.get('#cvat-objects-sidebar-state-item-4')
.should('contain', '4')

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -102,6 +102,15 @@ context('Lock/hide features.', () => {
cy.openJob();
});
beforeEach(() => {
cy.document().then((doc) => {
const tooltips = Array.from(doc.querySelectorAll('.ant-tooltip'));
if (tooltips.length > 0) {
cy.get('.ant-tooltip').invoke('hide');
}
});
});
describe(`Testing case "${caseId}"`, () => {
it('Draw several objects (different shapes, tracks, tags, labels)', () => {
cy.createPolygon(createPolygonShape);

@ -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]);
// });
// });
// });

@ -1,10 +1,10 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
/// <reference types="cypress" />
import { taskName, labelName } from '../../support/const';
import { labelName, taskName } from '../../support/const';
context('Filter property "shape" work correctly', () => {
const issueId = '1444';
@ -43,12 +43,13 @@ context('Filter property "shape" work correctly', () => {
cy.createPolygon(createPolygonShape);
cy.get('#cvat-objects-sidebar-state-item-2').should('contain', '2').and('contain', 'POLYGON SHAPE');
});
it('Input filter "shape == "polygon""', () => {
cy.get('.cvat-annotations-filters-input').type('shape == "polygon"{Enter}');
});
it('Only polygon is visible', () => {
cy.get('#cvat_canvas_shape_2').should('exist');
cy.get('#cvat_canvas_shape_1').should('not.exist');
});
// TODO: Update with new filters UI
// it('Input filter "shape == "polygon""', () => {
// cy.get('.cvat-annotations-filters-input').type('shape == "polygon"{Enter}');
// });
// it('Only polygon is visible', () => {
// cy.get('#cvat_canvas_shape_2').should('exist');
// cy.get('#cvat_canvas_shape_1').should('not.exist');
// });
});
});

@ -1,10 +1,10 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
/// <reference types="cypress" />
import { taskName, labelName } from '../../support/const';
import { labelName, taskName } from '../../support/const';
context('Navigation to empty frames', () => {
const issueId = '2485';
@ -33,10 +33,11 @@ context('Navigation to empty frames', () => {
cy.createRectangle(createRectangleShape2Points);
});
it('Input a filter to see the created objects.', () => {
cy.writeFilterValue(false, 'shape=="rectangle"');
cy.get('#cvat_canvas_shape_2').should('exist');
});
// TODO: Update with new filters UI
// it('Input a filter to see the created objects.', () => {
// cy.writeFilterValue(false, 'shape=="rectangle"');
// cy.get('#cvat_canvas_shape_2').should('exist');
// });
it('Go to 3rd frame.', () => {
cy.goCheckFrameNumber(3);
@ -50,21 +51,22 @@ context('Navigation to empty frames', () => {
}
});
it("Press go previous with a filter. CVAT get 2nd frame. Press again. Frame wasn't changed.", () => {
for (let i = 1; i <= 2; i++) {
cy.get('.cvat-player-previous-button-filtered').click({ force: true });
cy.checkFrameNum(2);
cy.get('#cvat_canvas_shape_1').should('exist');
}
});
// TODO: Update with new filters UI
// it("Press go previous with a filter. CVAT get 2nd frame. Press again. Frame wasn't changed.", () => {
// for (let i = 1; i <= 2; i++) {
// cy.get('.cvat-player-previous-button-filtered').click({ force: true });
// cy.checkFrameNum(2);
// cy.get('#cvat_canvas_shape_1').should('exist');
// }
// });
it("Press go next with a filter. CVAT get 4th frame. Press again. Frame wasn't changed.", () => {
for (let i = 1; i <= 2; i++) {
cy.get('.cvat-player-next-button-filtered').click({ force: true });
cy.checkFrameNum(4);
cy.get('#cvat_canvas_shape_2').should('exist');
}
});
// it("Press go next with a filter. CVAT get 4th frame. Press again. Frame wasn't changed.", () => {
// for (let i = 1; i <= 2; i++) {
// cy.get('.cvat-player-next-button-filtered').click({ force: true });
// cy.checkFrameNum(4);
// cy.get('#cvat_canvas_shape_2').should('exist');
// }
// });
it('Change navigation buttons mode to "Go next/previous to an empty frame".', () => {
for (const i of ['previous', 'next']) {
@ -73,16 +75,17 @@ context('Navigation to empty frames', () => {
}
});
it('Go previous to an empty frame. CVAT get 3rd frame.', () => {
cy.get('.cvat-player-previous-button-empty').click({ force: true });
cy.checkFrameNum(3);
cy.get('.cvat_canvas_shape').should('not.exist');
});
// TODO: Update with new filters UI
// it('Go previous to an empty frame. CVAT get 3rd frame.', () => {
// cy.get('.cvat-player-previous-button-empty').click({ force: true });
// cy.checkFrameNum(3);
// cy.get('.cvat_canvas_shape').should('not.exist');
// });
it('Go next to an empty frame. CVAT get 5th frame.', () => {
cy.get('.cvat-player-next-button-empty').click({ force: true });
cy.checkFrameNum(5);
cy.get('.cvat_canvas_shape').should('not.exist');
});
// it('Go next to an empty frame. CVAT get 5th frame.', () => {
// cy.get('.cvat-player-next-button-empty').click({ force: true });
// cy.checkFrameNum(5);
// cy.get('.cvat_canvas_shape').should('not.exist');
// });
});
});

@ -14,20 +14,20 @@ context('Annotation filter help dialog window.', () => {
});
describe(`Testing issue "${issueId}"`, () => {
it('Open annotation filters help dialog window. The window is visible.', () => {
cy.get('.cvat-annotations-filters-input').within(() => {
// class="ant-select-selection-placeholder" has CSS pointer-events: none
cy.get('.ant-select-selection-placeholder').invoke('css', 'pointer-events', 'auto'); // Replace CSS "pointer-events" to auto
cy.get('[aria-label="filter"]').click();
});
cy.get('.cvat-annotations-filters-help-modal-window').should('exist').and('be.visible');
});
it('Close annotation filters help dialog window. The window is closed.', () => {
cy.get('.cvat-annotations-filters-help-modal-window').within(() => {
cy.contains('button', 'OK').click();
});
cy.get('.cvat-annotations-filters-help-modal-window').should('not.exist');
});
// TODO: Update with new filters UI
// it('Open annotation filters help dialog window. The window is visible.', () => {
// cy.get('.cvat-annotations-filters-input').within(() => {
// // class="ant-select-selection-placeholder" has CSS pointer-events: none
// cy.get('.ant-select-selection-placeholder').invoke('css', 'pointer-events', 'auto'); // Replace CSS "pointer-events" to auto
// cy.get('[aria-label="filter"]').click();
// });
// cy.get('.cvat-annotations-filters-help-modal-window').should('exist').and('be.visible');
// });
// it('Close annotation filters help dialog window. The window is closed.', () => {
// cy.get('.cvat-annotations-filters-help-modal-window').within(() => {
// cy.contains('button', 'OK').click();
// });
// cy.get('.cvat-annotations-filters-help-modal-window').should('not.exist');
// });
});
});

Loading…
Cancel
Save