diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/api/DepositApiClient.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/api/DepositApiClient.js
index de71caddf..6f11cd956 100644
--- a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/api/DepositApiClient.js
+++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/api/DepositApiClient.js
@@ -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.");
}
@@ -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, {});
diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/api/DepositFilesService.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/api/DepositFilesService.js
index 738f723c5..95eba201a 100644
--- a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/api/DepositFilesService.js
+++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/api/DepositFilesService.js
@@ -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.");
}
@@ -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);
diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/api/DepositFilesService.test.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/api/DepositFilesService.test.js
index 3d17597ff..4fb123506 100644
--- a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/api/DepositFilesService.test.js
+++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/api/DepositFilesService.test.js
@@ -13,6 +13,7 @@ let fakeApiIsCancelled;
let fakeApiInitializeFileUpload;
let fakeApiUploadFile;
let fakeApiFinalizeFileUpload;
+let fakeApiUpdateFileMetadata;
let fakeApiDeleteFile;
class FakeFileApiClient extends DepositFileApiClient {
isCancelled(error) {
@@ -31,6 +32,10 @@ class FakeFileApiClient extends DepositFileApiClient {
return fakeApiFinalizeFileUpload(finalizeUploadUrl);
}
+ updateFileMetadata(updateUrl, fileMeta) {
+ return fakeApiUpdateFileMetadata(updateUrl, fileMeta);
+ }
+
deleteFile(fileLinks) {
return fakeApiDeleteFile(fileLinks);
}
@@ -75,6 +80,7 @@ beforeEach(() => {
progressFn(20);
});
fakeApiFinalizeFileUpload = jest.fn();
+ fakeApiUpdateFileMetadata = jest.fn();
fakeApiDeleteFile = jest.fn();
fakeOnUploadAdded = jest.fn();
@@ -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", () => {
diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/FileUploader/utils.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/FileUploader/utils.js
index ed3ca2a91..56f24090d 100644
--- a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/FileUploader/utils.js
+++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/FileUploader/utils.js
@@ -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,
diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/UppyUploader/RDMUppyUploaderPlugin.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/UppyUploader/RDMUppyUploaderPlugin.js
index 941a3b470..7e10a82ce 100644
--- a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/UppyUploader/RDMUppyUploaderPlugin.js
+++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/UppyUploader/RDMUppyUploaderPlugin.js
@@ -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,
});
@@ -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,
});
@@ -367,6 +369,7 @@ export class RDMUppyUploaderPlugin extends AwsS3Multipart {
file.meta.links = response.links;
this.uppy.setFileMeta(file.id, {
+ ...file.meta,
links: response.links,
});
@@ -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) {
+ await this.opts.updateFileMetadata(this.draftRecord, file);
+ }
const response = await this.opts.finalizeUpload(file);
return response.links.content;
}
diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/UppyUploader/UppyUploader.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/UppyUploader/UppyUploader.js
index 0cdd65c02..d0c86bbaf 100644
--- a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/UppyUploader/UppyUploader.js
+++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/UppyUploader/UppyUploader.js
@@ -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,
@@ -79,6 +88,7 @@ export const UppyUploaderComponent = ({
permissions,
record,
initializeFileUpload,
+ updateFileMetadata,
finalizeUpload,
deleteFile,
uploadPart,
@@ -92,6 +102,8 @@ export const UppyUploaderComponent = ({
decimalSizeDisplay,
filesLocked,
allowEmptyFiles,
+ allowedMetaFields,
+ startEvent,
...uiProps
}) => {
// We extract the working copy of the draft stored as `values` in formik
@@ -99,6 +111,7 @@ export const UppyUploaderComponent = ({
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;
@@ -147,6 +160,7 @@ export const UppyUploaderComponent = ({
quota,
// Bind Redux file actions to the uploader plugin
initializeFileUpload,
+ updateFileMetadata,
finalizeUpload,
saveAndFetchDraft,
setUploadProgress,
@@ -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)
+ : uppyFiles;
+ }
+ });
+ }, [uppy, activeAllowedMetaFields]);
+
React.useEffect(() => {
const uploaderPlugin = uppy.getPlugin("RDMUppyUploaderPlugin");
if (uploaderPlugin) {
@@ -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}
/>
)}
@@ -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,
@@ -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 = {
@@ -408,4 +447,6 @@ UppyUploaderComponent.defaultProps = {
decimalSizeDisplay: true,
filesLocked: false,
allowEmptyFiles: true,
+ allowedMetaFields: undefined,
+ startEvent: undefined,
};
diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/UppyUploader/events.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/UppyUploader/events.js
new file mode 100644
index 000000000..c43bea155
--- /dev/null
+++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/UppyUploader/events.js
@@ -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 `` component dynamically.
+ */
+export const getUppyDashboardEventsProps = (startEvent) => {
+ return {
+ showSelectedFiles: startEvent.event === UPPY_EVENTS.UPLOAD_FILE_WITHOUT_EDIT ? false : true,
+ };
+};
diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/UppyUploader/index.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/UppyUploader/index.js
index d19ab6201..f25f30d1b 100644
--- a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/UppyUploader/index.js
+++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/UppyUploader/index.js
@@ -10,6 +10,7 @@ import {
deleteFile,
importParentFiles,
initializeFileUpload,
+ updateFileMetadata,
uploadPart,
finalizeUpload,
saveAndFetchDraft,
@@ -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)),
@@ -46,3 +48,6 @@ export const UppyUploader = connect(
mapStateToProps,
mapDispatchToProps
)(UppyUploaderComponent);
+
+export { UPPY_EVENTS } from "./events";
+export { defaultAllowedMetaFields } from "./metaFields";
\ No newline at end of file
diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/UppyUploader/metaFields.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/UppyUploader/metaFields.js
new file mode 100644
index 000000000..ec038b207
--- /dev/null
+++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/UppyUploader/metaFields.js
@@ -0,0 +1,149 @@
+// 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.
+
+/**
+ * Default allowed metadata fields configuration.
+ * @example
+ * [
+ * {
+ * id: "caption",
+ * defaultValue: "",
+ * name: i18next.t("Caption"),
+ * placeholder: i18next.t("Set the Caption here"),
+ * condition: (file) => file.type && file.type.startsWith("image/")
+ * },
+ * {
+ * id: "featured",
+ * defaultValue: false,
+ * name: i18next.t("Feature Image"),
+ * render: ({ value, onChange, required, form }, h) => {
+ * return h("input", {
+ * type: "checkbox",
+ * onChange: (ev) => onChange(ev.target.checked),
+ * checked: value,
+ * defaultChecked: value,
+ * required,
+ * form,
+ * });
+ * },
+ * condition: (file) => file.type && file.type.startsWith("image/")
+ * },
+ * {
+ * id: "fileNote",
+ * defaultValue: "",
+ * name: i18next.t("File Note"),
+ * placeholder: i18next.t("Set the file Note here"),
+ * },
+ * {
+ * id: "fileType",
+ * defaultValue: (file) => {
+ * if (file.type) {
+ * if (file.type.startsWith("image/")) return "image";
+ * }
+ * return "other";
+ * },
+ * },
+ * ]
+ *
+ * @type {Object[]} metaFields - Array of metadata field configuration objects. Each object defines how a specific metadata field should be handled and rendered in the Uppy Dashboard.
+ * @property {string} metaFields[].id - The unique identifier of the metadata field. Used as the key in the file.meta.metadata dictionary.
+ * @property {any|function(Object): any} [metaFields[].defaultValue] - Default value or a function resolving a default value based on the file object.
+ * @property {string} [metaFields[].name] - Optional display name of the field. If provided, it will be used to render a standard input field, otherwise it won't be editable in UI.
+ * @property {string} [metaFields[].placeholder] - Optional placeholder text for the input UI.
+ * @property {function(Object, Function): Object} [metaFields[].render] - Optional custom render function for advanced UI rendering of the field using Preact `h` function. If not provided, a standard text input will be rendered when `name` is set. See {@link https://uppy.io/docs/dashboard/#metafields|Uppy Dashboard metaFields documentation}.
+ * @property {function(Object): Boolean} [metaFields[].condition] - Optional function to conditionally attach or render the field based on the respective file properties (e.g. file.type).
+ */
+export const defaultAllowedMetaFields = [
+ // {
+ // id: "description",
+ // defaultValue: "",
+ // name: i18next.t("Description"),
+ // placeholder: i18next.t("Set the file description here"),
+ // },
+];
+
+/**
+ * Higher-order function that generates a metaFields configuration function for Uppy Dashboard.
+ * Evaluates custom fields passed in `allowedMetaFields` to render them.
+ *
+ * @param {Array