diff --git a/package-lock.json b/package-lock.json index 7f6ec13e..e8165d0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3714,6 +3714,11 @@ } } }, + "classnames": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" + }, "clean-css": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", @@ -8868,6 +8873,27 @@ "object-visit": "^1.0.0" } }, + "material-ui-popup-state": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/material-ui-popup-state/-/material-ui-popup-state-1.6.1.tgz", + "integrity": "sha512-I1Cu8hc3RPPTZ7p946q6qQB+n68JuhoiOxclV+wft8bplYGHFLjbNmF59wQMTN6oRZD42QuVtTxDv7LIi0eB8w==", + "requires": { + "@babel/runtime": "^7.1.5", + "@material-ui/types": "^4.1.1", + "classnames": "^2.2.6", + "prop-types": "^15.0.0" + }, + "dependencies": { + "@material-ui/types": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-4.1.1.tgz", + "integrity": "sha512-AN+GZNXytX9yxGi0JOfxHrRTbhFybjUJ05rnsBVjcB+16e466Z0Xe5IxawuOayVZgTBNDxmPKo5j4V6OnMtaSQ==", + "requires": { + "@types/react": "*" + } + } + } + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", diff --git a/package.json b/package.json index 914345d6..5332f06f 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@testing-library/react": "^9.5.0", "@testing-library/user-event": "^7.2.1", "konva": "^4.2.2", + "material-ui-popup-state": "^1.6.1", "qs": "^6.9.4", "react": "^16.13.1", "react-debounce-input": "^3.2.2", diff --git a/src/components/CommentsPopper.tsx b/src/components/CommentsPopper.tsx new file mode 100644 index 00000000..bc6cda12 --- /dev/null +++ b/src/components/CommentsPopper.tsx @@ -0,0 +1,98 @@ +import React from "react"; +import { + Button, + Popper, + Fade, + Paper, + makeStyles, + TextField, + Badge, + IconButton, +} from "@material-ui/core"; +import { + usePopupState, + bindToggle, + bindPopper, +} from "material-ui-popup-state/hooks"; +import { Comment } from "@material-ui/icons"; + +const useStyles = makeStyles((theme) => ({ + popperContainer: { + zIndex: 1400, + }, + contentContainer: { + padding: theme.spacing(2), + }, +})); + +interface IProps { + text: string | undefined; + onSave: (comment: string) => Promise; +} + +export const CommentsPopper: React.FunctionComponent = ({ + text, + onSave, +}) => { + const classes = useStyles(); + const popupState = usePopupState({ + variant: "popper", + popupId: "commentPopper", + }); + const [comment, setComment] = React.useState(""); + + React.useEffect(() => setComment(text ? text : ""), [text]); + + return ( + + + + + + + + {({ TransitionProps }) => ( + + + + + setComment((event.target as HTMLInputElement).value) + } + inputProps={{ + "data-testid": "comment", + }} + /> + + + + + )} + + + ); +}; diff --git a/src/components/DrawArea.tsx b/src/components/DrawArea.tsx index c0d82ba6..6904166d 100644 --- a/src/components/DrawArea.tsx +++ b/src/components/DrawArea.tsx @@ -75,7 +75,7 @@ export const DrawArea: FunctionComponent = ({ const [isDrawMode, setIsDrawMode] = drawModeState; const [isDrawing, setIsDrawing] = React.useState(isDrawMode); - const [image, loading] = useImage(staticService.getImage(imageName)); + const [image, imageStatus] = useImage(staticService.getImage(imageName)); const handleContentMousedown = (e: any) => { if (!isDrawMode) return; @@ -126,124 +126,122 @@ export const DrawArea: FunctionComponent = ({ - {imageName ? ( - loading === "loading" ? ( - - - - + {imageStatus === "loading" && ( + + + - ) : ( - + + )} + {(!imageName || imageStatus === "failed") && } + {imageName && imageStatus === "loaded" && ( + +
{ + if (!isDrawMode && isDrag && !selectedRectId) { + event.preventDefault(); + setStagePos({ + x: event.clientX - stageInitPos.x, + y: event.clientY - stageInitPos.y, + }); + setStageOffset(stagePos); + } + }} + onMouseUp={(event) => { + setIsDrag(false); + setStageInitPos(stagePos); + }} + onMouseLeave={(event) => { + setIsDrag(false); + setStageInitPos(stagePos); + }} + onMouseDown={(event) => { + setIsDrag(true); + setStageInitPos({ + x: event.clientX - stageOffset.x, + y: event.clientY - stageOffset.y, + }); }} > -
{ - if (!isDrawMode && isDrag && !selectedRectId) { - event.preventDefault(); - setStagePos({ - x: event.clientX - stageInitPos.x, - y: event.clientY - stageInitPos.y, - }); - setStageOffset(stagePos); - } - }} - onMouseUp={(event) => { - setIsDrag(false); - setStageInitPos(stagePos); - }} - onMouseLeave={(event) => { - setIsDrag(false); - setStageInitPos(stagePos); - }} - onMouseDown={(event) => { - setIsDrag(true); - setStageInitPos({ - x: event.clientX - stageOffset.x, - y: event.clientY - stageOffset.y, - }); + transform: `scale(${stageScale})`, + transformOrigin: "top left", }} + onContentMousedown={handleContentMousedown} + onContentMouseup={handleContentMouseup} + onContentMouseMove={handleContentMouseMove} > - - - { - document.body.style.cursor = isDrawMode - ? "crosshair" - : "grab"; - }} - onMouseDown={(event) => { - document.body.style.cursor = "grabbing"; - }} - onMouseUp={(event) => { - document.body.style.cursor = "grab"; - }} - onMouseLeave={(event) => { - document.body.style.cursor = "default"; - }} - /> - {ignoreAreas.map((rect, i) => { - return ( - setSelectedRectId(rect.id)} - onChange={(newAttrs: RectConfig) => { - const rects = ignoreAreas.slice(); + + { + document.body.style.cursor = isDrawMode + ? "crosshair" + : "grab"; + }} + onMouseDown={(event) => { + document.body.style.cursor = "grabbing"; + }} + onMouseUp={(event) => { + document.body.style.cursor = "grab"; + }} + onMouseLeave={(event) => { + document.body.style.cursor = "default"; + }} + /> + {ignoreAreas.map((rect, i) => { + return ( + setSelectedRectId(rect.id)} + onChange={(newAttrs: RectConfig) => { + const rects = ignoreAreas.slice(); - rects[i].x = Math.round(newAttrs.x || 0); - rects[i].y = Math.round(newAttrs.y || 0); - rects[i].width = Math.round( - newAttrs.width || MIN_RECT_SIDE_PIXEL - ); - rects[i].height = Math.round( - newAttrs.height || MIN_RECT_SIDE_PIXEL - ); + rects[i].x = Math.round(newAttrs.x || 0); + rects[i].y = Math.round(newAttrs.y || 0); + rects[i].width = Math.round( + newAttrs.width || MIN_RECT_SIDE_PIXEL + ); + rects[i].height = Math.round( + newAttrs.height || MIN_RECT_SIDE_PIXEL + ); - setIgnoreAreas(rects); - }} - /> - ); - })} - - -
+ setIgnoreAreas(rects); + }} + /> + ); + })} + +
- - ) - ) : ( - +
+
)}
diff --git a/src/components/TestDetailsModal.tsx b/src/components/TestDetailsModal.tsx index 6ed53d6c..98a78b73 100644 --- a/src/components/TestDetailsModal.tsx +++ b/src/components/TestDetailsModal.tsx @@ -37,6 +37,7 @@ import useImage from "use-image"; import { routes } from "../constants"; import { useTestRunDispatch, updateTestRun, selectTestRun } from "../contexts"; import { DrawArea } from "./DrawArea"; +import { CommentsPopper } from "./CommentsPopper"; const useStyles = makeStyles((theme) => ({ imageContainer: { @@ -202,6 +203,22 @@ const TestDetailsModal: React.FunctionComponent<{ Baseline history
+ + + Promise.all([ + // update in test run + testRunService + .setComment(testRun.id, comment) + .then((testRun) => updateTestRun(testRunDispatch, testRun)), + // update in variation + testVariationService + .setComment(testRun.testVariationId, comment), + ]) + } + /> + diff --git a/src/services/testRun.service.ts b/src/services/testRun.service.ts index bb1e1f61..ea586081 100644 --- a/src/services/testRun.service.ts +++ b/src/services/testRun.service.ts @@ -12,6 +12,7 @@ export const testRunService = { approve, reject, setIgnoreAreas, + setComment, }; function getList(buildId: string): Promise { @@ -86,3 +87,15 @@ function setIgnoreAreas( requestOptions ).then(handleResponse); } + +function setComment(id: string, comment: string): Promise { + const requestOptions = { + method: "PUT", + headers: { "Content-Type": "application/json", ...authHeader() }, + body: JSON.stringify({ comment }), + }; + + return fetch(`${API_URL}${ENDPOINT_URL}/comment/${id}`, requestOptions).then( + handleResponse + ); +} diff --git a/src/services/testVariation.service.ts b/src/services/testVariation.service.ts index 7574fc51..5cd179ab 100644 --- a/src/services/testVariation.service.ts +++ b/src/services/testVariation.service.ts @@ -3,12 +3,13 @@ import { handleResponse, authHeader } from "../_helpers/service.helpers"; import { API_URL } from "../_config/api.config"; import { IgnoreArea } from "../types/ignoreArea"; -const ENDPOINT_URL = "/test-variations" +const ENDPOINT_URL = "/test-variations"; export const testVariationService = { getList, getDetails, - setIgnoreAreas + setIgnoreAreas, + setComment, }; function getList(projectId: String): Promise { @@ -17,7 +18,10 @@ function getList(projectId: String): Promise { headers: authHeader(), }; - return fetch(`${API_URL}${ENDPOINT_URL}?projectId=${projectId}`, requestOptions).then(handleResponse); + return fetch( + `${API_URL}${ENDPOINT_URL}?projectId=${projectId}`, + requestOptions + ).then(handleResponse); } function getDetails(id: String): Promise { @@ -26,7 +30,9 @@ function getDetails(id: String): Promise { headers: authHeader(), }; - return fetch(`${API_URL}${ENDPOINT_URL}/${id}`, requestOptions).then(handleResponse); + return fetch(`${API_URL}${ENDPOINT_URL}/${id}`, requestOptions).then( + handleResponse + ); } function setIgnoreAreas( @@ -43,4 +49,16 @@ function setIgnoreAreas( `${API_URL}${ENDPOINT_URL}/ignoreArea/${variationId}`, requestOptions ).then(handleResponse); -} \ No newline at end of file +} + +function setComment(id: string, comment: string): Promise { + const requestOptions = { + method: "PUT", + headers: { "Content-Type": "application/json", ...authHeader() }, + body: JSON.stringify({ comment }), + }; + + return fetch(`${API_URL}${ENDPOINT_URL}/comment/${id}`, requestOptions).then( + handleResponse + ); +} diff --git a/src/types/testRun.ts b/src/types/testRun.ts index 0164bc55..9a36171b 100644 --- a/src/types/testRun.ts +++ b/src/types/testRun.ts @@ -16,4 +16,5 @@ export interface TestRun { viewport: string; device: string; ignoreAreas: string; + comment?: string; } diff --git a/src/types/testVariation.ts b/src/types/testVariation.ts index 2a75a30e..400ded41 100644 --- a/src/types/testVariation.ts +++ b/src/types/testVariation.ts @@ -10,5 +10,6 @@ export interface TestVariation { device: string; ignoreAreas: string; projectId: string; - baselines: Baseline[] + baselines: Baseline[]; + comment?: string; }