diff --git a/cypress/downloads/downloads.htm b/cypress/downloads/downloads.htm deleted file mode 100644 index fe2280c65..000000000 Binary files a/cypress/downloads/downloads.htm and /dev/null differ diff --git a/cypress/integration/edit_patient_activity.spec.js b/cypress/integration/edit_patient_activity.spec.js index 04cdb3120..47beddfe0 100644 --- a/cypress/integration/edit_patient_activity.spec.js +++ b/cypress/integration/edit_patient_activity.spec.js @@ -8,7 +8,7 @@ describe("EditPatientActivity spec", () => { cy.get("[class=editPatient]"); }); - it.skip("should have access to the user credentials", () => { }); + it.skip("should have access to the user credentials", () => {}); it("should have a PatientDataForm as a child component", () => { cy.get("[class=patientDataForm]"); @@ -44,6 +44,8 @@ describe("EditPatientActivity spec", () => { { force: true } ); + cy.get("[class=MuiDialogContent-root]").contains("Confirm").click(); + cy.wait(1000); cy.get("[class=profilePicture]") .find("img") @@ -79,11 +81,10 @@ describe("EditPatientActivity spec", () => { it("should not leave on the Cancel button click, if the Cancel button of the Cancel Dialog is click", () => { cy.get("[id=firstName]").clear().type("Marcelo"); cy.get("[class=patientDataForm]").contains("Cancel").click(); - cy.url().then(url => { + cy.url().then((url) => { cy.get("div.dialog__buttonSet").contains("Keep").click(); - cy.url().should('eq', url); + cy.url().should("eq", url); }); //cy.get("[id=firstName]").should("have.value", "Antonio Carlos"); }); - }); diff --git a/cypress/integration/new_patient_activity.spec.js b/cypress/integration/new_patient_activity.spec.js index 158b8d5cd..91fc300cc 100644 --- a/cypress/integration/new_patient_activity.spec.js +++ b/cypress/integration/new_patient_activity.spec.js @@ -8,7 +8,7 @@ describe("NewPatientActivity spec", () => { cy.get("[class=newPatient]"); }); - it.skip("should have access to the user credentials", () => { }); + it.skip("should have access to the user credentials", () => {}); it("should have a PatientDataForm as a child component", () => { cy.get("[class=patientDataForm]"); @@ -44,6 +44,8 @@ describe("NewPatientActivity spec", () => { { force: true } ); + cy.get("[class=MuiDialogContent-root]").contains("Confirm").click(); + cy.wait(1000); cy.get("[class=profilePicture]") .find("img") diff --git a/package-lock.json b/package-lock.json index f6cab6d5c..f81a919e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "@types/react-router": "^5.1.19", "@types/react-router-dom": "^5.3.3", "@types/yup": "^0.29.0", + "browser-image-compression": "^2.0.2", "chart.js": "^3.9.1", "classnames": "^2.2.6", "date-fns": "^2.16.1", @@ -15776,6 +15777,14 @@ "integrity": "sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==", "dev": true }, + "node_modules/browser-image-compression": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz", + "integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==", + "dependencies": { + "uzip": "0.20201231.0" + } + }, "node_modules/browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", @@ -15874,6 +15883,12 @@ "pako": "~1.0.5" } }, + "node_modules/browserify-zlib/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, "node_modules/browserslist": { "version": "4.21.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz", @@ -29568,12 +29583,6 @@ "node": ">=6" } }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true - }, "node_modules/parallel-transform": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", @@ -32010,7 +32019,8 @@ }, "node_modules/react-image-crop": { "version": "9.1.1", - "license": "ISC", + "resolved": "https://registry.npmjs.org/react-image-crop/-/react-image-crop-9.1.1.tgz", + "integrity": "sha512-n7O3Cn7RuZJF8ooau2atwCGWZHqO7akl/pa6IR+1jWy5X0x4enTOCWOeswYhcBOv0ohmDHctAlf6mcUuSSwsow==", "dependencies": { "clsx": "^1.1.1" }, @@ -36312,6 +36322,11 @@ "integrity": "sha512-dsNgbLaTrd6l3MMxTtouOCFw4CBFc/3a+GgYA2YyrJvyQ1u6q4pcu3ktLoUZ/VN/Aw9WsauazbgsgdfVWgAKQg==", "dev": true }, + "node_modules/uzip": { + "version": "0.20201231.0", + "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz", + "integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==" + }, "node_modules/v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", @@ -49526,6 +49541,14 @@ "integrity": "sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==", "dev": true }, + "browser-image-compression": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz", + "integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==", + "requires": { + "uzip": "0.20201231.0" + } + }, "browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", @@ -49610,6 +49633,14 @@ "dev": true, "requires": { "pako": "~1.0.5" + }, + "dependencies": { + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + } } }, "browserslist": { @@ -59803,12 +59834,6 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, - "pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true - }, "parallel-transform": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", @@ -61406,6 +61431,8 @@ }, "react-image-crop": { "version": "9.1.1", + "resolved": "https://registry.npmjs.org/react-image-crop/-/react-image-crop-9.1.1.tgz", + "integrity": "sha512-n7O3Cn7RuZJF8ooau2atwCGWZHqO7akl/pa6IR+1jWy5X0x4enTOCWOeswYhcBOv0ohmDHctAlf6mcUuSSwsow==", "requires": { "clsx": "^1.1.1" } @@ -64610,6 +64637,11 @@ "integrity": "sha512-dsNgbLaTrd6l3MMxTtouOCFw4CBFc/3a+GgYA2YyrJvyQ1u6q4pcu3ktLoUZ/VN/Aw9WsauazbgsgdfVWgAKQg==", "dev": true }, + "uzip": { + "version": "0.20201231.0", + "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz", + "integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==" + }, "v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", diff --git a/package.json b/package.json index 21b250ba7..3eee8fa25 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@types/react-router": "^5.1.19", "@types/react-router-dom": "^5.3.3", "@types/yup": "^0.29.0", + "browser-image-compression": "^2.0.2", "chart.js": "^3.9.1", "classnames": "^2.2.6", "date-fns": "^2.16.1", diff --git a/src/components/accessories/profilePicture/ProfilePicture.tsx b/src/components/accessories/profilePicture/ProfilePicture.tsx index c2bbde2f3..fc9b3b951 100644 --- a/src/components/accessories/profilePicture/ProfilePicture.tsx +++ b/src/components/accessories/profilePicture/ProfilePicture.tsx @@ -14,6 +14,7 @@ import AddRoundedIcon from "@material-ui/icons/AddRounded"; import PhotoCameraIcon from "@material-ui/icons/PhotoCamera"; import AddPhotoAlternateIcon from "@material-ui/icons/AddPhotoAlternate"; import React, { + ChangeEvent, FunctionComponent, useCallback, useEffect, @@ -25,9 +26,15 @@ import Webcam from "../../accessories/webcam/Webcam"; import profilePicturePlaceholder from "../../../assets/profilePicturePlaceholder.png"; import "./styles.scss"; import { IProps } from "./types"; -import { handlePictureSelection, preprocessImage } from "./utils"; +import { + extractPictureFromSelection, + handlePictureSelection, + preprocessImage, +} from "./utils"; import classNames from "classnames"; import { GridCloseIcon } from "@material-ui/data-grid"; +import { ProfilePictureCropper } from "../profilePictureCropper/ProfilePictureCropper"; +import { isEmpty } from "lodash"; export const ProfilePicture: FunctionComponent = ({ isEditable, @@ -42,18 +49,25 @@ export const ProfilePicture: FunctionComponent = ({ original: "", }); - const [showError, setShowError] = React.useState(""); - const [showModal, setShowModal] = React.useState(false); - const [showWebcam, setShowWebcam] = React.useState(false); + const [showError, setShowError] = useState(""); + const [showModal, setShowModal] = useState(false); + const [showWebcam, setShowWebcam] = useState(false); + const [showCropper, setShowCropper] = useState(false); + const [fromFileSystem, setFromFileSystem] = useState(false); + const [pictureToResize, setPictureToResize] = useState(""); const { t } = useTranslation(); const handleCloseError = () => { + removePicture(); setShowError(""); }; useEffect(() => { if (preLoadedPicture) { - preprocessImage(setPicture, preLoadedPicture); + setPicture({ + preview: "data:image/jpeg;base64," + preLoadedPicture, + original: preLoadedPicture, + }); } }, [preLoadedPicture]); @@ -63,6 +77,13 @@ export const ProfilePicture: FunctionComponent = ({ } }, [onChange, picture.original]); + useEffect(() => { + if (!showModal && !isEmpty(pictureToResize) && fromFileSystem) { + setFromFileSystem(false); + openCropper(); + } + }, [pictureToResize]); + const pictureInputRef = useRef(null); const choosePicture = () => pictureInputRef.current?.click(); @@ -75,8 +96,11 @@ export const ProfilePicture: FunctionComponent = ({ const openWebcam = () => setShowWebcam(true); const closeWebcam = () => setShowWebcam(false); + const openCropper = () => setShowCropper(true); + const closeCropper = () => setShowCropper(false); const removePicture = () => { + setPictureToResize(""); setPicture({ preview: profilePicturePlaceholder, original: "", @@ -86,14 +110,36 @@ export const ProfilePicture: FunctionComponent = ({ } }; + const handleCropped = useCallback( + (value: string) => { + preprocessImage(setPicture, value, setShowError); + closeCropper(); + }, + [setPicture] + ); + const confirmWebcamPicture = useCallback( (image: string) => { - preprocessImage(setPicture, image); + preprocessImage(setPicture, image, setShowError); closeModal(); }, [setPicture] ); + const handleChange = useCallback( + () => (e: ChangeEvent) => { + setFromFileSystem(true); + extractPictureFromSelection(setPictureToResize)(e); + }, + [setPictureToResize] + ); + + const handleReset = () => { + closeCropper(); + removePicture(); + pictureInputRef.current?.click(); + }; + useEffect(() => { if (shouldReset && resetCallback) { removePicture(); @@ -103,13 +149,20 @@ export const ProfilePicture: FunctionComponent = ({ return (
+
{ const canvas = document.createElement("canvas"); @@ -24,45 +27,64 @@ const createPreview = (img: HTMLImageElement) => { return canvas.toDataURL("image/jpeg", 0.7); // get the data from canvas as 70% JPG }; -export const handlePictureSelection = ( - setPicture: Dispatch< - SetStateAction<{ - preview: string; - original: string; - }> - >, setShowError: React.Dispatch>, maxFileUpload: number -) => (e: ChangeEvent): void => { - const newPic = e.target.files && e.target.files[0]; - if (getFileSize(newPic, maxFileUpload)) { +export const handlePictureSelection = + ( + setPicture: Dispatch< + SetStateAction<{ + preview: string; + original: string; + }> + >, + setShowError: React.Dispatch> + ) => + (e: ChangeEvent): void => { + const newPic = e.target.files && e.target.files[0]; if (newPic) { const dataURLReader = new FileReader(); dataURLReader.onload = (e) => { const pictureURI = e.target?.result; if (typeof pictureURI === "string") { - preprocessImage(setPicture, pictureURI); + preprocessImage(setPicture, pictureURI, setShowError); } }; dataURLReader.readAsDataURL(newPic); } - } else { - setShowError("File is too big! (Max upload file is " + maxFileUpload / 1000 + " KB)"); - return; - } -}; + }; -export const getFileSize = (file: File | null, maxFileUpload: number): boolean => ( - !file || file.size > maxFileUpload ? false : true -); +export const extractPictureFromSelection = + (setPictureToResize: React.Dispatch>) => + (e: ChangeEvent): void => { + const newPic = e.target.files && e.target.files[0]; + if (newPic) { + const dataURLReader = new FileReader(); + dataURLReader.onload = (e) => { + const pictureURI = e.target?.result; + if (typeof pictureURI === "string") { + setPictureToResize(pictureURI); + } + }; + dataURLReader.readAsDataURL(newPic); + } + }; + +export const getFileSize = ( + file: File | null, + maxFileUpload: number +): boolean => (!file || file.size > maxFileUpload ? false : true); + +export const isValidSize = (file: Blob, maxFileUpload: number): boolean => + file.size > maxFileUpload ? false : true; -export const preprocessImage = ( +export const preprocessImage = async ( setPicture: Dispatch< SetStateAction<{ preview: string; original: string; }> >, - picture: string -): void => { + picture: string, + setShowError?: React.Dispatch> +) => { let pictureURI = ""; let pictureData = ""; if (picture.includes("data:")) { @@ -72,12 +94,30 @@ export const preprocessImage = ( pictureURI = "data:image/jpeg;base64," + picture; pictureData = picture; } - - const image = new Image(); - image.src = pictureURI; - - image.onload = function () { - const preview = createPreview(image); - setPicture({ original: pictureData, preview }); + let file = await imageCompression.getFilefromDataUrl(picture, "avatar"); + const compressionOptions = { + maxSizeMB: MAX_FILE_UPLOAD_SIZE / 1024 / 1024, + useWebWorker: true, }; + file = await imageCompression(file, compressionOptions); + pictureURI = await imageCompression.getDataUrlFromFile(file); + pictureData = pictureURI.split(",")[1]; + if (file.size < MAX_FILE_UPLOAD_SIZE) { + const image = new Image(); + image.src = pictureURI; + + image.onload = function () { + const preview = createPreview(image); + setPicture({ original: pictureData, preview }); + }; + } else { + if (setShowError) { + setShowError( + "File is too big! (Max upload file is " + + MAX_FILE_UPLOAD_SIZE / 1024 + + " KB)" + ); + } + return; + } }; diff --git a/src/components/accessories/profilePictureCropper/ProfilePictureCropper.tsx b/src/components/accessories/profilePictureCropper/ProfilePictureCropper.tsx new file mode 100644 index 000000000..9b6c6c391 --- /dev/null +++ b/src/components/accessories/profilePictureCropper/ProfilePictureCropper.tsx @@ -0,0 +1,28 @@ +import React, { FunctionComponent } from "react"; +import "./styles.scss"; +import { IProps } from "./types"; +import { Dialog, DialogContent, DialogContentText } from "@material-ui/core"; +import ImageResize from "../imageResize/ImageResize"; + +export const ProfilePictureCropper: FunctionComponent = ({ + picture, + onSave, + onReset, + open, +}) => { + return ( +
+ + + + + + + +
+ ); +}; diff --git a/src/components/accessories/profilePictureCropper/styles.scss b/src/components/accessories/profilePictureCropper/styles.scss new file mode 100644 index 000000000..f3ef6c9d7 --- /dev/null +++ b/src/components/accessories/profilePictureCropper/styles.scss @@ -0,0 +1,14 @@ +@import "../../../styles/variables"; + +.croppedProfilePicture { + display: flex; + flex-direction: column; + align-items: center; + position: relative; +} +.croppedProfilePicture img { + width: 100%; + height: auto; + overflow: hidden; + object-fit: cover; +} diff --git a/src/components/accessories/profilePictureCropper/styles.ts b/src/components/accessories/profilePictureCropper/styles.ts new file mode 100644 index 000000000..53f748415 --- /dev/null +++ b/src/components/accessories/profilePictureCropper/styles.ts @@ -0,0 +1,47 @@ +import { Theme } from "@material-ui/core"; +import { makeStyles } from "@material-ui/styles"; + +export const useStyles = makeStyles((theme: Theme) => ({ + cropContainer: { + position: "relative", + width: "100%", + height: 200, + background: "#333", + [theme.breakpoints.up("sm")]: { + height: 400, + }, + }, + cropButton: { + flexShrink: 0, + marginLeft: 16, + }, + controls: { + padding: 16, + display: "flex", + flexDirection: "column", + alignItems: "stretch", + [theme.breakpoints.up("sm")]: { + flexDirection: "row", + alignItems: "center", + }, + }, + sliderContainer: { + display: "flex", + flex: "1", + alignItems: "center", + }, + sliderLabel: { + [theme.breakpoints.down("xs")]: { + minWidth: 65, + }, + }, + slider: { + padding: "22px 0px", + marginLeft: 32, + [theme.breakpoints.up("sm")]: { + flexDirection: "row", + alignItems: "center", + margin: "0 16px", + }, + }, +})); diff --git a/src/components/accessories/profilePictureCropper/types.ts b/src/components/accessories/profilePictureCropper/types.ts new file mode 100644 index 000000000..075ee8e0e --- /dev/null +++ b/src/components/accessories/profilePictureCropper/types.ts @@ -0,0 +1,8 @@ +import { CSSProperties } from "react"; + +export interface IProps { + open: boolean; + picture: string; + onSave: (image: string) => void; + onReset: () => void; +} diff --git a/src/components/accessories/profilePictureCropper/utils.ts b/src/components/accessories/profilePictureCropper/utils.ts new file mode 100644 index 000000000..105fd6fb7 --- /dev/null +++ b/src/components/accessories/profilePictureCropper/utils.ts @@ -0,0 +1,83 @@ +import { ChangeEvent, Dispatch, SetStateAction } from "react"; + +const createPreview = (img: HTMLImageElement) => { + const canvas = document.createElement("canvas"); + let width = img.width; + let height = img.height; + // calculate the width and height, constraining the proportions + if (width > height) { + if (width > 180) { + height = Math.round((height *= 180 / width)); + width = 180; + } + } else { + if (height > 160) { + width = Math.round((width *= 160 / height)); + height = 160; + } + } + // resize the canvas and draw the image data into it + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext("2d"); + ctx?.drawImage(img, 0, 0, width, height); + return canvas.toDataURL("image/jpeg", 0.7); // get the data from canvas as 70% JPG +}; + +export const handlePictureSelection = ( + setPicture: Dispatch< + SetStateAction<{ + preview: string; + original: string; + }> + >, setShowError: React.Dispatch>, maxFileUpload: number +) => (e: ChangeEvent): void => { + const newPic = e.target.files && e.target.files[0]; + if (getFileSize(newPic, maxFileUpload)) { + if (newPic) { + const dataURLReader = new FileReader(); + dataURLReader.onload = (e) => { + const pictureURI = e.target?.result; + if (typeof pictureURI === "string") { + preprocessImage(setPicture, pictureURI); + } + }; + dataURLReader.readAsDataURL(newPic); + } + } else { + setShowError("File is too big! (Max upload file is " + maxFileUpload / 1000 + " KB)"); + return; + } +}; + +export const getFileSize = (file: File | null, maxFileUpload: number): boolean => ( + !file || file.size > maxFileUpload ? false : true +); + +export const preprocessImage = ( + setPicture: Dispatch< + SetStateAction<{ + preview: string; + original: string; + }> + >, + picture: string +): void => { + let pictureURI = ""; + let pictureData = ""; + if (picture.includes("data:")) { + pictureURI = picture; + pictureData = picture.split(",")[1]; + } else { + pictureURI = "data:image/jpeg;base64," + picture; + pictureData = picture; + } + + const image = new Image(); + image.src = pictureURI; + + image.onload = function () { + const preview = createPreview(image); + setPicture({ original: pictureData, preview }); + }; +}; diff --git a/src/components/accessories/table/Table.tsx b/src/components/accessories/table/Table.tsx index a2dacb3c9..a5227689e 100644 --- a/src/components/accessories/table/Table.tsx +++ b/src/components/accessories/table/Table.tsx @@ -32,6 +32,8 @@ import { import ConfirmationDialog from "../confirmationDialog/ConfirmationDialog"; import { useTranslation } from "react-i18next"; import warningIcon from "../../../assets/warning-icon.png"; +import SmallButton from "../smallButton/SmallButton"; +import Button from "../button/Button"; const Table: FunctionComponent = ({ rowData, @@ -64,6 +66,7 @@ const Table: FunctionComponent = ({ const [openDeleteConfirmation, setOpenDeleteConfirmation] = useState(false); const [openCancelConfirmation, setOpenCancelConfirmation] = useState(false); const [currentRow, setCurrentRow] = useState({} as any); + const [expanded, setExpanded] = useState(false); const handleChangePage = (event: unknown, newPage: number) => { setPage(newPage); }; @@ -224,9 +227,18 @@ const Table: FunctionComponent = ({ setOpenCancelConfirmation(false); }; + const handleExpand = () => { + setExpanded(!expanded); + }; + return ( <> +
+ +
@@ -276,6 +288,7 @@ const Table: FunctionComponent = ({ showEmptyCell={showEmptyCell} renderCellDetails={renderItemDetails} detailColSpan={detailColSpan} + expanded={expanded} /> ))} diff --git a/src/components/accessories/table/TableBodyRow.tsx b/src/components/accessories/table/TableBodyRow.tsx index ca4a753c5..0467d3889 100644 --- a/src/components/accessories/table/TableBodyRow.tsx +++ b/src/components/accessories/table/TableBodyRow.tsx @@ -20,19 +20,13 @@ const TableBodyRow: FunctionComponent = ({ renderCellDetails, coreRow, detailColSpan, + expanded, }) => { const [open, setOpen] = React.useState(false); - const isPrintMode = useMediaQuery("print"); - useHotkeys("ctrl+p", async (event, handler) => { - setOpen(true); - await sleep(1000); - }); useEffect(() => { - if (!isPrintMode) { - setOpen(false); - } - }, [isPrintMode]); + setOpen(expanded ?? open); + }, [expanded]); return ( <> @@ -68,7 +62,7 @@ const TableBodyRow: FunctionComponent = ({ colSpan={detailColSpan ?? 6} > (row: T) => any; coreRow?: any; detailColSpan?: number; + expanded?: boolean; } export type TActions = diff --git a/src/index.tsx b/src/index.tsx index 76d3ec29a..d37ee229d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -35,7 +35,7 @@ import layouts from "./state/layouts/reducer"; import dashboard from "./state/dashboard/reducer"; if (process.env.REACT_APP_USE_MOCK_API) { - //console.log("Using mocked api"); + console.log("Using mocked api"); makeServer(); } diff --git a/src/resources/i18n/en.json b/src/resources/i18n/en.json index d093039a0..ccc5dea76 100644 --- a/src/resources/i18n/en.json +++ b/src/resources/i18n/en.json @@ -444,7 +444,9 @@ "thisyear": "This Year", "lastyear": "Last Year" }, - "continue": "Continue ?" + "continue": "Continue ?", + "collapse_all": "Collapse All", + "expand_all": "Expand All" }, "permission": { "denied": "Permission denied", @@ -546,11 +548,11 @@ "statusmustbedone": "Exam status must be 'DONE' if results are provided", "changestatus": "Change Exam Status", "changelabstatusto": "The status of the exam {{code}} will be changed to {{status}}", - "statuses" : { - "DRAFT" : "DRAFT", - "ALL" : "ALL", - "OPEN" : "OPEN", - "DONE" : "DONE" + "statuses": { + "DRAFT": "DRAFT", + "ALL": "ALL", + "OPEN": "OPEN", + "DONE": "DONE" } }, "admission": { @@ -725,15 +727,15 @@ "elderly": "Elderly" }, "lab": { - "undefined" : {"txt": "Undefined"}, - "blood": {"txt": "Blood"}, - "cfs": {"txt": "CFS"}, - "film": {"txt": "FILM"}, - "sputum":{"txt": "Sputum"}, - "stool":{"txt": "Stool"}, - "swabs":{"txt": "Swabs"}, - "tissues":{"txt": "Tissues"}, - "urine":{"txt": "Urine"} + "undefined": { "txt": "Undefined" }, + "blood": { "txt": "Blood" }, + "cfs": { "txt": "CFS" }, + "film": { "txt": "FILM" }, + "sputum": { "txt": "Sputum" }, + "stool": { "txt": "Stool" }, + "swabs": { "txt": "Swabs" }, + "tissues": { "txt": "Tissues" }, + "urine": { "txt": "Urine" } } } }