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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { FileManagerPermissions } from "../../features/permissions/abstractions.
import { GetSettingsRepository } from "../../features/settings/abstractions.js";
import { FileDetailsPresenter as Abstraction, type IFileDetailsPresenter } from "./abstractions.js";
import { FileDetailsPresenter } from "./FileDetailsPresenter.js";
import { WcpService } from "@webiny/app-admin/features/wcp/abstractions.js";
import type { FmFile } from "../../features/shared/types.js";

// ---------------------------------------------------------------------------
Expand All @@ -33,7 +34,7 @@ function createMockFormModel(overrides?: Partial<IFormVM>): IFormModel {
const data: Record<string, unknown> = {};

return {
field: vi.fn() as any,
field: vi.fn().mockReturnValue({ setDisabled: vi.fn() }) as any,
fields: vi.fn() as any,
layout: vi.fn() as any,
getData: vi.fn(() => ({ ...data })),
Expand Down Expand Up @@ -110,6 +111,17 @@ function createMockSettingsRepository(): GetSettingsRepository.Interface {
};
}

function createMockWcpService(canUsePrivateFiles = true): WcpService.Interface {
return {
getProject: vi.fn().mockReturnValue({
canUsePrivateFiles: vi.fn().mockReturnValue(canUsePrivateFiles)
}),
isLoaded: vi.fn().mockReturnValue(true),
canUseFeature: vi.fn().mockReturnValue(false),
loadProject: vi.fn().mockResolvedValue(undefined)
} as unknown as WcpService.Interface;
}

function createTestFile(overrides?: Partial<FmFile>): FmFile {
return {
id: "file-1",
Expand Down Expand Up @@ -141,16 +153,18 @@ interface Mocks {
formModelFactory: FormModelFactory.Interface;
permissions: FileManagerPermissions.Interface;
settingsRepository: GetSettingsRepository.Interface;
wcpService: WcpService.Interface;
}

function createMocks(): Mocks {
function createMocks(canUsePrivateFiles = true): Mocks {
return {
getFileUseCase: createMockGetFileUseCase(),
updateFileUseCase: createMockUpdateFileUseCase(),
deleteFileUseCase: createMockDeleteFileUseCase(),
formModelFactory: createMockFormModelFactory(),
permissions: createMockPermissions(),
settingsRepository: createMockSettingsRepository()
settingsRepository: createMockSettingsRepository(),
wcpService: createMockWcpService(canUsePrivateFiles)
};
}

Expand All @@ -163,6 +177,7 @@ function createContainer(mocks: Mocks) {
container.registerInstance(FormModelFactory, mocks.formModelFactory);
container.registerInstance(FileManagerPermissions, mocks.permissions);
container.registerInstance(GetSettingsRepository, mocks.settingsRepository);
container.registerInstance(WcpService, mocks.wcpService);

// Register the real FileDetailsPresenter implementation.
container.register(FileDetailsPresenter).inSingletonScope();
Expand Down Expand Up @@ -344,4 +359,59 @@ describe("FileDetailsPresenter", () => {
expect(loadingDuringExecution).toBe("Loading file...");
expect(presenter.vm.loading).toBeNull();
});

// -------------------------------------------------------------------
// When Private Files is NOT licensed.
// -------------------------------------------------------------------

describe("when Private Files is NOT licensed", () => {
let unlicensedMocks: Mocks;
let unlicensedPresenter: IFileDetailsPresenter;

beforeEach(() => {
unlicensedMocks = createMocks(false);
const container = createContainer(unlicensedMocks);
unlicensedPresenter = container.resolve(Abstraction);
});

it("should NOT include accessControl in the save payload", async () => {
await unlicensedPresenter.loadFile("file-1");

const mockForm = (unlicensedMocks.formModelFactory.create as ReturnType<typeof vi.fn>)
.mock.results[1].value as IFormModel;
(mockForm.submit as ReturnType<typeof vi.fn>).mockResolvedValue({
name: "updated.jpg",
description: "",
tags: ["updated"]
});

await unlicensedPresenter.saveFile();

const callArgs = (unlicensedMocks.updateFileUseCase.execute as ReturnType<typeof vi.fn>)
.mock.calls[0][0];
expect(callArgs.data).not.toHaveProperty("accessControl");
});

it("should NOT set accessControl in form data during loadFile", async () => {
await unlicensedPresenter.loadFile("file-1");

const mockForm = (unlicensedMocks.formModelFactory.create as ReturnType<typeof vi.fn>)
.mock.results[1].value as IFormModel;
const setDataCall = (mockForm.setData as ReturnType<typeof vi.fn>).mock.calls[0][0];
expect(setDataCall).not.toHaveProperty("accessControl");
});

it("should NOT disable accessControl field when user lacks edit permission", async () => {
(unlicensedMocks.permissions.canEdit as ReturnType<typeof vi.fn>).mockReturnValue(
false
);

await unlicensedPresenter.loadFile("file-1");

const mockForm = (unlicensedMocks.formModelFactory.create as ReturnType<typeof vi.fn>)
.mock.results[1].value as IFormModel;
expect(mockForm.field).toHaveBeenCalledTimes(3);
expect(mockForm.field).not.toHaveBeenCalledWith("accessControl");
});
});
});
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { makeAutoObservable, runInAction, computed } from "mobx";
import { WcpService } from "@webiny/app-admin/features/wcp/abstractions.js";
import {
FileDetailsPresenter as Abstraction,
type IFileDetailsPresenter,
type IFileDetailsViewModel
} from "./abstractions.js";
import { GetFileUseCase } from "../../features/getFile/abstractions.js";
import { UpdateFileUseCase } from "../../features/updateFile/abstractions.js";
import { DeleteFileUseCase } from "../../features/deleteFile/abstractions.js";
import { FormModelFactory } from "@webiny/app-admin/features/formModel/abstractions.js";
import type { IFormModel } from "@webiny/app-admin/features/formModel/abstractions.js";
import { FileManagerPermissions } from "../../features/permissions/abstractions.js";
import { GetSettingsRepository } from "../../features/settings/abstractions.js";
import type { FmFile } from "../../features/shared/types.js";
import type { UpdateFileData } from "../../features/updateFile/abstractions.js";

class FileDetailsPresenterImpl implements IFileDetailsPresenter {
private file: FmFile | null = null;
Expand All @@ -21,10 +22,10 @@ class FileDetailsPresenterImpl implements IFileDetailsPresenter {
constructor(
private getFileUseCase: GetFileUseCase.Interface,
private updateFileUseCase: UpdateFileUseCase.Interface,
private deleteFileUseCase: DeleteFileUseCase.Interface,
private formModelFactory: FormModelFactory.Interface,
private permissions: FileManagerPermissions.Interface,
private settingsRepository: GetSettingsRepository.Interface
private settingsRepository: GetSettingsRepository.Interface,
private wcp: WcpService.Interface
) {
// Build an empty form initially.
this.form = this.buildForm();
Expand Down Expand Up @@ -56,18 +57,26 @@ class FileDetailsPresenterImpl implements IFileDetailsPresenter {
if (result.success) {
this.file = result.file;
this.form = this.buildForm();
this.form.setData({

const formData: Record<string, unknown> = {
name: result.file.name ?? "",
description: result.file.description ?? "",
tags: result.file.tags ?? [],
accessControl: result.file.accessControl?.type ?? "public"
});
tags: result.file.tags ?? []
};

if (this.canUsePrivateFiles()) {
formData.accessControl = result.file.accessControl?.type ?? "public";
}

this.form.setData(formData);

if (!this.permissions.canEdit("file", result.file)) {
this.form.field("name").setDisabled(true);
this.form.field("description").setDisabled(true);
this.form.field("tags").setDisabled(true);
this.form.field("accessControl").setDisabled(true);
if (this.canUsePrivateFiles()) {
this.form.field("accessControl").setDisabled(true);
}
}
}
});
Expand All @@ -93,16 +102,21 @@ class FileDetailsPresenterImpl implements IFileDetailsPresenter {
});

try {
const updateData: UpdateFileData = {
name: data.name as string,
description: data.description as string,
tags: data.tags as string[]
};

if (this.canUsePrivateFiles()) {
updateData.accessControl = {
type: data.accessControl as "public" | "private-authenticated"
};
}

await this.updateFileUseCase.execute({
id: this.file.id,
data: {
name: data.name as string,
description: data.description as string,
tags: data.tags as string[],
accessControl: {
type: data.accessControl as "public" | "private-authenticated"
}
}
data: updateData
});
return true;
} finally {
Expand All @@ -114,33 +128,55 @@ class FileDetailsPresenterImpl implements IFileDetailsPresenter {

private buildForm(): IFormModel {
return this.formModelFactory.create({
fields: fields => ({
name: fields.text().label("Name").required().placeholder("Enter name"),
description: fields
.text()
.label("Description")
.renderer("textarea")
.placeholder("Enter description"),
tags: fields.text().label("Tags").list().renderer("tags").placeholder("Add tags"),
accessControl: fields
.text()
.label("Access Control")
.help("Control who can access this file.")
.options([
{ label: "Public", value: "public" },
{ label: "Private (Authenticated)", value: "private-authenticated" }
])
.defaultValue("public")
}),
layout: layout => [
layout.row("name"),
layout.row("description"),
layout.row("tags"),
layout.row("accessControl")
]
fields: fields => {
const fieldDefs: Record<string, any> = {
name: fields.text().label("Name").required().placeholder("Enter name"),
description: fields
.text()
.label("Description")
.renderer("textarea")
.placeholder("Enter description"),
tags: fields
.text()
.label("Tags")
.list()
.renderer("tags")
.placeholder("Add tags")
};

if (this.canUsePrivateFiles()) {
fieldDefs.accessControl = fields
.text()
.label("Access Control")
.help("Control who can access this file.")
.options([
{ label: "Public", value: "public" },
{
label: "Private (Authenticated)",
value: "private-authenticated"
}
])
.defaultValue("public");
}

return fieldDefs;
},
layout: layout => {
const rows = [layout.row("name"), layout.row("description"), layout.row("tags")];

if (this.canUsePrivateFiles()) {
rows.push(layout.row("accessControl"));
}

return rows;
}
});
}

private canUsePrivateFiles(): boolean {
return this.wcp.getProject().canUsePrivateFiles();
}

private buildPreviewUrl(): string | null {
if (!this.file?.key) {
return null;
Expand All @@ -155,9 +191,9 @@ export const FileDetailsPresenter = Abstraction.createImplementation({
dependencies: [
GetFileUseCase,
UpdateFileUseCase,
DeleteFileUseCase,
FormModelFactory,
FileManagerPermissions,
GetSettingsRepository
GetSettingsRepository,
WcpService
]
});