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 @@ -335,6 +335,10 @@ export class DepositFileApiClient {
throw new Error("Not implemented.");
}

updateFileMetadata(updateUrl, fileMeta) {
throw new Error("Not implemented.");
}

deleteFile(fileLinks) {
throw new Error("Not implemented.");
}
Expand Down Expand Up @@ -385,6 +389,12 @@ export class RDMDepositFileApiClient extends DepositFileApiClient {
return this.axiosWithConfig.post(finalizeUploadUrl, {});
}

updateFileMetadata(updateUrl, fileMeta) {
return this.axiosWithConfig.put(updateUrl, fileMeta, {
headers: { "Content-Type": "application/json" },
});
}

importParentRecordFiles(draftLinks) {
const link = `${draftLinks.self}/actions/files-import`;
return this.axiosWithConfig.post(link, {});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ export class DepositFilesService {
throw new Error("Not implemented.");
}

async updateFileMetadata(updateUrl, fileMeta) {
throw new Error("Not implemented.");
}

async delete(fileLinks) {
throw new Error("Not implemented.");
}
Expand Down Expand Up @@ -225,6 +229,10 @@ export class RDMDepositFilesService extends DepositFilesService {
return (await this.fileApiClient.finalizeFileUpload(commitFileURL)).data;
};

updateFileMetadata = async (updateUrl, fileMeta) => {
return (await this.fileApiClient.updateFileMetadata(updateUrl, fileMeta)).data;
}

importParentRecordFiles = async (draftLinks) => {
const response = await this.fileApiClient.importParentRecordFiles(draftLinks);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ let fakeApiIsCancelled;
let fakeApiInitializeFileUpload;
let fakeApiUploadFile;
let fakeApiFinalizeFileUpload;
let fakeApiUpdateFileMetadata;
let fakeApiDeleteFile;
class FakeFileApiClient extends DepositFileApiClient {
isCancelled(error) {
Expand All @@ -31,6 +32,10 @@ class FakeFileApiClient extends DepositFileApiClient {
return fakeApiFinalizeFileUpload(finalizeUploadUrl);
}

updateFileMetadata(updateUrl, fileMeta) {
return fakeApiUpdateFileMetadata(updateUrl, fileMeta);
}

deleteFile(fileLinks) {
return fakeApiDeleteFile(fileLinks);
}
Expand Down Expand Up @@ -75,6 +80,7 @@ beforeEach(() => {
progressFn(20);
});
fakeApiFinalizeFileUpload = jest.fn();
fakeApiUpdateFileMetadata = jest.fn();
fakeApiDeleteFile = jest.fn();

fakeOnUploadAdded = jest.fn();
Expand Down Expand Up @@ -211,6 +217,14 @@ describe("DepositFilesService tests", () => {
expect(fakeOnUploadCompleted).not.toHaveBeenCalled();
expect(fakeOnUploadFailed).not.toHaveBeenCalled();
});

it("it should call updateFileMetadata without errors", async () => {
fakeApiUpdateFileMetadata.mockReturnValueOnce({ data: { message: "ok" } });
const updateUrl = "updateUrl";
const fileMeta = { id: "file1", links: {}, metadata: { description: "123" } };
await filesService.updateFileMetadata(updateUrl, fileMeta);
expect(fakeApiUpdateFileMetadata).toHaveBeenCalledWith(updateUrl, fileMeta);
});
});

describe("Concurrent uploads tests", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const getFilesList = (filesState) => {
name: fileState.name,
size: fileState.size,
checksum: fileState.checksum,
metadata: fileState.metadata,
links: fileState.links,
uploadState: {
// initial: fileState.status === UploadState.initial,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ export class RDMUppyUploaderPlugin extends AwsS3Multipart {
try {
const response = await this.opts.initializeFileUpload(this.draftRecord, file);
this.uppy.setFileMeta(file.id, {
...file.meta,
file_id: response.file_id,
links: response.links,
});
Expand Down Expand Up @@ -322,6 +323,7 @@ export class RDMUppyUploaderPlugin extends AwsS3Multipart {

// Map any links to Uppy file state for further use (e.g. to fetch signed part URLs)
this.uppy.setFileMeta(file.id, {
...file.meta,
file_id: response.file_id,
links: response.links,
});
Expand Down Expand Up @@ -367,6 +369,7 @@ export class RDMUppyUploaderPlugin extends AwsS3Multipart {

file.meta.links = response.links;
this.uppy.setFileMeta(file.id, {
...file.meta,
links: response.links,
});

Expand Down Expand Up @@ -395,6 +398,9 @@ export class RDMUppyUploaderPlugin extends AwsS3Multipart {
* - The default implementation calls out to Companion’s S3 signing endpoints.
*/
async completeMultipartUpload(file) {
if (file.meta?.metadata && Object.keys(file.meta.metadata).length > 0 && this.opts.updateFileMetadata) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If metadata update fails, the whole upload will fail without calling finalize, will it be properly cleaned up? I think finalize will handle cleanup properly if it fails

await this.opts.updateFileMetadata(this.draftRecord, file);
}
const response = await this.opts.finalizeUpload(file);
return response.links.content;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ import { UploadState } from "../../state/reducers/files";
import { i18next } from "@translations/invenio_rdm_records/i18next";
import { getFilesList, FilesListTable, FileUploaderToolbar } from "../FileUploader";
import { useUppyLocale } from "./locale";
import {
defaultAllowedMetaFields,
getMetaFieldsRenderers,
onBeforeUploadProcessMetaFields,
} from "./metaFields";
import {
UPPY_EVENTS,
getUppyDashboardEventsProps,
} from "./events";

const defaultDashboardProps = {
showRemoveButtonAfterComplete: false,
Expand Down Expand Up @@ -79,6 +88,7 @@ export const UppyUploaderComponent = ({
permissions,
record,
initializeFileUpload,
updateFileMetadata,
finalizeUpload,
deleteFile,
uploadPart,
Expand All @@ -92,13 +102,16 @@ export const UppyUploaderComponent = ({
decimalSizeDisplay,
filesLocked,
allowEmptyFiles,
allowedMetaFields,
startEvent,
...uiProps
}) => {
// We extract the working copy of the draft stored as `values` in formik
const { values: formikDraft, errors, initialErrors } = useFormikContext();
const { filesList } = getFilesList(files ?? {});
const hasError = (errors.files || initialErrors?.files) && files;
const locale = useUppyLocale();
const activeAllowedMetaFields = config?.allowedMetaFields || allowedMetaFields || defaultAllowedMetaFields;
const filesEnabled = _get(formikDraft, "files.enabled", false);
const filesSize = filesList.reduce((totalSize, file) => (totalSize += file.size), 0);
const lockFileUploader = !isDraftRecord && filesLocked;
Expand Down Expand Up @@ -147,6 +160,7 @@ export const UppyUploaderComponent = ({
quota,
// Bind Redux file actions to the uploader plugin
initializeFileUpload,
updateFileMetadata,
finalizeUpload,
saveAndFetchDraft,
setUploadProgress,
Expand All @@ -169,6 +183,16 @@ export const UppyUploaderComponent = ({
uppy.setOptions({ onBeforeFileAdded });
}, [uppy, filesList]);

React.useEffect(() => {
uppy.setOptions({
onBeforeUpload: (uppyFiles) => {
return activeAllowedMetaFields && activeAllowedMetaFields.length > 0
? onBeforeUploadProcessMetaFields(uppyFiles, files, activeAllowedMetaFields)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files from closure not in deps, could get stale

: uppyFiles;
}
});
}, [uppy, activeAllowedMetaFields]);

React.useEffect(() => {
const uploaderPlugin = uppy.getPlugin("RDMUppyUploaderPlugin");
if (uploaderPlugin) {
Expand Down Expand Up @@ -304,10 +328,10 @@ export const UppyUploaderComponent = ({
disabled={!filesLeft || lockFileUploader}
// `null` means "do not display a Done button in a status bar"
doneButtonHandler={null}
note={i18next.t(
"File addition, removal or modification are not allowed after you have published your upload."
)}
note={i18next.t("File addition, removal or modification are not allowed after you have published your upload.")}
metaFields={activeAllowedMetaFields && activeAllowedMetaFields.length > 0 ? getMetaFieldsRenderers(activeAllowedMetaFields) : undefined}
{...defaultDashboardProps}
{...(startEvent && getUppyDashboardEventsProps(startEvent))}
{...uiProps}
/>
)}
Expand Down Expand Up @@ -379,6 +403,7 @@ UppyUploaderComponent.propTypes = {
isFileImportInProgress: PropTypes.bool,
importParentFiles: PropTypes.func.isRequired,
initializeFileUpload: PropTypes.func.isRequired,
updateFileMetadata: PropTypes.func.isRequired,
finalizeUpload: PropTypes.func.isRequired,
uploadPart: PropTypes.func.isRequired,
setUploadProgress: PropTypes.func.isRequired,
Expand All @@ -388,6 +413,20 @@ UppyUploaderComponent.propTypes = {
filesLocked: PropTypes.bool,
permissions: PropTypes.object,
allowEmptyFiles: PropTypes.bool,
allowedMetaFields: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
defaultValue: PropTypes.any,
name: PropTypes.string,
placeholder: PropTypes.string,
render: PropTypes.func,
condition: PropTypes.func,
})
),
startEvent: PropTypes.shape({
event: PropTypes.oneOf(Object.values(UPPY_EVENTS)).isRequired,
payload: PropTypes.object,
}),
};

UppyUploaderComponent.defaultProps = {
Expand All @@ -408,4 +447,6 @@ UppyUploaderComponent.defaultProps = {
decimalSizeDisplay: true,
filesLocked: false,
allowEmptyFiles: true,
allowedMetaFields: undefined,
startEvent: undefined,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// This file is part of Invenio-RDM-Records
// Copyright (C) 2020-2025 CERN.
// Copyright (C) 2025 CESNET.
//
// Invenio-RDM-Records is free software; you can redistribute it and/or modify it
// under the terms of the MIT License; see LICENSE file for more details.

/**
* Enumeration of possible supported Uppy dashboard events.
* @constant {Object}
* @property {string} UPLOAD_FILE_WITHOUT_EDIT - Event triggered for auto-uploading without explicit editing user interaction.
*/
export const UPPY_EVENTS = {
UPLOAD_FILE_WITHOUT_EDIT: "upload-file-without-edit",
};

/**
* Computes conditional UI properties for the Uppy Dashboard based on the active event state.
*
* @param {Object} startEvent - The currently active event, if any (e.g. { event: UPPY_EVENTS.EDIT_FILE }).
* @returns {Object} A dictionary of props specifically mapped to the `<Dashboard />` component dynamically.
*/
export const getUppyDashboardEventsProps = (startEvent) => {
return {
showSelectedFiles: startEvent.event === UPPY_EVENTS.UPLOAD_FILE_WITHOUT_EDIT ? false : true,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
deleteFile,
importParentFiles,
initializeFileUpload,
updateFileMetadata,
uploadPart,
finalizeUpload,
saveAndFetchDraft,
Expand All @@ -34,6 +35,7 @@ const mapStateToProps = (state) => {

const mapDispatchToProps = (dispatch) => ({
initializeFileUpload: (draft, file) => dispatch(initializeFileUpload(draft, file)),
updateFileMetadata: (draft, serverFile, file) => dispatch(updateFileMetadata(draft, serverFile, file)),
finalizeUpload: (file) => dispatch(finalizeUpload(file.meta.links.commit, file)),
importParentFiles: () => dispatch(importParentFiles()),
setUploadProgress: (file, percent) => dispatch(setUploadProgress(file, percent)),
Expand All @@ -46,3 +48,6 @@ export const UppyUploader = connect(
mapStateToProps,
mapDispatchToProps
)(UppyUploaderComponent);

export { UPPY_EVENTS } from "./events";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only used internally, is there a reason to export for other parts of UI?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make available events a public enum. Not just a string literal. But it's optional ofc.

export { defaultAllowedMetaFields } from "./metaFields";
Loading