From 777a3dd15fb20bc7f6b345aaeb689c45b9517a68 Mon Sep 17 00:00:00 2001 From: Surat Das Date: Fri, 18 Jun 2021 14:01:55 -0700 Subject: [PATCH 1/3] Issue #276: Enable to bulk clear/apply ignore area for test runs --- src/components/TestRunList/BulkOperation.tsx | 109 +++++++++++++++---- src/services/testRun.service.ts | 13 +++ 2 files changed, 103 insertions(+), 19 deletions(-) diff --git a/src/components/TestRunList/BulkOperation.tsx b/src/components/TestRunList/BulkOperation.tsx index 5ac55650..b563c06b 100644 --- a/src/components/TestRunList/BulkOperation.tsx +++ b/src/components/TestRunList/BulkOperation.tsx @@ -1,11 +1,12 @@ import React from "react"; -import { Typography, IconButton, Tooltip } from "@material-ui/core"; -import { BaseComponentProps, RowModel } from "@material-ui/data-grid"; +import { Typography, IconButton, Tooltip, LinearProgress } from "@material-ui/core"; +import { BaseComponentProps, InternalRowsState, RowModel } from "@material-ui/data-grid"; import { BaseModal } from "../BaseModal"; import { useSnackbar } from "notistack"; -import { Delete, ThumbDown, ThumbUp } from "@material-ui/icons"; +import { Collections, Delete, LayersClear, ThumbDown, ThumbUp } from "@material-ui/icons"; import { testRunService } from "../../services"; import { TestStatus } from "../../types"; +import { IgnoreArea } from "../../types/ignoreArea"; export const BulkOperation: React.FunctionComponent = ( props: BaseComponentProps @@ -14,9 +15,13 @@ export const BulkOperation: React.FunctionComponent = ( const [approveDialogOpen, setApproveDialogOpen] = React.useState(false); const [rejectDialogOpen, setRejectDialogOpen] = React.useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false); + const [applyIgnoreDialogOpen, setApplyIgnoreDialogOpen] = React.useState(false); + const [clearIgnoreDialogOpen, setClearIgnoreDialogOpen] = React.useState(false); + const [isProcessing, setIsProcessing] = React.useState(false); - const rows: Record = props.state.selection; - const count = Object.keys(rows).length; + const allRows: InternalRowsState = props.state.rows; + const selectedRows: Record = props.state.selection; + const count = Object.keys(selectedRows).length; const toggleApproveDialogOpen = () => { setApproveDialogOpen(!approveDialogOpen); @@ -27,21 +32,39 @@ export const BulkOperation: React.FunctionComponent = ( const toggleDeleteDialogOpen = () => { setDeleteDialogOpen(!deleteDialogOpen); }; + const toggleApplyIgnoreDialogOpen = () => { + setApplyIgnoreDialogOpen(!applyIgnoreDialogOpen); + }; + const toggleClearIgnoreDialogOpen = () => { + setClearIgnoreDialogOpen(!clearIgnoreDialogOpen); + }; const getTitle = () => { + if (applyIgnoreDialogOpen) { + return "Apply Selected Ignore Area To All Images"; + } + if (clearIgnoreDialogOpen) { + return "Clear Ignore Area For Selected Items"; + } return submitButtonText() + " Test Runs"; }; const submitButtonText = (): string => { - if (deleteDialogOpen) { - return "Delete"; - } if (approveDialogOpen) { return "Approve"; } if (rejectDialogOpen) { return "Reject"; } + if (deleteDialogOpen) { + return "Delete"; + } + if (applyIgnoreDialogOpen) { + return "Apply"; + } + if (clearIgnoreDialogOpen) { + return "Clear"; + } return ""; }; @@ -55,6 +78,12 @@ export const BulkOperation: React.FunctionComponent = ( if (rejectDialogOpen) { return toggleRejectDialogOpen(); } + if (applyIgnoreDialogOpen) { + return toggleApplyIgnoreDialogOpen(); + } + if (clearIgnoreDialogOpen) { + return toggleClearIgnoreDialogOpen(); + } }; const isRowEligibleForApproveOrReject = (id: string) => { @@ -72,6 +101,23 @@ export const BulkOperation: React.FunctionComponent = ( if (isRowEligibleForApproveOrReject(id)) { processApproveReject(id); } + if (clearIgnoreDialogOpen) { + testRunService.setIgnoreAreas(id, []); + } + if (applyIgnoreDialogOpen) { + const testRun = testRunService.getTestRunDetails(id); + let runningNumber = 0; + allRows.allRows.forEach(async (eachRow) => { + const ignoreAreaToSet: IgnoreArea[] = JSON.parse((await testRun).ignoreAreas); + if (ignoreAreaToSet.length > 0) { + //Add a running number to make id unique. + ignoreAreaToSet.forEach((e) => { + e.id = (Date.now() + (++runningNumber)).toString().slice(0, 13); + }); + testRunService.setIgnoreAreas(eachRow.toString(), ignoreAreaToSet); + } + }); + } }; const processApproveReject = (id: string) => { @@ -90,9 +136,22 @@ export const BulkOperation: React.FunctionComponent = ( if (approveDialogOpen) { return toggleApproveDialogOpen(); } + if (applyIgnoreDialogOpen) { + return toggleApplyIgnoreDialogOpen(); + } + if (clearIgnoreDialogOpen) { + return toggleClearIgnoreDialogOpen(); + } return toggleRejectDialogOpen(); }; + const getProcessSuccessMessage = () => { + if (applyIgnoreDialogOpen) { + return "Selected image ignore area has been applied to all images."; + } + return `${count} test runs processed.`; + }; + return ( <> @@ -116,28 +175,39 @@ export const BulkOperation: React.FunctionComponent = ( + + + + + + + + + + + + + + {`Are you sure you want to ${submitButtonText().toLowerCase()} ${count} items?`} + {applyIgnoreDialogOpen + ? `Are you sure you want to apply ignore area to all images? Works well if images are of same resolution.` + : `Are you sure you want to ${submitButtonText().toLowerCase()} ${count} items?`} } onSubmit={() => { - enqueueSnackbar( - "Wait for the confirmation message until operation is completed.", - { - variant: "info", - } - ); - + setIsProcessing(true); Promise.all( - Object.keys(rows).map((id: string) => processAction(id)) + Object.keys(selectedRows).map((id: string) => processAction(id)) ) .then(() => { - enqueueSnackbar(`${count} test runs processed.`, { + setIsProcessing(false); + enqueueSnackbar(getProcessSuccessMessage(), { variant: "success", }); }) @@ -149,6 +219,7 @@ export const BulkOperation: React.FunctionComponent = ( closeModal(); }} /> + { isProcessing && } ); }; diff --git a/src/services/testRun.service.ts b/src/services/testRun.service.ts index 93062102..a3ef9a8a 100644 --- a/src/services/testRun.service.ts +++ b/src/services/testRun.service.ts @@ -67,6 +67,18 @@ async function setIgnoreAreas( ).then(handleResponse); } +async function getTestRunDetails(id: string): Promise { + const requestOptions = { + method: "GET", + headers: authHeader(), + }; + + return fetch( + `${API_URL}${ENDPOINT_URL}/${id}`, + requestOptions + ).then(handleResponse); +} + async function setComment(id: string, comment: string): Promise { const requestOptions = { method: "PUT", @@ -84,6 +96,7 @@ export const testRunService = { remove, approve, reject, + getTestRunDetails, setIgnoreAreas, setComment, }; From d1b59ea455bd99d8a6272899736e5446cfc06bbc Mon Sep 17 00:00:00 2001 From: Surat Das Date: Sat, 19 Jun 2021 19:32:30 -0700 Subject: [PATCH 2/3] Added clear ignroe from details view and removed bulk apply logic --- src/components/TestDetailsModal.tsx | 16 ++++- src/components/TestRunList/BulkOperation.tsx | 66 +++++--------------- src/services/testRun.service.ts | 13 ---- 3 files changed, 28 insertions(+), 67 deletions(-) diff --git a/src/components/TestDetailsModal.tsx b/src/components/TestDetailsModal.tsx index 8631ab59..c6dced5b 100644 --- a/src/components/TestDetailsModal.tsx +++ b/src/components/TestDetailsModal.tsx @@ -25,7 +25,7 @@ import { TestStatus } from "../types/testStatus"; import { useHistory, Prompt } from "react-router-dom"; import { IgnoreArea } from "../types/ignoreArea"; import { KonvaEventObject } from "konva/types/Node"; -import { Close, Add, Delete, Save, WarningRounded } from "@material-ui/icons"; +import { Close, Add, Delete, Save, WarningRounded, LayersClear } from "@material-ui/icons"; import { TestRunDetails } from "./TestRunDetails"; import useImage from "use-image"; import { routes } from "../constants"; @@ -330,7 +330,7 @@ const TestDetailsModal: React.FunctionComponent<{ selectedRectId && deleteIgnoreArea(selectedRectId) } @@ -338,6 +338,18 @@ const TestDetailsModal: React.FunctionComponent<{ + + + + setIgnoreAreas([]) + } + > + + + + = ( const [approveDialogOpen, setApproveDialogOpen] = React.useState(false); const [rejectDialogOpen, setRejectDialogOpen] = React.useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false); - const [applyIgnoreDialogOpen, setApplyIgnoreDialogOpen] = React.useState(false); const [clearIgnoreDialogOpen, setClearIgnoreDialogOpen] = React.useState(false); const [isProcessing, setIsProcessing] = React.useState(false); @@ -32,17 +31,11 @@ export const BulkOperation: React.FunctionComponent = ( const toggleDeleteDialogOpen = () => { setDeleteDialogOpen(!deleteDialogOpen); }; - const toggleApplyIgnoreDialogOpen = () => { - setApplyIgnoreDialogOpen(!applyIgnoreDialogOpen); - }; const toggleClearIgnoreDialogOpen = () => { setClearIgnoreDialogOpen(!clearIgnoreDialogOpen); }; const getTitle = () => { - if (applyIgnoreDialogOpen) { - return "Apply Selected Ignore Area To All Images"; - } if (clearIgnoreDialogOpen) { return "Clear Ignore Area For Selected Items"; } @@ -59,9 +52,6 @@ export const BulkOperation: React.FunctionComponent = ( if (deleteDialogOpen) { return "Delete"; } - if (applyIgnoreDialogOpen) { - return "Apply"; - } if (clearIgnoreDialogOpen) { return "Clear"; } @@ -78,9 +68,6 @@ export const BulkOperation: React.FunctionComponent = ( if (rejectDialogOpen) { return toggleRejectDialogOpen(); } - if (applyIgnoreDialogOpen) { - return toggleApplyIgnoreDialogOpen(); - } if (clearIgnoreDialogOpen) { return toggleClearIgnoreDialogOpen(); } @@ -104,20 +91,6 @@ export const BulkOperation: React.FunctionComponent = ( if (clearIgnoreDialogOpen) { testRunService.setIgnoreAreas(id, []); } - if (applyIgnoreDialogOpen) { - const testRun = testRunService.getTestRunDetails(id); - let runningNumber = 0; - allRows.allRows.forEach(async (eachRow) => { - const ignoreAreaToSet: IgnoreArea[] = JSON.parse((await testRun).ignoreAreas); - if (ignoreAreaToSet.length > 0) { - //Add a running number to make id unique. - ignoreAreaToSet.forEach((e) => { - e.id = (Date.now() + (++runningNumber)).toString().slice(0, 13); - }); - testRunService.setIgnoreAreas(eachRow.toString(), ignoreAreaToSet); - } - }); - } }; const processApproveReject = (id: string) => { @@ -136,22 +109,12 @@ export const BulkOperation: React.FunctionComponent = ( if (approveDialogOpen) { return toggleApproveDialogOpen(); } - if (applyIgnoreDialogOpen) { - return toggleApplyIgnoreDialogOpen(); - } if (clearIgnoreDialogOpen) { return toggleClearIgnoreDialogOpen(); } return toggleRejectDialogOpen(); }; - const getProcessSuccessMessage = () => { - if (applyIgnoreDialogOpen) { - return "Selected image ignore area has been applied to all images."; - } - return `${count} test runs processed.`; - }; - return ( <> @@ -175,30 +138,29 @@ export const BulkOperation: React.FunctionComponent = ( - - - - - - - - + - + {applyIgnoreDialogOpen - ? `Are you sure you want to apply ignore area to all images? Works well if images are of same resolution.` - : `Are you sure you want to ${submitButtonText().toLowerCase()} ${count} items?`} + + {`Are you sure you want to ${submitButtonText().toLowerCase()} ${count} items?`} + } onSubmit={() => { setIsProcessing(true); @@ -207,7 +169,7 @@ export const BulkOperation: React.FunctionComponent = ( ) .then(() => { setIsProcessing(false); - enqueueSnackbar(getProcessSuccessMessage(), { + enqueueSnackbar(`${count} test runs processed.`, { variant: "success", }); }) diff --git a/src/services/testRun.service.ts b/src/services/testRun.service.ts index a3ef9a8a..93062102 100644 --- a/src/services/testRun.service.ts +++ b/src/services/testRun.service.ts @@ -67,18 +67,6 @@ async function setIgnoreAreas( ).then(handleResponse); } -async function getTestRunDetails(id: string): Promise { - const requestOptions = { - method: "GET", - headers: authHeader(), - }; - - return fetch( - `${API_URL}${ENDPOINT_URL}/${id}`, - requestOptions - ).then(handleResponse); -} - async function setComment(id: string, comment: string): Promise { const requestOptions = { method: "PUT", @@ -96,7 +84,6 @@ export const testRunService = { remove, approve, reject, - getTestRunDetails, setIgnoreAreas, setComment, }; From b13ad22085c6c6c94f3b1b4e90106e88d628be8d Mon Sep 17 00:00:00 2001 From: Pavel Strunkin Date: Mon, 21 Jun 2021 17:39:10 +0300 Subject: [PATCH 3/3] fix: compare area inverted goes over image size https://github.com/Visual-Regression-Tracker/Visual-Regression-Tracker/issues/279 --- package-lock.json | 6 ++ package.json | 1 + src/_helpers/ignoreArea.helper.ts | 87 +++++++++++++++++++++++ src/components/TestDetailsModal.tsx | 103 ++++++++++------------------ 4 files changed, 131 insertions(+), 66 deletions(-) create mode 100644 src/_helpers/ignoreArea.helper.ts diff --git a/package-lock.json b/package-lock.json index baccd4ee..bac37a1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5771,6 +5771,12 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" }, + "@types/lodash": { + "version": "4.14.170", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.170.tgz", + "integrity": "sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q==", + "dev": true + }, "@types/material-ui": { "version": "0.21.8", "resolved": "https://registry.npmjs.org/@types/material-ui/-/material-ui-0.21.8.tgz", diff --git a/package.json b/package.json index 69c87229..766f8408 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ }, "devDependencies": { "@types/jest": "^26.0.20", + "@types/lodash": "^4.14.170", "@types/node": "^13.13.0", "@types/qs": "^6.9.5", "@types/react": "^16.9.43", diff --git a/src/_helpers/ignoreArea.helper.ts b/src/_helpers/ignoreArea.helper.ts new file mode 100644 index 00000000..4d517601 --- /dev/null +++ b/src/_helpers/ignoreArea.helper.ts @@ -0,0 +1,87 @@ +import { uniqueId } from "lodash"; +import { IgnoreArea } from "../types/ignoreArea"; + +export const invertIgnoreArea = ( + imageWidth: number, + imageHeight: number, + ignoreArea?: IgnoreArea +): IgnoreArea[] => { + if (!ignoreArea) { + return []; + } + + const ignoreArea1 = { + x: 0, + y: 0, + width: ignoreArea.x, + height: ignoreArea.y, + id: uniqueId(), + }; + + const ignoreArea2 = { + x: ignoreArea.x, + y: 0, + width: ignoreArea.width, + height: ignoreArea.y, + id: uniqueId(), + }; + + const ignoreArea3 = { + x: ignoreArea.x + ignoreArea.width, + y: 0, + width: imageWidth - (ignoreArea1.width + ignoreArea2.width), + height: ignoreArea.y, + id: uniqueId(), + }; + + const ignoreArea4 = { + x: ignoreArea1.x, + y: ignoreArea1.y + ignoreArea1.height, + width: ignoreArea1.width, + height: ignoreArea.height, + id: uniqueId(), + }; + + const ignoreArea5 = { + x: ignoreArea3.x, + y: ignoreArea3.y + ignoreArea3.height, + width: ignoreArea3.width, + height: ignoreArea.height, + id: uniqueId(), + }; + + const ignoreArea6 = { + x: ignoreArea4.x, + y: ignoreArea4.y + ignoreArea4.height, + width: ignoreArea1.width, + height: imageHeight - (ignoreArea1.height + ignoreArea.height), + id: uniqueId(), + }; + + const ignoreArea7 = { + x: ignoreArea.x, + y: ignoreArea.y + ignoreArea.height, + width: ignoreArea.width, + height: ignoreArea6.height, + id: uniqueId(), + }; + + const ignoreArea8 = { + x: ignoreArea5.x, + y: ignoreArea5.y + ignoreArea5.height, + width: ignoreArea5.width, + height: ignoreArea6.height, + id: uniqueId(), + }; + + return [ + ignoreArea1, + ignoreArea2, + ignoreArea3, + ignoreArea4, + ignoreArea5, + ignoreArea6, + ignoreArea7, + ignoreArea8, + ]; +}; diff --git a/src/components/TestDetailsModal.tsx b/src/components/TestDetailsModal.tsx index c6dced5b..41b28e34 100644 --- a/src/components/TestDetailsModal.tsx +++ b/src/components/TestDetailsModal.tsx @@ -25,7 +25,14 @@ import { TestStatus } from "../types/testStatus"; import { useHistory, Prompt } from "react-router-dom"; import { IgnoreArea } from "../types/ignoreArea"; import { KonvaEventObject } from "konva/types/Node"; -import { Close, Add, Delete, Save, WarningRounded, LayersClear } from "@material-ui/icons"; +import { + Close, + Add, + Delete, + Save, + WarningRounded, + LayersClear, +} from "@material-ui/icons"; import { TestRunDetails } from "./TestRunDetails"; import useImage from "use-image"; import { routes } from "../constants"; @@ -35,6 +42,8 @@ import { CommentsPopper } from "./CommentsPopper"; import { useSnackbar } from "notistack"; import { ScaleActionsSpeedDial } from "./ZoomSpeedDial"; import { ApproveRejectButtons } from "./ApproveRejectButtons"; +import { head } from "lodash"; +import { invertIgnoreArea } from "../_helpers/ignoreArea.helper"; const defaultStagePos = { x: 0, @@ -79,7 +88,9 @@ const TestDetailsModal: React.FunctionComponent<{ ); const [isDrawMode, setIsDrawMode] = useState(false); - const [valueOfIgnoreOrCompare, setValueOfIgnoreOrCompare] = useState("Ignore Areas"); + const [valueOfIgnoreOrCompare, setValueOfIgnoreOrCompare] = useState( + "Ignore Areas" + ); const [isDiffShown, setIsDiffShown] = useState(false); const [selectedRectId, setSelectedRectId] = React.useState(); @@ -105,10 +116,7 @@ const TestDetailsModal: React.FunctionComponent<{ // update in test run testRunService.setIgnoreAreas(testRun.id, ignoreAreas), // update in variation - testVariationService.setIgnoreAreas( - testRun.testVariationId, - ignoreAreas - ), + testVariationService.setIgnoreAreas(testRun.testVariationId, ignoreAreas), ]) .then(() => { enqueueSnackbar(successMessage, { @@ -122,56 +130,21 @@ const TestDetailsModal: React.FunctionComponent<{ ); }; - const isValidCompareArea = () => { - if (ignoreAreas.length === 1) { - return true; - } - enqueueSnackbar("Select one and only one area to compare.", { - variant: "error", - }); - return false; - }; - const saveIgnoreAreasOrCompareArea = () => { if (valueOfIgnoreOrCompare.includes("Ignore")) { saveTestRun(ignoreAreas, "Ignore areas are updated."); - return; - } - if (isValidCompareArea()) { - const imageHeight = image?.height ? image.height : 0; - const imageWidth = image?.width ? image.width : 0; - const firstIgnoreArea = ignoreAreas[0]; - let ignoreArea1 = Object.assign({}, firstIgnoreArea); - ignoreArea1.x = 0; - ignoreArea1.y = 0; - ignoreArea1.width = firstIgnoreArea.x; - ignoreArea1.height = imageHeight; - ignoreArea1.id = firstIgnoreArea.id + 1; - - let ignoreArea2 = Object.assign({}, firstIgnoreArea); - ignoreArea2.x = firstIgnoreArea.x; - ignoreArea2.y = 0; - ignoreArea2.width = imageWidth; - ignoreArea2.height = firstIgnoreArea.y; - ignoreArea2.id = firstIgnoreArea.id + 2; - - let ignoreArea3 = Object.assign({}, firstIgnoreArea); - ignoreArea3.x = firstIgnoreArea.x + firstIgnoreArea.width; - ignoreArea3.y = firstIgnoreArea.y; - ignoreArea3.height = imageHeight; - ignoreArea3.width = imageWidth; - ignoreArea3.id = firstIgnoreArea.id + 3; - - let ignoreArea4 = Object.assign({}, firstIgnoreArea); - ignoreArea4.x = firstIgnoreArea.x; - ignoreArea4.y = firstIgnoreArea.y + firstIgnoreArea.height; - ignoreArea4.height = imageHeight; - ignoreArea4.width = firstIgnoreArea.width; - ignoreArea4.id = firstIgnoreArea.id + 4; + } else { + const invertedIgnoreAreas = invertIgnoreArea( + image!.width, + image!.height, + head(ignoreAreas) + ); - const newIgnoreArea = [ignoreArea1, ignoreArea2, ignoreArea3, ignoreArea4]; - setIgnoreAreas(newIgnoreArea); - saveTestRun(newIgnoreArea, "Selected area has been inverted to ignore areas and saved."); + setIgnoreAreas(invertedIgnoreAreas); + saveTestRun( + invertedIgnoreAreas, + "Selected area has been inverted to ignore areas and saved." + ); } }; @@ -199,9 +172,9 @@ const TestDetailsModal: React.FunctionComponent<{ const fitStageToScreen = () => { const scale = image ? Math.min( - stageWidth < image.width ? stageWidth / image.width : 1, - stageHeigth < image.height ? stageHeigth / image.height : 1 - ) + stageWidth < image.width ? stageWidth / image.width : 1, + stageHeigth < image.height ? stageHeigth / image.height : 1 + ) : 1; setStageScale(scale); resetPositioin(); @@ -267,10 +240,10 @@ const TestDetailsModal: React.FunctionComponent<{ )} {(testRun.status === TestStatus.unresolved || testRun.status === TestStatus.new) && ( - - - - )} + + + + )} @@ -308,7 +281,9 @@ const TestDetailsModal: React.FunctionComponent<{ id="area-select" labelId="areaSelect" value={valueOfIgnoreOrCompare} - onChange={(event) => onIgnoreOrCompareSelectChange(event.target.value as string)} + onChange={(event) => + onIgnoreOrCompareSelectChange(event.target.value as string) + } > {["Ignore Areas", "Compare Area"].map((eachItem) => ( @@ -342,9 +317,7 @@ const TestDetailsModal: React.FunctionComponent<{ - setIgnoreAreas([]) - } + onClick={() => setIgnoreAreas([])} > @@ -353,9 +326,7 @@ const TestDetailsModal: React.FunctionComponent<{ - saveIgnoreAreasOrCompareArea() - } + onClick={() => saveIgnoreAreasOrCompareArea()} >