diff --git a/src/app/(protected)/workorders/_components/comp-doc/comp-doc-dialog.tsx b/src/app/(protected)/workorders/_components/comp-doc/comp-doc-dialog.tsx
index 74d9c75e..6fe91dd6 100644
--- a/src/app/(protected)/workorders/_components/comp-doc/comp-doc-dialog.tsx
+++ b/src/app/(protected)/workorders/_components/comp-doc/comp-doc-dialog.tsx
@@ -11,6 +11,7 @@ import {
formatMediaSizeLimit,
isWorkOrderDocumentsReadOnly,
isWorkOrderFileTooLarge,
+ MAX_WORK_ORDER_COMPLETION_DOC_BYTES,
} from "@/domain/work-orders/utils/work-order-media-limits";
import { useCompDocDialog } from "@/app/(protected)/workorders/_hooks/use-comp-doc-dialog";
import { CompDocDialogFooter } from "./comp-doc-dialog-footer";
@@ -35,8 +36,8 @@ type UseCompDocPdfUploadArgs = {
};
function rejectCompDocPdf(file: File, rowVersion: string): string | null {
- if (isWorkOrderFileTooLarge(file)) {
- return `File exceeds the ${formatMediaSizeLimit()} limit.`;
+ if (isWorkOrderFileTooLarge(file, MAX_WORK_ORDER_COMPLETION_DOC_BYTES)) {
+ return `File exceeds the ${formatMediaSizeLimit(MAX_WORK_ORDER_COMPLETION_DOC_BYTES)} limit.`;
}
if (file.type !== "application/pdf" && !file.name.toLowerCase().endsWith(".pdf")) {
return "Please upload a PDF file.";
diff --git a/src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-pdf-actions.ts b/src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-pdf-actions.ts
index 5d3b6475..23861a53 100644
--- a/src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-pdf-actions.ts
+++ b/src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-pdf-actions.ts
@@ -11,6 +11,7 @@ import { fmtDocNow } from "@/domain/work-orders/utils/completion-doc-state";
import {
formatMediaSizeLimit,
isWorkOrderFileTooLarge,
+ MAX_WORK_ORDER_COMPLETION_DOC_BYTES,
} from "@/domain/work-orders/utils/work-order-media-limits";
type UseCompDocPdfActionsArgs = {
@@ -55,8 +56,10 @@ export function useCompDocPdfActions({
function handleUploadPdf(files: FileList | null) {
const file = files?.[0];
if (!file) return;
- if (isWorkOrderFileTooLarge(file)) {
- toast.error(`File exceeds the ${formatMediaSizeLimit()} limit.`);
+ if (isWorkOrderFileTooLarge(file, MAX_WORK_ORDER_COMPLETION_DOC_BYTES)) {
+ toast.error(
+ `File exceeds the ${formatMediaSizeLimit(MAX_WORK_ORDER_COMPLETION_DOC_BYTES)} limit.`,
+ );
return;
}
if (file.type !== "application/pdf" && !file.name.toLowerCase().endsWith(".pdf")) {
diff --git a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-completion-tab.tsx b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-completion-tab.tsx
index cf89d2bf..f88e3867 100644
--- a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-completion-tab.tsx
+++ b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-completion-tab.tsx
@@ -11,6 +11,7 @@ import { hasCompletionMediaGallery } from "@/domain/work-orders/utils/partition-
import {
formatMediaSizeLimit,
isWorkOrderFileTooLarge,
+ MAX_WORK_ORDER_COMPLETION_DOC_BYTES,
} from "@/domain/work-orders/utils/work-order-media-limits";
import { SlideOverCompletionActions } from "./slide-over-completion-actions";
import { SlideOverCompletionMedia } from "./slide-over-completion-media";
@@ -71,8 +72,10 @@ export function SlideOverCompletionTab({
const handlePdfSelected = (files: FileList | null) => {
const file = files?.[0];
if (!file || !onUploadCompletionPdf) return;
- if (isWorkOrderFileTooLarge(file)) {
- toast.error(`File exceeds the ${formatMediaSizeLimit()} limit.`);
+ if (isWorkOrderFileTooLarge(file, MAX_WORK_ORDER_COMPLETION_DOC_BYTES)) {
+ toast.error(
+ `File exceeds the ${formatMediaSizeLimit(MAX_WORK_ORDER_COMPLETION_DOC_BYTES)} limit.`,
+ );
return;
}
if (file.type !== "application/pdf" && !file.name.toLowerCase().endsWith(".pdf")) {
diff --git a/src/domain/work-orders/utils/work-order-media-limits.ts b/src/domain/work-orders/utils/work-order-media-limits.ts
index 5d566ccd..00d9d261 100644
--- a/src/domain/work-orders/utils/work-order-media-limits.ts
+++ b/src/domain/work-orders/utils/work-order-media-limits.ts
@@ -1,7 +1,10 @@
import { isWorkOrderCompletionSurfaceReadOnly } from "@/domain/work-orders/utils/work-order-lock";
-/** Backend RequestSizeLimit for media and completion-doc uploads. */
-export const MAX_WORK_ORDER_MEDIA_BYTES = 50_000_000;
+/** Backend RequestSizeLimit for media uploads and Extra Docs. */
+export const MAX_WORK_ORDER_MEDIA_BYTES = 200_000_000;
+
+/** Backend RequestSizeLimit for completion-document uploads. */
+export const MAX_WORK_ORDER_COMPLETION_DOC_BYTES = 50_000_000;
const MEDIA_MIME_TYPES = new Set([
"image/jpeg",
diff --git a/src/test/app/(protected)/workorders/media-upload-modal-api.test.tsx b/src/test/app/(protected)/workorders/media-upload-modal-api.test.tsx
index 7202587d..48c4241c 100644
--- a/src/test/app/(protected)/workorders/media-upload-modal-api.test.tsx
+++ b/src/test/app/(protected)/workorders/media-upload-modal-api.test.tsx
@@ -117,7 +117,7 @@ describe("MediaUploadModal API wiring", () => {
});
expect(screen.getByText("Photos & Videos")).toBeInTheDocument();
expect(
- screen.getByText("Accepted: JPG, PNG, MP4, MOV · Max 50MB per file"),
+ screen.getByText("Accepted: JPG, PNG, MP4, MOV · Max 200MB per file"),
).toBeInTheDocument();
expect(screen.queryByText(/categorize after upload/i)).not.toBeInTheDocument();
});
diff --git a/src/test/app/(protected)/workorders/media-uploader-dropzone.test.tsx b/src/test/app/(protected)/workorders/media-uploader-dropzone.test.tsx
index 35da0ef4..48ccb432 100644
--- a/src/test/app/(protected)/workorders/media-uploader-dropzone.test.tsx
+++ b/src/test/app/(protected)/workorders/media-uploader-dropzone.test.tsx
@@ -1,10 +1,17 @@
import { createRef } from "react";
-import { screen } from "@testing-library/react";
+import { fireEvent, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
+import { toast } from "react-toastify";
+import { MediaUploader } from "@/app/(protected)/workorders/_components/media/media-uploader";
import { MediaUploaderDropzone } from "@/app/(protected)/workorders/_components/media/media-uploader-dropzone";
-import { workOrderMediaAcceptAttribute } from "@/domain/work-orders/utils/work-order-media-limits";
+import {
+ MAX_WORK_ORDER_MEDIA_BYTES,
+ workOrderMediaAcceptAttribute,
+} from "@/domain/work-orders/utils/work-order-media-limits";
import { renderWithProviders } from "@/test/test-utils";
+vi.mock("react-toastify", () => ({ toast: { error: vi.fn() } }));
+
describe("MediaUploaderDropzone", () => {
it("matches the AAP Photos accept copy and keeps it media-only", () => {
renderWithProviders(
@@ -16,7 +23,7 @@ describe("MediaUploaderDropzone", () => {
);
expect(
- screen.getByText("Accepted: JPG, PNG, MP4, MOV · Max 50MB per file"),
+ screen.getByText("Accepted: JPG, PNG, MP4, MOV · Max 200MB per file"),
).toBeInTheDocument();
expect(screen.queryByText(/categorize after upload/i)).not.toBeInTheDocument();
expect(screen.queryByText(/PDF/)).not.toBeInTheDocument();
@@ -25,4 +32,19 @@ describe("MediaUploaderDropzone", () => {
expect(input?.getAttribute("accept")).toBe(workOrderMediaAcceptAttribute());
expect(input?.getAttribute("accept")).not.toContain(".pdf");
});
+
+ it("rejects an oversized media file with the generic 200 MB toast", () => {
+ renderWithProviders();
+
+ const oversizedFile = {
+ name: "mobile-clip.MP4",
+ type: "application/octet-stream",
+ size: MAX_WORK_ORDER_MEDIA_BYTES + 1,
+ } as File;
+ const input = document.querySelector('input[type="file"]');
+
+ fireEvent.change(input as HTMLInputElement, { target: { files: [oversizedFile] } });
+
+ expect(toast.error).toHaveBeenCalledWith('"mobile-clip.MP4" exceeds the 200 MB limit.');
+ });
});
diff --git a/src/test/app/(protected)/workorders/slide-over-extras-aveta.test.tsx b/src/test/app/(protected)/workorders/slide-over-extras-aveta.test.tsx
index ba674966..7dcdc7d2 100644
--- a/src/test/app/(protected)/workorders/slide-over-extras-aveta.test.tsx
+++ b/src/test/app/(protected)/workorders/slide-over-extras-aveta.test.tsx
@@ -140,13 +140,13 @@ describe("SlideOverExtrasTab Aveta", () => {
expect(getMediaContent).not.toHaveBeenCalled();
});
- it("shows Extra Docs accept copy with PDF DOC and 50MB", () => {
+ it("shows Extra Docs accept copy with PDF DOC and 200MB", () => {
renderWithProviders(
,
);
expect(
- screen.getByText("Accepted: JPG, PNG, MP4, MOV, PDF, DOC · Max 50MB per file"),
+ screen.getByText("Accepted: JPG, PNG, MP4, MOV, PDF, DOC · Max 200MB per file"),
).toBeInTheDocument();
const input = document.querySelector('input[type="file"]');
expect(input?.getAttribute("accept")).toContain(".pdf");
diff --git a/src/test/domain/work-orders/utils/work-order-media-limits.test.ts b/src/test/domain/work-orders/utils/work-order-media-limits.test.ts
index 9cf3bc76..da86cd31 100644
--- a/src/test/domain/work-orders/utils/work-order-media-limits.test.ts
+++ b/src/test/domain/work-orders/utils/work-order-media-limits.test.ts
@@ -6,6 +6,7 @@ import {
isWorkOrderExtraDocTypeAllowed,
isWorkOrderFileTooLarge,
isWorkOrderMediaTypeAllowed,
+ MAX_WORK_ORDER_COMPLETION_DOC_BYTES,
MAX_WORK_ORDER_MEDIA_BYTES,
workOrderExtraDocsAcceptAttribute,
workOrderMediaAcceptAttribute,
@@ -24,13 +25,31 @@ describe("work-order-media-limits", () => {
expect(canDeleteWorkOrderMedia({ id: "0" })).toBe(false);
});
- it("caps uploads at 50_000_000 bytes and formats decimal MB", () => {
- expect(MAX_WORK_ORDER_MEDIA_BYTES).toBe(50_000_000);
- expect(formatMediaSizeLimit()).toBe("50 MB");
+ it("caps uploads at 200_000_000 bytes and formats decimal MB", () => {
+ expect(MAX_WORK_ORDER_MEDIA_BYTES).toBe(200_000_000);
+ expect(formatMediaSizeLimit()).toBe("200 MB");
+ expect(isWorkOrderFileTooLarge({ size: MAX_WORK_ORDER_MEDIA_BYTES } as File)).toBe(false);
expect(isWorkOrderFileTooLarge({ size: MAX_WORK_ORDER_MEDIA_BYTES + 1 } as File)).toBe(true);
expect(isWorkOrderFileTooLarge({ size: 10 } as File)).toBe(false);
});
+ it("caps completion-document uploads at 50_000_000 bytes and formats the completion copy", () => {
+ expect(MAX_WORK_ORDER_COMPLETION_DOC_BYTES).toBe(50_000_000);
+ expect(formatMediaSizeLimit(MAX_WORK_ORDER_COMPLETION_DOC_BYTES)).toBe("50 MB");
+ expect(
+ isWorkOrderFileTooLarge(
+ { size: MAX_WORK_ORDER_COMPLETION_DOC_BYTES } as File,
+ MAX_WORK_ORDER_COMPLETION_DOC_BYTES,
+ ),
+ ).toBe(false);
+ expect(
+ isWorkOrderFileTooLarge(
+ { size: MAX_WORK_ORDER_COMPLETION_DOC_BYTES + 1 } as File,
+ MAX_WORK_ORDER_COMPLETION_DOC_BYTES,
+ ),
+ ).toBe(true);
+ });
+
it("allows only JPG PNG MP4 MOV by MIME or extension", () => {
expect(isWorkOrderMediaTypeAllowed({ name: "a.jpg", type: "image/jpeg" } as File)).toBe(true);
expect(isWorkOrderMediaTypeAllowed({ name: "a.jpg", type: "image/jpg" } as File)).toBe(true);
@@ -39,7 +58,19 @@ describe("work-order-media-limits", () => {
expect(isWorkOrderMediaTypeAllowed({ name: "a.mov", type: "video/quicktime" } as File)).toBe(
true,
);
+ expect(isWorkOrderMediaTypeAllowed({ name: "a.MP4", type: "" } as File)).toBe(true);
+ expect(isWorkOrderMediaTypeAllowed({ name: "a.MOV", type: "" } as File)).toBe(true);
+ expect(isWorkOrderMediaTypeAllowed({ name: "a.mov", type: "video/quicktime" } as File)).toBe(
+ true,
+ );
+ expect(
+ isWorkOrderMediaTypeAllowed({ name: "a.mp4", type: "application/octet-stream" } as File),
+ ).toBe(true);
expect(isWorkOrderMediaTypeAllowed({ name: "a.gif", type: "image/gif" } as File)).toBe(false);
+ expect(isWorkOrderMediaTypeAllowed({ name: "a.webm", type: "video/webm" } as File)).toBe(false);
+ expect(
+ isWorkOrderMediaTypeAllowed({ name: "a.txt", type: "application/octet-stream" } as File),
+ ).toBe(false);
expect(isWorkOrderMediaTypeAllowed({ name: "a.pdf", type: "application/pdf" } as File)).toBe(
false,
);