Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feat)O3-2211: draw using a custom simple svg editor #5

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,16 @@
},
"dependencies": {
"@carbon/react": "^1.33.0",
"@openmrs/esm-patient-common-lib": "^5.0.0",
"@openmrs/openmrs-form-engine-lib": "latest",
"canvg": "^4.0.1",
"html2canvas": "^1.4.1",
"lodash-es": "^4.17.21",
"react-image-annotate": "^1.8.0"
},
"peerDependencies": {
"@openmrs/esm-framework": "*",
"@openmrs/esm-patient-common-lib": "5.x",
"dayjs": "1.x",
"react": "18.x",
"react-i18next": "11.x",
Expand Down
28 changes: 28 additions & 0 deletions src/attachments-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export interface UploadedFile {
file?: File;
base64Content: string;
fileName: string;
fileType: string;
fileDescription: string;
status?: "uploading" | "complete";

Check failure on line 8 in src/attachments-types.ts

View workflow job for this annotation

GitHub Actions / build

Delete `··⏎`
}

Check failure on line 10 in src/attachments-types.ts

View workflow job for this annotation

GitHub Actions / build

Delete `··`
export interface Attachment {
id: string;
src: string;
title: string;
description: string;
dateTime: string;
bytesMimeType: string;
bytesContentFamily: string;

Check failure on line 19 in src/attachments-types.ts

View workflow job for this annotation

GitHub Actions / build

Delete `⏎`
}
export interface AttachmentResponse {
bytesContentFamily: string;
bytesMimeType: string;
comment: string;
dateTime: string;
uuid: string;

Check failure on line 26 in src/attachments-types.ts

View workflow job for this annotation

GitHub Actions / build

Delete `⏎`

}
88 changes: 88 additions & 0 deletions src/attachments/attachments.resource.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useMemo } from "react";
import useSWR from "swr";
import { FetchResponse, openmrsFetch } from "@openmrs/esm-framework";
import { AttachmentResponse, UploadedFile } from "../attachments-types";

export const attachmentUrl = "/ws/rest/v1/attachment";

export function getAttachmentByUuid(
attachmentUuid: string,
abortController: AbortController
) {
return openmrsFetch(`${attachmentUrl}/${attachmentUuid}`, {
signal: abortController.signal,
});
}

export function useAttachments(
patientUuid: string,
includeEncounterless: boolean
) {
const { data, error, mutate, isLoading, isValidating } = useSWR<
FetchResponse<{ results: Array<AttachmentResponse> }>
>(
`${attachmentUrl}?patient=${patientUuid}&includeEncounterless=${includeEncounterless}`,
openmrsFetch
);

const results = useMemo(
() => ({
isLoading,
data: data?.data.results ?? [],
error,
mutate,
isValidating,
}),
[data, error, isLoading, isValidating, mutate]
);

return results;
}

export function getAttachments(
patientUuid: string,
includeEncounterless: boolean,
abortController: AbortController
) {
return openmrsFetch(
`${attachmentUrl}?patient=${patientUuid}&includeEncounterless=${includeEncounterless}`,
{
signal: abortController.signal,
}
);
}

export async function createAttachment(
patientUuid: string,
fileToUpload: UploadedFile
) {
const formData = new FormData();

formData.append("fileCaption", fileToUpload.fileName);
formData.append("patient", patientUuid);

if (fileToUpload.file) {
formData.append("file", fileToUpload.file);
} else {
formData.append(
"file",
new File([""], fileToUpload.fileName),
fileToUpload.fileName
);
formData.append("base64Content", fileToUpload.base64Content);
}
return openmrsFetch(`${attachmentUrl}`, {
jona42-ui marked this conversation as resolved.
Show resolved Hide resolved
method: "POST",
body: formData,
});
}

export function deleteAttachmentPermanently(
attachmentUuid: string,
abortController: AbortController
) {
return openmrsFetch(`${attachmentUrl}/${attachmentUuid}`, {
method: "DELETE",
signal: abortController.signal,
});
};

Check failure on line 88 in src/attachments/attachments.resource.tsx

View workflow job for this annotation

GitHub Actions / build

Replace `;` with `⏎`

Check failure on line 88 in src/attachments/attachments.resource.tsx

View workflow job for this annotation

GitHub Actions / build

Unnecessary semicolon
145 changes: 145 additions & 0 deletions src/components/drawing-widget/drawing-widget.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React, { useState, useEffect, useCallback } from "react";
import ReactImageAnnotate, { Annotation } from "react-image-annotate";
import { Add, Crop } from "@carbon/react/icons";
import { useTranslation } from "react-i18next";
import { CardHeader } from "@openmrs/esm-patient-common-lib";
import { Button } from "@carbon/react";


Check failure on line 8 in src/components/drawing-widget/drawing-widget.component.tsx

View workflow job for this annotation

GitHub Actions / build

Delete `⏎`
interface RegionData {
type: string;
x: number;
y: number;
}

export interface ImageData {
src: string;
name: string;
regions: RegionData[];
}

interface DrawingWidgetProps {
selectedImage: string;
taskDescription: string;
imagesData: ImageData[];
regionClsList?: string[];
enabledTools?: string[];
onExit: (annotations: ImageData[]) => void;
}

const DrawingWidget: React.FC<DrawingWidgetProps> = ({ onExit }) => {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [selectedImage] = useState<string | null>(null);
jona42-ui marked this conversation as resolved.
Show resolved Hide resolved
const [annotations, setAnnotations] = useState<ImageData[]>([]);
const [, setLoading] = useState<boolean>(true);
jona42-ui marked this conversation as resolved.
Show resolved Hide resolved
const [activeImage, setActiveImage] = useState<ImageData | null>(null);
const { t } = useTranslation();

Check failure on line 36 in src/components/drawing-widget/drawing-widget.component.tsx

View workflow job for this annotation

GitHub Actions / build

Delete `⏎·`


const CreatePointIcon = () => <Add />;
const CreateBoxIcon = () => <Crop />;
jona42-ui marked this conversation as resolved.
Show resolved Hide resolved

useEffect(() => {
const selectedImageURLParam = new URLSearchParams(
window.location.search
).get("image-url");
jona42-ui marked this conversation as resolved.
Show resolved Hide resolved
if (selectedImageURLParam) {
const activeImage: ImageData = {
src: selectedImageURLParam,
name: "Image from URL",
regions: [],
};
setActiveImage(activeImage);
setLoading(false);
} else {
setLoading(false);
}
}, []);

useEffect(() => {
if (activeImage) {
const initialAnnotations: ImageData[] = [
{
...activeImage,
regions: activeImage.regions || [],
},
];
setAnnotations(initialAnnotations);
}
}, [activeImage]);

const handleExit = useCallback(() => {
onExit(annotations);
jona42-ui marked this conversation as resolved.
Show resolved Hide resolved
}, [onExit, annotations]);

const handleAnnotationChange = useCallback(

Check failure on line 75 in src/components/drawing-widget/drawing-widget.component.tsx

View workflow job for this annotation

GitHub Actions / build

Delete `⏎····`
(newAnnotations: Annotation[]) => {
setAnnotations((prevAnnotations) =>

Check failure on line 77 in src/components/drawing-widget/drawing-widget.component.tsx

View workflow job for this annotation

GitHub Actions / build

Replace `······` with `····`
prevAnnotations.map((prevAnnotation, index) =>
index === 0
? { ...prevAnnotation, regions: newAnnotations }
: prevAnnotation
jona42-ui marked this conversation as resolved.
Show resolved Hide resolved
)
);
},
[]
);

useEffect(() => {
if (selectedImage) {
const image: ImageData = {
src: selectedImage,
name: "Selected Image",
regions: [],
};
setActiveImage(image);
}
}, [selectedImage]);
jona42-ui marked this conversation as resolved.
Show resolved Hide resolved

const images: ImageData[] = selectedFile
? [
{
src: URL.createObjectURL(selectedFile),
name: selectedFile.name,
regions: [
{ type: "point", x: 100, y: 150 },
{ type: "point", x: 200, y: 250 },
],
},
]
: [];

return (
<div className="drawing-widget">
{activeImage || selectedFile ? (
jona42-ui marked this conversation as resolved.
Show resolved Hide resolved
<ReactImageAnnotate
labelImages
regionClsList={["Alpha", "Beta", "Charlie", "Delta"]}
regionTagList={["tag1", "tag2", "tag3"]}
images={images}
onExit={handleExit}
onChange={handleAnnotationChange}
allowComments={true}
toolIcons={{
"create-point": CreatePointIcon,
"create-box": CreateBoxIcon,
}}
/>
) : (
<div>No image to display.</div>
)}
<CardHeader title={t('Add Diagram', 'add diagram')}>
<input
type="file"
onChange={(e) => setSelectedFile(e.target.files?.[0] || null)}
/>

<Button kind="ghost" renderIcon={Add} iconDescription="Add attachment">
{t('add', 'Add')}
</Button>
</CardHeader>
</div>
);
};

export default DrawingWidget;
61 changes: 61 additions & 0 deletions src/draw-page.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React, { useState, useRef } from "react";
import { useTranslation } from "react-i18next";
import DrawingWidget, {
ImageData,
} from "./components/drawing-widget/drawing-widget.component";
import { createAttachment } from "./attachments/attachments.resource";
import { useParams } from 'react-router-dom';
import html2canvas from "html2canvas";

const DrawPage: React.FC = () => {

const { patientUuid } = useParams();
useTranslation();
jona42-ui marked this conversation as resolved.
Show resolved Hide resolved
const [activeImage] = useState<ImageData | null>(null);
jona42-ui marked this conversation as resolved.
Show resolved Hide resolved
const drawingWidgetRef = useRef<HTMLDivElement>(null);

const handleSaveAnnotations = async () => {

// Convert SVG to PNG using html2canvas
if (drawingWidgetRef.current) {
const canvas = await html2canvas(drawingWidgetRef.current);
const pngDataUrl = canvas.toDataURL("image/png");

try {

// const patientUuid = "ca111ca5-d285-47a4-a6ab-60918dcd44ab";
// Make an API request to save the PNG image using the custom createAttachment function
await createAttachment(patientUuid, {
file: new File([pngDataUrl], "annotated_image.png"),
fileName: "annotated_image.png",
fileType: "image/png",
fileDescription: "Annotated Image",
base64Content: "",
});
//Todo
// Handle errors or show a success notification to the user
} catch (error) {
//Todo
// Handle errors or show an error notification to the user
}
}
//TODO
// Display the serialized SVG or PNG.
};


return (
<div>
<div ref={drawingWidgetRef} id="drawing-widget">
<DrawingWidget
selectedImage={activeImage?.src || ""}
jona42-ui marked this conversation as resolved.
Show resolved Hide resolved
taskDescription="Annotate the image"
imagesData={activeImage ? [activeImage] : []}
onExit={handleSaveAnnotations}
/>
</div>
</div>
);
};

export default DrawPage;
10 changes: 5 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { configSchema } from "./config-schema";
const moduleName = "@openmrs/esm-draw-app";

const options = {
featureName: "hello-world",
moduleName,
featureName: "draw",
moduleName: "@openmrs/esm-draw-app",
};

export const importTranslation = require.context(
Expand All @@ -19,7 +19,7 @@ export function startupApp() {
defineConfigSchema(moduleName, configSchema);
}

export const root = getAsyncLifecycle(
() => import("./root.component"),
export const DrawPage = getAsyncLifecycle(
() => import("./draw-page.component"),
options
);
);
17 changes: 0 additions & 17 deletions src/root.component.tsx

This file was deleted.

10 changes: 0 additions & 10 deletions src/root.scss

This file was deleted.

Loading
Loading