diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5d215641..0d479945 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,7 +21,8 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-image-label": "^1.3.4", - "react-router-dom": "^6.26.0" + "react-router-dom": "^6.26.0", + "zustand": "^5.0.1" }, "devDependencies": { "@eslint/js": "^9.8.0", @@ -1521,14 +1522,14 @@ "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.4", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz", "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -2526,7 +2527,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/data-urls": { @@ -7393,6 +7394,34 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.1.tgz", + "integrity": "sha512-pRET7Lao2z+n5R/HduXMio35TncTlSW68WsYBq2Lg1ASspsNGjpwLAsij3RpouyV6+kHMwwwzP0bZPD70/Jx/w==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/frontend/package.json b/frontend/package.json index 08dbd394..1efcaf81 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,7 +28,8 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-image-label": "^1.3.4", - "react-router-dom": "^6.26.0" + "react-router-dom": "^6.26.0", + "zustand": "^5.0.1" }, "devDependencies": { "@eslint/js": "^9.8.0", diff --git a/frontend/src/components/ImageAnnotator.tsx b/frontend/src/components/ImageAnnotator.tsx index ec1d14fc..1c39439e 100644 --- a/frontend/src/components/ImageAnnotator.tsx +++ b/frontend/src/components/ImageAnnotator.tsx @@ -2,6 +2,7 @@ import { FC, useEffect } from 'react'; import { ImageAnnotator, Shape } from 'react-image-label'; import { useAnnotationContext } from '../contexts/AnnotationContext'; import { LABELS } from '../constants/labels'; +import {useCreateTemplateStore} from "../types/templates.ts"; interface MultiImageAnnotatorProps { images: string[]; @@ -11,7 +12,8 @@ interface MultiImageAnnotatorProps { export const MultiImageAnnotator: FC = ({ images, initialShapes = [[]] }) => { const {selectedField, setHandles, annotator, shapes, setShapes, index, setDrawnFields, drawnFields, setSelectedField} = useAnnotationContext(); - + const storeShapes = useCreateTemplateStore((state) => state.setShapes); + const getStoredShapes = useCreateTemplateStore((state) => state.shapes); @@ -24,7 +26,8 @@ export const MultiImageAnnotator: FC = ({ images, init // for field?.color.slice(0,7) to remove the alpha channel from the hexcode updatedShapes[index] = [...(updatedShapes[index] || []), {...shape, field: selectedField?.name as string, color: field?.color, id: tempFieldsSet.size}]; setShapes(updatedShapes); - localStorage.setItem('shapes', JSON.stringify(updatedShapes)); + setShapes(updatedShapes); + storeShapes(updatedShapes); annotator?.updateCategories(shape.id, [], `${field?.color}4D`); setDrawnFields(tempFieldsSet); setSelectedField(null); @@ -35,9 +38,8 @@ export const MultiImageAnnotator: FC = ({ images, init annotator?.edit(shape.id); }; useEffect(() => { - const getShapes = async () => { - const localStorageShapes = await JSON.parse(localStorage.getItem('shapes') || '[]') as unknown as Array> || []; - setShapes(localStorageShapes.length > 0 ? localStorageShapes : initialShapes); + const getShapes = () => { + setShapes(getStoredShapes); } getShapes(); }, []) diff --git a/frontend/src/pages/AnnotateTemplate.tsx b/frontend/src/pages/AnnotateTemplate.tsx index cb80b22a..c48d8842 100644 --- a/frontend/src/pages/AnnotateTemplate.tsx +++ b/frontend/src/pages/AnnotateTemplate.tsx @@ -16,6 +16,8 @@ import CheckIcon from '../assets/check.svg'; import Toolbar from "../components/Toolbar.tsx"; import "./AnnotateTemplate.scss"; +import {useMutation, useQuery} from "@tanstack/react-query"; +import {TemplateAPI, useCreateTemplateStore} from "../types/templates.ts"; interface AccordionItemProps { title: React.ReactNode | string; @@ -48,6 +50,9 @@ export interface ImageData { const AnnotateTemplate: React.FC = () => { const [images, setImages] = useState([]); + const storeImages = useCreateTemplateStore((state) => state.setBaseImages); + const baseImages = useCreateTemplateStore((state) => state.baseImages); + const [localIds, setLocalIds] = useState>(new Map()); const navigate = useNavigate(); const { files } = useFiles(); @@ -94,17 +99,16 @@ const AnnotateTemplate: React.FC = () => { }; convertPdfToImages(pdfFile).then((imgs) => { + // TODO: This is redundant, we should just store the images in the store setImages(imgs); - localStorage.setItem('images', JSON.stringify(imgs)); + storeImages(imgs); }); }, [files, pdfFile]); useEffect(() => { const getImage = async () => { - const localImages = await JSON.parse( - localStorage.getItem('images') || "[]" - ); + const localImages = baseImages; if (localImages && localImages.length > 0) { - setImages(localImages.images); + setImages(localImages); } }; @@ -112,6 +116,7 @@ const AnnotateTemplate: React.FC = () => { setDrawnFields(new Set()); setSelectedField(null); } + // TODO: Ask Kevin what this is for, it's not clear getImage(); return () => handleUnmount(); }, [setDrawnFields, setSelectedField]); diff --git a/frontend/src/pages/SaveTemplate.tsx b/frontend/src/pages/SaveTemplate.tsx index 9f146353..555d2ecf 100644 --- a/frontend/src/pages/SaveTemplate.tsx +++ b/frontend/src/pages/SaveTemplate.tsx @@ -1,34 +1,49 @@ -import { Label, TextInput } from "@trussworks/react-uswds"; -import { Divider } from "../components/Divider"; -import { UploadHeader } from "../components/Header"; -import { Stepper } from "../components/Stepper"; -import { AnnotateStep } from "../utils/constants"; -import { useNavigate } from "react-router-dom"; -import { useAnnotationContext } from "../contexts/AnnotationContext"; -import { FileType, Page } from "../contexts/FilesContext"; +import {Label, TextInput} from "@trussworks/react-uswds"; +import {Divider} from "../components/Divider"; +import {UploadHeader} from "../components/Header"; +import {Stepper} from "../components/Stepper"; +import {AnnotateStep} from "../utils/constants"; +import {useNavigate} from "react-router-dom"; +import {useAnnotationContext} from "../contexts/AnnotationContext"; +import {FileType, Page} from "../contexts/FilesContext"; import hexRgb from "hex-rgb"; -import { ImageData } from "./AnnotateTemplate"; -import { makeScreenshots } from "../utils/functions"; - +import {makeScreenshots} from "../utils/functions"; +import {useCreateTemplateStore} from "../types/templates.ts"; export const SaveTemplate = () => { const navigate = useNavigate(); - const { fields, setDescription, setName, name, description, shapes, setShapes, setFields, setDrawnFields, setSelectedField } = useAnnotationContext() - + const { + fields, + setDescription, + setName, + name, + description, + shapes, + setShapes, + setFields, + setDrawnFields, + setSelectedField + } = useAnnotationContext() + const baseImages = useCreateTemplateStore((state) => state.baseImages); + const storeTemplateImages = useCreateTemplateStore((state) => state.setTemplateImages); + const reset = useCreateTemplateStore((state) => state.reset); + const handleSubmit = async () => { - const images: ImageData[] = localStorage.getItem('images') ? JSON.parse(localStorage.getItem('images') as string) : []; + // const images: ImageData[] = localStorage.getItem('images') ? JSON.parse(localStorage.getItem('images') as string) : []; + const images = baseImages; let pages: Page[] = []; const tempFields = fields.filter(field => field.size > 0); - - const screenshots = await makeScreenshots() + + const screenshots = await makeScreenshots(images, shapes); + storeTemplateImages(screenshots); if (images.length > 0) { pages = tempFields.map((_, index) => { const shape = shapes[index] return { fieldNames: shape.map(s => { - const { red, green, blue } = hexRgb(s.color as string); + const {red, green, blue} = hexRgb(s.color as string); return { type: 'text', color: `${red},${green},${blue}`, @@ -44,34 +59,31 @@ export const SaveTemplate = () => { name, description, pages: pages - - } - let existingTemplates = [] - try { - const data = localStorage.getItem('templates'); - if (data) { - existingTemplates = JSON.parse(data); } - } catch { - console.error("Invalid information found in templates, it will be overwritten") - } - localStorage.setItem('templates', JSON.stringify([...existingTemplates, tempFile])) + let existingTemplates = [] + try { + const data = localStorage.getItem('templates'); + if (data) { + existingTemplates = JSON.parse(data); + } + + } catch { + console.error("Invalid information found in templates, it will be overwritten") + } + // TODO: Need to persist this to the backend + localStorage.setItem('templates', JSON.stringify([...existingTemplates, tempFile])) } setShapes([]); setFields([new Set(), new Set()]); setDrawnFields(new Set()); setSelectedField(null); - localStorage.setItem('shapes', ''); - localStorage.setItem('images', ''); - localStorage.setItem('screenshots', ''); - localStorage.setItem('images', ''); - localStorage.setItem('files', ''); + reset(); navigate("/") - } - + } + return (
diff --git a/frontend/src/types/templates.ts b/frontend/src/types/templates.ts index cd293dd5..4eb0c53b 100644 --- a/frontend/src/types/templates.ts +++ b/frontend/src/types/templates.ts @@ -1,4 +1,6 @@ import {Organization, Page, User} from "./models.ts"; +import {create} from "zustand"; +import {Shape} from "react-image-label"; export interface Template { id: string; @@ -7,15 +9,16 @@ export interface Template { labName: string pages: Page[]; status: TemplateStatus; - createdBy: User; + createdBy: User | null; updatedBy: User | undefined; - createdAt: Date; + createdAt: Date | null; updatedAt: Date | undefined; - organization: Organization; + organization: Organization | null; } type TemplateStatus = "Completed" | "In Progress" | "Deprecated" + const MIDDLEWARE_URL = import.meta.env.MIDDLEWARE_API_URL || 'http://localhost:8081'; export const TemplateAPI = { getTemplates: async (): Promise => { @@ -25,5 +28,19 @@ export const TemplateAPI = { } const jsonResponse = await response.json(); return jsonResponse._embedded.templates; - } + }, } + +// This is the store for the new template, basically we can treat this as we were treating local storage +export const useCreateTemplateStore = create((set) => ({ + baseImages: [], + shapes: [], + templateImages: [], + reset: () => { + set({baseImages: [], shapes: [], templateImages: []}) + }, + setTemplateImages: (images: string[]) => set({templateImages: images}), + setShapes: (shapes: Shape[]) => set({shapes: shapes}), + setBaseImages: (images: ImageData[]) => set({baseImages: images}), + +})); diff --git a/frontend/src/utils/functions.ts b/frontend/src/utils/functions.ts index bddd76c6..65c02ee4 100644 --- a/frontend/src/utils/functions.ts +++ b/frontend/src/utils/functions.ts @@ -1,10 +1,12 @@ import { CustomShape } from "../contexts/AnnotationContext"; import { ImageData } from "../pages/AnnotateTemplate"; +import {useCreateTemplateStore} from "../types/templates.ts"; -export const makeScreenshots = async () => { - const images: ImageData[] = JSON.parse(localStorage.getItem('images') || '[]') as ImageData[]; - const shapes: CustomShape[][] = JSON.parse(localStorage.getItem('shapes') || '[]') as CustomShape[][]; +export const makeScreenshots = async (images, shapes) => { + // const images: ImageData[] = JSON.parse(localStorage.getItem('images') || '[]') as ImageData[]; + // const shapes: CustomShape[][] = JSON.parse(localStorage.getItem('shapes') || '[]') as CustomShape[][]; const screenshots: string[] = []; + for (let i = 0; i < images.length; i++) { try { const screenshot = await createScreenshot(images[i], shapes[i] ?? []); @@ -15,7 +17,7 @@ export const makeScreenshots = async () => { } // Final log of all screenshots - localStorage.setItem('screenshots', JSON.stringify(screenshots)); + // localStorage.setItem('screenshots', JSON.stringify(screenshots)); return screenshots; };