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

issue #276: fetching images from backend #479

Merged
merged 9 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const customJestConfig = {
preset: "ts-jest",
testEnvironment: "jsdom",
moduleNameMapper: {
"swiper": "identity-obj-proxy",
swiper: "identity-obj-proxy",
"\\.(css|less|scss|sss|styl)$": "identity-obj-proxy",
"^@/(.*)$": "<rootDir>/src/$1",
},
Expand Down
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"jest-environment-jsdom": "^29.7.0",
"js-cookie": "^3.0.5",
"libphonenumber-js": "^1.11.19",
"magic-bytes.js": "^1.10.0",
"next": "15.1.6",
"postcss": "^8.5.2",
"react": "^19",
Expand Down
3 changes: 2 additions & 1 deletion public/locales/en/inspectionPage.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"getInspectionFailed": "Failed to fetch inspection details",
"deleteSuccess": "Inspection deleted successfully",
"discardFailed": "Failed to discard inspection",
"internalErrorLabelNotFound": "Internal error: Label not found"
"internalErrorLabelNotFound": "Internal error: Label not found",
"getPictureSetFailed": "Failed to get picture set"
},
"alt": {
"edit": "Edit inspection",
Expand Down
6 changes: 4 additions & 2 deletions public/locales/en/labelDataValidator.json
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@
"duplicateUnit": "Duplicate unit",
"fetchAborted": "Fetch aborted",
"noFileUploaded": "No file uploaded",
"requestCancelled": "Request cancelled"
"requestCancelled": "Request cancelled",
"folderCreationFailed": "Could not save images. Please try again later or report the issue"
},
"stepper": { "next": "Next", "back": "Back", "submit": "Submit" },
"alert": {
Expand All @@ -199,6 +200,7 @@
"alreadyCompleted": "This inspection has already been completed.",
"labelExtractionFailed": "Label data extraction failed",
"labelExtractionSuccess": "Label data extracted successfully. Saving initial version…",
"initialSaveSuccess": "Initial label data saved successfully"
"initialSaveSuccess": "Initial label data saved successfully",
"getPictureSetFailed": "Failed to get picture set"
}
}
3 changes: 2 additions & 1 deletion public/locales/fr/inspectionPage.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"getInspectionFailed": "Échec de la récupération des détails de l'inspection",
"deleteSuccess": "Inspection supprimée avec succès",
"discardFailed": "Échec de la suppression de l'inspection",
"internalErrorLabelNotFound": "Erreur interne : Étiquette non trouvée"
"internalErrorLabelNotFound": "Erreur interne : Étiquette non trouvée",
"getPictureSetFailed": "Échec de l'obtention de l'ensemble d'images"
},
"alt": {
"edit": "Modifier l'inspection",
Expand Down
6 changes: 4 additions & 2 deletions public/locales/fr/labelDataValidator.json
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@
"duplicateUnit": "Unité dupliquée",
"fetchAborted": "Fetch annulé",
"noFileUploaded": "Aucun fichier téléchargé",
"requestCancelled": "Requête annulée"
"requestCancelled": "Requête annulée",
"folderCreationFailed": "Échec de l’enregistrement des images. Veuillez réessayer plus tard ou signaler le problème"
},
"stepper": {
"next": "Suivant",
Expand All @@ -204,6 +205,7 @@
"failedFetchInspection": "Échec de la récupération de l'inspection.",
"alreadyCompleted": "Cette inspection est déjà terminée.",
"labelExtractionSuccess": "Données de l'étiquette extraites avec succès. Enregistrement de la version initiale…",
"initialSaveSuccess": "Données de l'étiquette initiale enregistrées avec succès"
"initialSaveSuccess": "Données de l'étiquette initiale enregistrées avec succès",
"getPictureSetFailed": "Échec de l'obtention de l'ensemble d'images"
}
}
4 changes: 2 additions & 2 deletions src/app/__tests__/HomePage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable react/display-name */
/* eslint-disable react-hooks/rules-of-hooks */
import FileUploaded from "@/classe/File";
import FileUploaded from "@/classes/File";
import useUploadedFilesStore from "@/stores/fileStore";
import useLabelDataStore from "@/stores/labelDataStore";
import { VERIFIED_LABEL_DATA } from "@/utils/client/constants";
Expand Down Expand Up @@ -311,7 +311,7 @@ describe("HomePage Component", () => {

it("should clear uploaded files and reset label data on mount", () => {
useUploadedFilesStore.getState().addUploadedFile(
FileUploaded.newFile(
new FileUploaded(
{ username: "testUser" },
"/uploads/test1.png",
new File(["dummy content"], "test1.png", {
Expand Down
48 changes: 36 additions & 12 deletions src/app/api-next/extract-label-data/route.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,50 @@
import { handleApiError } from "@/utils/server/apiErrors";
import { MISSING_AUTH_RESPONSE } from "@/utils/server/apiResponses";
import { pipelineApi } from "@/utils/server/backend";
import { mapLabelDataOutputToLabelData } from "@/utils/server/modelTransformation";
import { filesApi, pipelineApi } from "@/utils/server/backend";
import { mapBackendLabelDataToLabelData } from "@/utils/server/modelTransformation";

export async function POST(request: Request) {
const formData = await request.formData();
console.debug("[post extract-label-data] request body (formdata):", formData);
const files = formData.getAll("files") as File[];

const authHeader = request.headers.get("Authorization");
if (!authHeader) {
return MISSING_AUTH_RESPONSE;
}

return pipelineApi
.analyzeDocumentAnalyzePost(files)
.then((analyzeResponse) => {
const formData = await request.formData();
console.debug("[post extract-label-data] request body (formdata):", formData);
const files = formData.getAll("files") as File[];

return Promise.allSettled([
pipelineApi.analyzeDocumentAnalyzePost(files, {
headers: { Authorization: authHeader },
}),
filesApi.createFolderFilesPost(files, {
headers: { Authorization: authHeader },
}),
])
.then(([analyzeResult, folderResult]) => {
if (analyzeResult.status === "rejected") {
throw analyzeResult.reason;
}

console.debug(
"[post extract-label-data] response data:",
analyzeResponse.data,
"[post extract-label-data] analyze response data:",
analyzeResult.value.data,
);
const labelData = mapLabelDataOutputToLabelData(analyzeResponse.data);

if (folderResult.status === "fulfilled") {
console.debug(
"[post extract-label-data] folder response data:",
folderResult.value.data,
);
analyzeResult.value.data.picture_set_id = folderResult.value.data.id;
} else {
console.warn(
"[post extract-label-data] folder creation failed:",
folderResult.reason,
);
}

const labelData = mapBackendLabelDataToLabelData(analyzeResult.value.data);
console.debug("[post extract-label-data] returned labelData:", labelData);
return Response.json(labelData);
})
Expand Down
14 changes: 6 additions & 8 deletions src/app/api-next/inspections/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { MISSING_AUTH_RESPONSE } from "@/utils/server/apiResponses";
import { inspectionsApi } from "@/utils/server/backend";
import {
mapInspectionToLabelData,
mapLabelDataToBackendLabelData,
mapLabelDataToInspectionCreate,
} from "@/utils/server/modelTransformation";

export async function POST(request: Request) {
Expand All @@ -12,16 +12,14 @@ export async function POST(request: Request) {
return MISSING_AUTH_RESPONSE;
}

const formData = await request.formData();
console.debug("[post inspections] request body (formdata):", formData);
const files = formData.getAll("files") as File[];
const labelDataString = formData.get("labelData") as string;
const labelData = JSON.parse(labelDataString);
const labelDataInput = mapLabelDataToBackendLabelData(labelData);
const labelData = await request.json();
console.debug("[post inspections] request body:", labelData);

const labelDataInput = mapLabelDataToInspectionCreate(labelData);
console.debug("[post inspections] sent labelDataInput:", labelDataInput);

return inspectionsApi
.postInspectionInspectionsPost(labelDataInput, files, {
.postInspectionInspectionsPost(labelDataInput, {
headers: { Authorization: authHeader },
})
.then((inspectionsResponse) => {
Expand Down
46 changes: 46 additions & 0 deletions src/app/api-next/pictures/[pictureSetId]/[pictureId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { handleApiError } from "@/utils/server/apiErrors";
import {
createErrorResponse,
MISSING_AUTH_RESPONSE,
} from "@/utils/server/apiResponses";
import { filesApi } from "@/utils/server/backend";
import { validate } from "uuid";

export async function GET(
request: Request,
{ params }: { params: Promise<{ pictureSetId: string; pictureId: string }> },
) {
const authHeader = request.headers.get("Authorization");
if (!authHeader) {
return MISSING_AUTH_RESPONSE;
}

const { pictureSetId, pictureId } = await params;
if (!validate(pictureSetId)) {
return createErrorResponse("Invalid pictureSetId", 400);
}
if (!validate(pictureId)) {
return createErrorResponse("Invalid pictureId", 400);
}

console.debug(
"[get pictures] pictureSetId:",
pictureSetId,
"pictureId:",
pictureId,
);

return filesApi
.getFileFilesFolderIdFileIdGet(pictureSetId, pictureId, {
headers: { Authorization: authHeader },
responseType: "arraybuffer",
})
.then((r) => {
const file = r.data;
console.debug("[get pictures] received File object:", typeof file);
return new Response(file);
})
.catch((error) => {
return handleApiError(error);
});
}
36 changes: 36 additions & 0 deletions src/app/api-next/pictures/[pictureSetId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { handleApiError } from "@/utils/server/apiErrors";
import {
INVALID_ID_RESPONSE,
MISSING_AUTH_RESPONSE,
} from "@/utils/server/apiResponses";
import { filesApi } from "@/utils/server/backend";
import { validate } from "uuid";

export async function GET(
request: Request,
{ params }: { params: Promise<{ pictureSetId: string }> },
) {
const authHeader = request.headers.get("Authorization");
if (!authHeader) {
return MISSING_AUTH_RESPONSE;
}

const pictureSetId = (await params).pictureSetId;
if (!validate(pictureSetId)) {
return INVALID_ID_RESPONSE;
}

console.debug("[get pictures] pictureSetId:", pictureSetId);

return filesApi
.getFolderFilesFolderIdGet(pictureSetId, {
headers: { Authorization: authHeader },
})
.then((r) => {
console.debug("[get pictures] response:", r.data);
return Response.json(r.data);
})
.catch((error) => {
return handleApiError(error);
});
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import FileUploaded from "@/classe/File";
import useUploadedFilesStore from "@/stores/fileStore";
import { QuantityChips } from "@/components/QuantityChips";
import useLabelDataStore from "@/stores/labelDataStore";
import { Quantity } from "@/types/types";
import {
Expand All @@ -9,7 +8,6 @@ import {
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import axios from "axios";
import LabelDataConfirmationPage from "../page";
import { QuantityChips } from "@/components/QuantityChips";

const mockedRouterPush = jest.fn();
jest.mock("next/navigation", () => ({
Expand Down Expand Up @@ -104,31 +102,12 @@ describe("LabelDataConfirmationPage", () => {
);
});

it("should send a POST request with the correct FormData when labelData does not have an inspectionId and include uploaded files", async () => {
const mockFile = new File(["file content"], "test-file.txt", {
type: "text/plain",
});
const mockFileUploaded = new FileUploaded(
{
path: "mock-path",
user: { username: "test-user" },
file: mockFile,
uploadDate: new Date(),
type: "pdf",
name: mockFile.name,
},
"mock-path",
mockFile,
);

it("should send a POST request with the correct JSON payload when labelData does not have an inspectionId", async () => {
const mockPost = jest.spyOn(axios, "post").mockResolvedValueOnce({
data: { inspectionId: "5678" },
});

useLabelDataStore.setState({ labelData: VERIFIED_LABEL_DATA });
useUploadedFilesStore.setState({
uploadedFiles: [mockFileUploaded],
});

const mockSetLabelData = jest.fn();
jest
Expand All @@ -146,21 +125,15 @@ describe("LabelDataConfirmationPage", () => {

expect(mockPost).toHaveBeenCalledWith(
"/api-next/inspections",
expect.any(FormData),
VERIFIED_LABEL_DATA,
expect.objectContaining({
headers: expect.any(Object),
headers: expect.objectContaining({
Authorization: expect.any(String),
"Content-Type": "application/json",
}),
signal: expect.any(AbortSignal),
}),
);

const capturedFormData = mockPost.mock.calls[0][1] as FormData;

expect(capturedFormData.get("labelData")).toEqual(
JSON.stringify(VERIFIED_LABEL_DATA),
);
const fileEntries = capturedFormData.getAll("files") as File[];
expect(fileEntries.length).toBe(1);
expect(fileEntries[0]).toEqual(mockFile);
});
});

Expand Down
Loading
Loading