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.

<img width="586" height="166" alt="image"
src="https://github.com/user-attachments/assets/529c3dcb-ee00-4a6d-ae53-ef8657204369"
/>
This commit is contained in:
James Brunton
2026-05-05 11:19:11 +00:00
committed by GitHub
parent 44f2d0d679
commit 95c39b4648
5 changed files with 156 additions and 7 deletions
@@ -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> = {},
): 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(
<MantineProvider>
<FileEditorFileName file={file} />
</MantineProvider>,
);
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();
});
});
@@ -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) => (
<PrivateContent>{file.name}</PrivateContent>
const FileEditorFileName = ({
file,
maxLength = 40,
}: FileEditorFileNameProps) => (
<PrivateContent>{truncateCenter(file.name, maxLength)}</PrivateContent>
);
export default FileEditorFileName;
@@ -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",
}}
>
<Text size="lg" fw={700} className={styles.title} title={file.name}>
<PrivateContent>{truncateCenter(file.name, 40)}</PrivateContent>
<Text
size="lg"
fw={700}
className={styles.title}
title={file.name}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "0.25rem",
}}
>
<FileEditorFileName file={file} />
</Text>
<Text
size="sm"
@@ -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> = {},
): 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(
<MantineProvider>
<FileEditorFileName file={file} />
</MantineProvider>,
);
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();
});
});
@@ -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 (
<>
<PrivateContent>{file.name}</PrivateContent>
<PrivateContent>{truncateCenter(file.name, maxLength)}</PrivateContent>
{!file.localFilePath && (
<Tooltip label={t("fileNotSavedToDisk", "Not saved to disk")}>
<span