From 95c39b46485df9acb19daaf73b63a12456d38956 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Tue, 5 May 2026 12:19:11 +0100 Subject: [PATCH] Fix missing desktop save indicator on files (#6310) # Description of Changes Save indicator stopped showing up after #6050, which fixed the missing truncation on filenames, but accidentally bypassed the save indicator component at the same time. This PR puts the component back in and makes it support truncation so we can have both. image --- .../fileEditor/FileEditorFileName.test.tsx | 53 +++++++++++++ .../fileEditor/FileEditorFileName.tsx | 9 ++- .../fileEditor/FileEditorThumbnail.tsx | 17 ++++- .../fileEditor/FileEditorFileName.test.tsx | 75 +++++++++++++++++++ .../fileEditor/FileEditorFileName.tsx | 9 ++- 5 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 frontend/src/core/components/fileEditor/FileEditorFileName.test.tsx create mode 100644 frontend/src/desktop/components/fileEditor/FileEditorFileName.test.tsx 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 000000000..4d153ef08 --- /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 b526e8561..89c2e7f08 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 81ea1d27d..cf5ca7da2 100644 --- a/frontend/src/core/components/fileEditor/FileEditorThumbnail.tsx +++ b/frontend/src/core/components/fileEditor/FileEditorThumbnail.tsx @@ -45,7 +45,7 @@ import { PrivateContent } from "@app/components/shared/PrivateContent"; 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 FileEditorFileName from "@app/components/fileEditor/FileEditorFileName"; interface FileEditorThumbnailProps { file: StirlingFileStub; @@ -580,8 +580,19 @@ const FileEditorThumbnail = ({ marginBottom: "0.5rem", }} > - - {truncateCenter(file.name, 40)} + + = {}, +): 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 c173a4dc2..46349fd31 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 && (