Skip to content
Open
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
4 changes: 4 additions & 0 deletions packages/pluggableWidgets/file-uploader-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Fixed

- We fixed an issue where renaming a file through a custom dialog did not update its name in the file list.

## [2.5.1] - 2026-10-13

### Fixed
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
import classNames from "classnames";
import { observer } from "mobx-react-lite";
import { KeyboardEvent, MouseEvent, ReactElement, ReactNode, useCallback } from "react";
import { KeyboardEvent, MouseEvent, ReactElement, ReactNode, useCallback, useEffect } from "react";
import { ActionsBar } from "./ActionsBar";
import { FileIcon } from "./FileIcon";
import { ProgressBar } from "./ProgressBar";
import { UploadInfo } from "./UploadInfo";
import { FileUploaderContainerProps } from "../../typings/FileUploaderProps";
import { FileStatus, FileStore } from "../stores/FileStore";
import { fileSize } from "../utils/fileSize";
import { subscribeToFileName } from "../utils/mx-data";

interface FileEntryContainerProps {
store: FileStore;
actions?: FileUploaderContainerProps["customButtons"];
}

export const FileEntryContainer = observer(({ store, actions }: FileEntryContainerProps): ReactElement | null => {
const objectItemId = store.objectItemId;

useEffect(() => {
if (!objectItemId) {
return;
}

return subscribeToFileName(objectItemId, () => {
store.fetchMxObject().catch(error => console.error("Could not refresh file name:", error));
});
}, [store, objectItemId]);

const defaultAction = actions?.find(a => {
return a.buttonIsDefault;
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import "@testing-library/jest-dom";
import { Big } from "big.js";
import { ObjectItem } from "mendix";
import { act, render, waitFor } from "@testing-library/react";
import { dynamic } from "@mendix/widget-plugin-test-utils";
import { FileUploaderContainerProps } from "../../../typings/FileUploaderProps";
import { FileStore } from "../../stores/FileStore";
import { FileUploaderStore } from "../../stores/FileUploaderStore";
import { TranslationsStoreProvider } from "../../utils/useTranslationsStore";
import { FileEntryContainer } from "../FileEntry";

jest.mock("../../utils/useRootStore", () => ({
useRootStore: () => ({ maxTotalFiles: 5, isReadOnly: false })
}));

// Mendix may update an object in place rather than return a new instance.
class FileObject {
name = "original.txt";

get2(attribute: string): string | Big {
return attribute === "Name" ? this.name : new Big(10);
}
}

describe("FileEntryContainer external rename", () => {
const item = { id: "file-1" } as ObjectItem;
let object: FileObject;
let subscriptions: Map<number, { guid: string; attr: string; callback: () => void }>;
let unsubscribe: jest.Mock;

beforeEach(() => {
object = new FileObject();
subscriptions = new Map();
let nextHandle = 0;
unsubscribe = jest.fn(handle => subscriptions.delete(handle));
(window as any).mx = {
data: {
get: jest.fn(({ callback }) => callback(object)),
saveDocument: jest.fn((_id, _name, _meta, _file, resolve) => resolve()),
subscribe: jest.fn(args => {
const handle = nextHandle++;
subscriptions.set(handle, args);
return handle;
}),
unsubscribe
}
};
});

afterEach(() => {
(window as any).mx = undefined;
});

it.each(["existing", "uploaded"])(
"updates an %s file's visible name and cleans up its subscription",
async type => {
const rootStore = {
uploadMode: "files",
isReadOnly: false,
announce: jest.fn(),
objectCreationHelper: {
request: jest.fn().mockResolvedValue(item),
reportUploadSuccess: jest.fn()
}
} as unknown as FileUploaderStore;
const store =
type === "existing"
? FileStore.existingFile(item, rootStore)
: FileStore.newFile(new File(["content"], "original.txt"), rootStore);
const props = {
removeButtonTextMessage: dynamic.available("Remove"),
downloadButtonTextMessage: dynamic.available("Download"),
uploadQueuedMessage: dynamic.available("Waiting"),
uploadInProgressMessage: dynamic.available("Uploading"),
uploadSuccessMessage: dynamic.available("Uploaded")
} as FileUploaderContainerProps;
const { container, unmount } = render(
<TranslationsStoreProvider props={props}>
<FileEntryContainer store={store} />
</TranslationsStoreProvider>
);

if (type === "uploaded") {
await act(async () => store.upload());
}
const name = container.querySelector(".entry-details-main-name")!;
await waitFor(() => expect(name).toHaveTextContent("original.txt"));
await waitFor(() => expect(subscriptions.size).toBe(1));

const subscription = [...subscriptions.values()][0];
expect(subscription).toMatchObject({ guid: item.id, attr: "Name" });
await act(async () => {
object.name = "renamed.pdf";
subscription.callback();
});

await waitFor(() => expect(name).toHaveTextContent("renamed.pdf"));
expect(container.querySelector(".file-entry")).toHaveAttribute("title", "renamed.pdf");
expect(store.mimeType).toBe("application/pdf");

await act(async () => {
object.name = "original.txt";
subscription.callback();
});
await waitFor(() => expect(name).toHaveTextContent("original.txt"));
expect(subscriptions.size).toBe(1);

unmount();
expect(unsubscribe).toHaveBeenCalledTimes(1);
expect(subscriptions.size).toBe(0);
}
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ jest.mock("../../utils/mx-data", () => ({
fetchDocumentUrl: jest.fn(),
fetchImageThumbnail: jest.fn(),
fetchMxObject: jest.fn(),
subscribeToFileName: jest.fn(() => jest.fn()),
removeObject: jest.fn(),
saveFile: jest.fn(),
fileHasContents: jest.fn()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ import {
} from "../utils/mx-data";

export type FileStatus =
"existingFile" | "missing" | "queued" | "uploading" | "done" | "uploadingError" | "validationError" | "rejected";
| "existingFile"
| "missing"
| "queued"
| "uploading"
| "done"
| "uploadingError"
| "validationError"
| "rejected";

let fileKey = 0;

Expand All @@ -29,6 +36,7 @@ export class FileStore {
private _file?: File = undefined;
private _objectItem?: ObjectItem = undefined;
private _mxObject?: MxObject = undefined;
private _title?: string = undefined;
private _thumbnailUrl?: string = undefined;
private _documentUrl?: string = undefined;
private _thumbnailFailed: boolean = false;
Expand All @@ -45,9 +53,10 @@ export class FileStore {
this._rootStore = rootStore;
this.fileStatus = type;

makeObservable<this, "_mxObject" | "_thumbnailUrl" | "_documentUrl" | "_thumbnailFailed">(this, {
makeObservable<this, "_mxObject" | "_title" | "_thumbnailUrl" | "_documentUrl" | "_thumbnailFailed">(this, {
fileStatus: observable,
_mxObject: observable,
_title: observable,
errorDescription: observable,
_thumbnailUrl: observable,
_documentUrl: observable,
Expand Down Expand Up @@ -76,6 +85,7 @@ export class FileStore {
}

this._mxObject = undefined;
this._title = undefined;
this._objectItem = undefined;
}

Expand Down Expand Up @@ -159,11 +169,7 @@ export class FileStore {
}

get title(): string {
if (this._mxObject) {
return this._mxObject.get2("Name")?.toString();
}

return this._file?.name ?? "...";
return this._title ?? this._file?.name ?? "...";
}

get size(): number {
Expand Down Expand Up @@ -218,6 +224,8 @@ export class FileStore {
const obj = await fetchMxObject(this._objectItem!);
runInAction(() => {
this._mxObject = obj;
// Mendix can update the same object instance without notifying MobX.
this._title = obj?.get2("Name")?.toString();
this.updateThumbnailUrl();
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export function fetchMxObject(objectItem: ObjectItem): Promise<MxObject> {
});
}

export function subscribeToFileName(guid: string, callback: () => void): () => void {
const handle = (window as any).mx.data.subscribe({ guid, attr: "Name", callback });
return () => (window as any).mx.data.unsubscribe(handle);
}

export async function fetchDocumentUrl(mxObject: MxObject): Promise<string> {
return (window as any).mx.data.getDocumentUrl(
mxObject.getGuid(),
Expand Down
Loading