diff --git a/frontend/src/core/components/fileEditor/FileEditorFileName.test.tsx b/frontend/src/core/components/fileEditor/FileEditorFileName.test.tsx new file mode 100644 index 0000000000..4d153ef081 --- /dev/null +++ b/frontend/src/core/components/fileEditor/FileEditorFileName.test.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { describe, expect, test } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { MantineProvider } from "@mantine/core"; +import FileEditorFileName from "@app/components/fileEditor/FileEditorFileName"; +import type { StirlingFileStub } from "@app/types/fileContext"; +import type { FileId } from "@app/types/file"; + +const buildFileStub = ( + overrides: Partial = {}, +): StirlingFileStub => ({ + id: "file-1" as FileId, + name: "report.pdf", + type: "application/pdf", + size: 1024, + lastModified: 0, + isLeaf: true, + originalFileId: "file-1", + versionNumber: 1, + ...overrides, +}); + +const renderName = (file: StirlingFileStub) => + render( + + + , + ); + +describe("FileEditorFileName (core / web)", () => { + test.each([ + { name: "not-saved", file: buildFileStub() }, + { + name: "dirty", + file: buildFileStub({ localFilePath: "/tmp/report.pdf", isDirty: true }), + }, + { + name: "saved", + file: buildFileStub({ localFilePath: "/tmp/report.pdf", isDirty: false }), + }, + ])("does not render a save indicator ($name)", ({ file }) => { + renderName(file); + + expect(screen.queryByLabelText("fileNotSavedToDisk")).toBeNull(); + expect(screen.queryByLabelText("unsavedChanges")).toBeNull(); + expect(screen.queryByLabelText("fileSavedToDisk")).toBeNull(); + }); + + test("renders the filename", () => { + renderName(buildFileStub({ name: "invoice.pdf" })); + expect(screen.getByText("invoice.pdf")).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/core/components/fileEditor/FileEditorFileName.tsx b/frontend/src/core/components/fileEditor/FileEditorFileName.tsx index b526e85613..89c2e7f084 100644 --- a/frontend/src/core/components/fileEditor/FileEditorFileName.tsx +++ b/frontend/src/core/components/fileEditor/FileEditorFileName.tsx @@ -1,13 +1,18 @@ import React from "react"; import { StirlingFileStub } from "@app/types/fileContext"; import { PrivateContent } from "@app/components/shared/PrivateContent"; +import { truncateCenter } from "@app/utils/textUtils"; interface FileEditorFileNameProps { file: StirlingFileStub; + maxLength?: number; } -const FileEditorFileName = ({ file }: FileEditorFileNameProps) => ( - {file.name} +const FileEditorFileName = ({ + file, + maxLength = 40, +}: FileEditorFileNameProps) => ( + {truncateCenter(file.name, maxLength)} ); export default FileEditorFileName; diff --git a/frontend/src/core/components/fileEditor/FileEditorThumbnail.tsx b/frontend/src/core/components/fileEditor/FileEditorThumbnail.tsx index aad5442596..a27c419adc 100644 --- a/frontend/src/core/components/fileEditor/FileEditorThumbnail.tsx +++ b/frontend/src/core/components/fileEditor/FileEditorThumbnail.tsx @@ -27,7 +27,6 @@ import { } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import { StirlingFileStub } from "@app/types/fileContext"; import { zipFileService } from "@app/services/zipFileService"; -import { useFileThumbnail } from "@app/hooks/useFileThumbnail"; import styles from "@app/components/fileEditor/FileEditorThumbnail.module.css"; import { useFileContext } from "@app/contexts/FileContext"; @@ -43,6 +42,7 @@ import UploadToServerModal from "@app/components/shared/UploadToServerModal"; import ShareFileModal from "@app/components/shared/ShareFileModal"; import { useAppConfig } from "@app/contexts/AppConfigContext"; import { truncateCenter } from "@app/utils/textUtils"; +import { useFileThumbnail } from "@app/hooks/useFileThumbnail"; import DocumentThumbnail from "@app/components/shared/filePreview/DocumentThumbnail"; interface FileEditorThumbnailProps { @@ -86,11 +86,6 @@ const FileEditorThumbnail = ({ } = useFileContext(); const { state, selectors } = useFileState(); const isMobile = useIsMobile(); - const { - isEncrypted, - thumbnail: displayThumbnail, - isGenerating: isThumbGenerating, - } = useFileThumbnail(file); const actualFile = useMemo( () => activeFiles.find((f) => f.fileId === file.id), @@ -102,6 +97,11 @@ const FileEditorThumbnail = ({ const hasError = state.ui.errorFileIds.includes(file.id); const pageCount = file.processedFile?.totalPages || 0; + const { + isEncrypted, + thumbnail: displayThumbnail, + isGenerating: isThumbGenerating, + } = useFileThumbnail(file); // Aspect ratio from page dimensions, falling back to letter size const firstPage = file.processedFile?.pages?.[0]; diff --git a/frontend/src/desktop/components/fileEditor/FileEditorFileName.test.tsx b/frontend/src/desktop/components/fileEditor/FileEditorFileName.test.tsx new file mode 100644 index 0000000000..10bd62488f --- /dev/null +++ b/frontend/src/desktop/components/fileEditor/FileEditorFileName.test.tsx @@ -0,0 +1,75 @@ +import React from "react"; +import { describe, expect, test } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { MantineProvider } from "@mantine/core"; +import FileEditorFileName from "@app/components/fileEditor/FileEditorFileName"; +import type { StirlingFileStub } from "@app/types/fileContext"; +import type { FileId } from "@app/types/file"; + +const buildFileStub = ( + overrides: Partial = {}, +): StirlingFileStub => ({ + id: "file-1" as FileId, + name: "report.pdf", + type: "application/pdf", + size: 1024, + lastModified: 0, + isLeaf: true, + originalFileId: "file-1", + versionNumber: 1, + ...overrides, +}); + +const renderName = (file: StirlingFileStub) => + render( + + + , + ); + +describe("FileEditorFileName (desktop)", () => { + test("renders red 'not saved' indicator when file has no local path", () => { + renderName(buildFileStub()); + + const indicator = screen.getByLabelText("fileNotSavedToDisk"); + expect(indicator).toBeInTheDocument(); + expect(indicator).toHaveStyle({ + backgroundColor: "var(--mantine-color-red-6)", + }); + expect(screen.queryByLabelText("unsavedChanges")).toBeNull(); + expect(screen.queryByLabelText("fileSavedToDisk")).toBeNull(); + }); + + test("renders yellow 'unsaved changes' indicator when file is dirty", () => { + renderName( + buildFileStub({ localFilePath: "/tmp/report.pdf", isDirty: true }), + ); + + const indicator = screen.getByLabelText("unsavedChanges"); + expect(indicator).toBeInTheDocument(); + expect(indicator).toHaveStyle({ + backgroundColor: "var(--mantine-color-yellow-6)", + }); + expect(screen.queryByLabelText("fileNotSavedToDisk")).toBeNull(); + expect(screen.queryByLabelText("fileSavedToDisk")).toBeNull(); + }); + + test("renders green 'saved' indicator when file is persisted and clean", () => { + renderName( + buildFileStub({ localFilePath: "/tmp/report.pdf", isDirty: false }), + ); + + const indicator = screen.getByLabelText("fileSavedToDisk"); + expect(indicator).toBeInTheDocument(); + expect(indicator).toHaveStyle({ + backgroundColor: "var(--mantine-color-green-6)", + }); + expect(screen.queryByLabelText("fileNotSavedToDisk")).toBeNull(); + expect(screen.queryByLabelText("unsavedChanges")).toBeNull(); + }); + + test("renders the filename alongside the indicator", () => { + renderName(buildFileStub({ name: "invoice.pdf" })); + expect(screen.getByText("invoice.pdf")).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/desktop/components/fileEditor/FileEditorFileName.tsx b/frontend/src/desktop/components/fileEditor/FileEditorFileName.tsx index c173a4dc2f..46349fd31a 100644 --- a/frontend/src/desktop/components/fileEditor/FileEditorFileName.tsx +++ b/frontend/src/desktop/components/fileEditor/FileEditorFileName.tsx @@ -3,17 +3,22 @@ import { Tooltip } from "@mantine/core"; import { useTranslation } from "react-i18next"; import { StirlingFileStub } from "@app/types/fileContext"; import { PrivateContent } from "@app/components/shared/PrivateContent"; +import { truncateCenter } from "@app/utils/textUtils"; interface FileEditorFileNameProps { file: StirlingFileStub; + maxLength?: number; } -const FileEditorFileName = ({ file }: FileEditorFileNameProps) => { +const FileEditorFileName = ({ + file, + maxLength = 40, +}: FileEditorFileNameProps) => { const { t } = useTranslation(); return ( <> - {file.name} + {truncateCenter(file.name, maxLength)} {!file.localFilePath && (