diff --git a/oarepo_ui/resources/resource.py b/oarepo_ui/resources/resource.py
index d1785f1a..2ae018da 100644
--- a/oarepo_ui/resources/resource.py
+++ b/oarepo_ui/resources/resource.py
@@ -114,7 +114,7 @@ def empty_record(self, resource_requestctx, **kwargs):
empty_data = dump_empty(self.api_config.schema)
files_field = getattr(self.api_config.record_cls, "files", None)
if files_field and isinstance(files_field, FilesField):
- empty_data["files"] = {"enabled": False}
+ empty_data["files"] = {"enabled": True}
empty_data = deepmerge.always_merger.merge(
empty_data, copy.deepcopy(self.config.empty_record)
)
diff --git a/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/api/client.js b/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/api/client.js
index 2f9bab0f..4589cb13 100644
--- a/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/api/client.js
+++ b/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/api/client.js
@@ -144,8 +144,8 @@ export class OARepoDepositApiClient extends DepositApiClient {
*/
readDraft = async (draftLinks) => {
return this._createResponse(() => {
- const response = this.axiosWithConfig.get(relativeUrl(draftLinks.self))
- return this.recordSerializer.deserialize(response)
+ const response = this.axiosWithConfig.get(relativeUrl(draftLinks.self));
+ return this.recordSerializer.deserialize(response);
});
};
@@ -176,3 +176,68 @@ export class OARepoDepositApiClient extends DepositApiClient {
);
};
}
+
+export class DepositFileApiClient {
+ constructor(additionalApiConfig) {
+ if (this.constructor === DepositFileApiClient) {
+ throw new Error("Abstract");
+ }
+ const additionalHeaders = _get(additionalApiConfig, "headers", {});
+ this.apiHeaders = Object.assign({}, BASE_HEADERS, additionalHeaders);
+
+ const apiConfig = {
+ withCredentials: true,
+ xsrfCookieName: "csrftoken",
+ xsrfHeaderName: "X-CSRFToken",
+ headers: this.apiHeaders.json,
+ };
+ this.axiosWithConfig = axios.create(apiConfig);
+ }
+
+ isCancelled(error) {
+ return axios.isCancel(error);
+ }
+
+ initializeFileUpload(initializeUploadUrl, filename) {
+ throw new Error("Not implemented.");
+ }
+
+ uploadFile(uploadUrl, file, onUploadProgress, cancel) {
+ throw new Error("Not implemented.");
+ }
+
+ finalizeFileUpload(finalizeUploadUrl) {
+ throw new Error("Not implemented.");
+ }
+
+ deleteFile(fileLinks) {
+ throw new Error("Not implemented.");
+ }
+}
+
+/**
+ * Default File API Client for deposits.
+ */
+export class OARepoDepositFileApiClient extends DepositFileApiClient {
+ _createResponse = async (axiosRequest) => {
+ let response;
+ try {
+ response = await axiosRequest();
+ const data = response.data || {};
+ return data;
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ };
+
+ readDraftFiles = async (draft) => {
+ return this._createResponse(() => {
+ const response = this.axiosWithConfig.get(relativeUrl(draft.links.files));
+ return response;
+ });
+ };
+
+ deleteFile = (fileLinks) => {
+ return this.axiosWithConfig.delete(fileLinks.self);
+ };
+}
diff --git a/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/forms/components/EDTFDatePickerField/EDTFDatePickerField.jsx b/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/forms/components/EDTFDatePickerField/EDTFDatePickerField.jsx
index 60e9131c..3fa42a07 100644
--- a/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/forms/components/EDTFDatePickerField/EDTFDatePickerField.jsx
+++ b/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/forms/components/EDTFDatePickerField/EDTFDatePickerField.jsx
@@ -219,7 +219,7 @@ export const EDTFSingleDatePicker = ({
icon,
helpText,
required,
- placeholderText,
+ placeholder,
calendarControlButtonClassName,
calendarControlIconName,
clearIconClassName,
@@ -249,6 +249,7 @@ export const EDTFSingleDatePicker = ({
fieldPath={fieldPath}
required={required}
autoComplete="off"
+ placeholder={placeholder}
label={}
icon={
field?.value ? (
@@ -281,7 +282,6 @@ export const EDTFSingleDatePicker = ({
{...props}
/>
)}
- placeholderText={placeholderText}
{...datePickerProps}
/>
)}
@@ -310,7 +310,7 @@ EDTFSingleDatePicker.propTypes = {
helpText: PropTypes.string,
datePickerProps: PropTypes.object,
required: PropTypes.bool,
- placeholderText: PropTypes.string,
+ placeholder: PropTypes.string,
calendarControlButtonClassName: PropTypes.string,
calendarControlIconName: PropTypes.string,
clearIconClassName: PropTypes.string,
@@ -320,7 +320,7 @@ EDTFSingleDatePicker.defaultProps = {
icon: "calendar",
helpText: i18next.t("Format: YYYY-MM-DD, YYYYY-MM or YYYY."),
required: false,
- placeholderText: i18next.t(
+ placeholder: i18next.t(
"Write a date or click on the calendar icon to select it"
),
calendarControlButtonClassName: "calendar-control-button",
diff --git a/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/forms/components/MultilingualTextInput/MultilingualTextInput.jsx b/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/forms/components/MultilingualTextInput/MultilingualTextInput.jsx
index 243f3986..d3c9f43f 100644
--- a/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/forms/components/MultilingualTextInput/MultilingualTextInput.jsx
+++ b/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/forms/components/MultilingualTextInput/MultilingualTextInput.jsx
@@ -27,6 +27,7 @@ export const MultilingualTextInput = ({
addButtonLabel,
lngFieldWidth,
showEmptyValue,
+ prefillLanguageWithDefaultLocale,
...uiProps
}) => {
const { defaultLocale } = useDefaultLocale();
@@ -43,7 +44,11 @@ export const MultilingualTextInput = ({
return (
@@ -106,6 +111,7 @@ MultilingualTextInput.propTypes = {
rich: PropTypes.bool,
defaultNewValue: PropTypes.object,
showEmptyValue: PropTypes.bool,
+ prefillLanguageWithDefaultLocale: PropTypes.bool,
};
MultilingualTextInput.defaultProps = {
@@ -117,4 +123,5 @@ MultilingualTextInput.defaultProps = {
label: undefined,
addButtonLabel: i18next.t("Add another language"),
showEmptyValue: false,
+ prefillLanguageWithDefaultLocale: false,
};
diff --git a/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/forms/hooks.js b/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/forms/hooks.js
index 6d1209b7..11f6b530 100644
--- a/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/forms/hooks.js
+++ b/oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/forms/hooks.js
@@ -1,6 +1,10 @@
import * as React from "react";
import { FormConfigContext } from "./contexts";
-import { OARepoDepositApiClient, OARepoDepositSerializer } from "../api";
+import {
+ OARepoDepositApiClient,
+ OARepoDepositSerializer,
+ OARepoDepositFileApiClient,
+} from "../api";
import _get from "lodash/get";
import _set from "lodash/set";
import { useFormikContext, getIn } from "formik";
@@ -46,8 +50,8 @@ export const useDefaultLocale = () => {
formConfig: { default_locale },
} = useFormConfig();
- return { defaultLocale: default_locale }
-}
+ return { defaultLocale: default_locale };
+};
export const useVocabularyOptions = (vocabularyType) => {
const {
@@ -66,15 +70,26 @@ export const useConfirmationModal = () => {
return { isModalOpen, handleCloseModal, handleOpenModal };
};
-export const useFormFieldValue = ({ subValuesPath, defaultValue, subValuesUnique = true }) => {
+export const useFormFieldValue = ({
+ subValuesPath,
+ defaultValue,
+ subValuesUnique = true,
+}) => {
const usedSubValues = (value) =>
value && typeof Array.isArray(value)
? value.map((val) => _get(val, "lang")) || []
: [];
- const defaultNewValue = (initialVal, usedSubValues = []) => _set({ ...initialVal }, subValuesPath, !usedSubValues?.includes(defaultValue) || !subValuesUnique ? defaultValue : "")
+ const defaultNewValue = (initialVal, usedSubValues = []) =>
+ _set(
+ { ...initialVal },
+ subValuesPath,
+ !usedSubValues?.includes(defaultValue) || !subValuesUnique
+ ? defaultValue
+ : ""
+ );
- return { usedSubValues, defaultNewValue }
-}
+ return { usedSubValues, defaultNewValue };
+};
export const useShowEmptyValue = (
fieldPath,
@@ -129,16 +144,20 @@ export const useDepositApiClient = (
isSubmitting,
values,
validateForm,
- setErrors,
setSubmitting,
setValues,
setFieldError,
setFieldValue,
+ setErrors,
} = formik;
const {
formConfig: { createUrl },
} = useFormConfig();
+ const [isSaving, setIsSaving] = React.useState(false);
+ const [isPublishing, setIsPublishing] = React.useState(false);
+ const [isDeleting, setIsDeleting] = React.useState(false);
+
const recordSerializer = serializer
? new serializer(internalFieldsArray, keysToRemove)
: new OARepoDepositSerializer(internalFieldsArray, keysToRemove);
@@ -147,20 +166,23 @@ export const useDepositApiClient = (
? new baseApiClient(createUrl, recordSerializer)
: new OARepoDepositApiClient(createUrl, recordSerializer);
- async function save () {
+ async function save(saveWithoutDisplayingValidationErrors = false) {
let response;
setSubmitting(true);
+ setIsSaving(true);
+ // purge any existing errors in internal fields before making save action
+ const valuesWithoutInternalFields = _omit(values, internalFieldsArray);
setErrors({});
try {
- response = await apiClient.saveOrCreateDraft(values);
+ response = await apiClient.saveOrCreateDraft(valuesWithoutInternalFields);
// when I am creating a new draft, it saves the response into formik's state, so that I would have access
// to the draft and draft links in the app. I we don't do that then each time I click on save it will
// create new draft, as I don't actually refresh the page, so the record from html is still empty. Invenio,
// solves this by keeping record in the store, but the idea here is to not create some central state,
// but use formik as some sort of auxiliary state.
- if (!values.id) {
+ if (!valuesWithoutInternalFields.id) {
window.history.replaceState(
undefined,
"",
@@ -178,7 +200,7 @@ export const useDepositApiClient = (
// save accepts posts/puts even with validation errors. Here I check if there are some errors in the response
// body. Here I am setting the individual error messages to the field
- if (response.errors) {
+ if (!saveWithoutDisplayingValidationErrors && response.errors) {
response.errors.forEach((error) =>
setFieldError(error.field, error.messages[0])
);
@@ -191,7 +213,8 @@ export const useDepositApiClient = (
});
return false;
}
- setFieldValue("successMessage", i18next.t("Draft saved successfully."));
+ if (!saveWithoutDisplayingValidationErrors)
+ setFieldValue("successMessage", i18next.t("Draft saved successfully."));
return response;
} catch (error) {
// handle 400 errors. Normally, axios would put messages in error.response. But for example
@@ -204,12 +227,14 @@ export const useDepositApiClient = (
return false;
} finally {
setSubmitting(false);
+ setIsSaving(false);
}
}
- async function publish () {
+ async function publish() {
// call save and if save returns false, exit
const saveResult = await save();
+
if (!saveResult) {
setFieldValue(
"BEvalidationErrors.errorMessage",
@@ -232,6 +257,7 @@ export const useDepositApiClient = (
return;
}
setSubmitting(true);
+ setIsPublishing(true);
let response;
try {
response = await apiClient.publishDraft(saveResult);
@@ -266,19 +292,21 @@ export const useDepositApiClient = (
return false;
} finally {
setSubmitting(false);
+ setIsPublishing(false);
}
}
- async function read (recordUrl) {
+ async function read(recordUrl) {
return await apiClient.readDraft({ self: recordUrl });
}
- async function _delete (redirectUrl) {
+ async function _delete(redirectUrl) {
if (!redirectUrl)
throw new Error(
"You must provide url where to be redirected after deleting a draft"
);
setSubmitting(true);
+ setIsDeleting(true);
try {
let response = await apiClient.deleteDraft(values);
@@ -298,6 +326,7 @@ export const useDepositApiClient = (
return false;
} finally {
setSubmitting(false);
+ setIsDeleting(false);
}
}
// we return also recordSerializer and apiClient instances, if someone wants to use this hook
@@ -305,6 +334,9 @@ export const useDepositApiClient = (
return {
values,
isSubmitting,
+ isSaving,
+ isPublishing,
+ isDeleting,
save,
publish,
read,
@@ -315,3 +347,51 @@ export const useDepositApiClient = (
formik,
};
};
+
+export const useDepositFileApiClient = (baseApiClient) => {
+ const formik = useFormikContext();
+
+ const { isSubmitting, values, setFieldValue, setSubmitting, setValues } =
+ formik;
+
+ const apiClient = baseApiClient
+ ? new baseApiClient()
+ : new OARepoDepositFileApiClient();
+
+ async function read(draft) {
+ return await apiClient.readDraftFiles(draft);
+ }
+ async function _delete(file) {
+ setValues(
+ _omit(values, [
+ "errors",
+ "BEvalidationErrors",
+ "FEvalidationErrors",
+ "httpErrors",
+ "successMessage",
+ ])
+ );
+ setSubmitting(true);
+ try {
+ let response = await apiClient.deleteFile(file?.links);
+ return Promise.resolve(response);
+ } catch (error) {
+ setFieldValue(
+ "httpErrors",
+ error?.response?.data?.message ?? error.message
+ );
+ return false;
+ } finally {
+ setSubmitting(false);
+ }
+ }
+ return {
+ values,
+ isSubmitting,
+ _delete,
+ read,
+ apiClient,
+ formik,
+ setFieldValue,
+ };
+};
diff --git a/oarepo_ui/theme/webpack.py b/oarepo_ui/theme/webpack.py
index a4aa7b65..b8d06979 100644
--- a/oarepo_ui/theme/webpack.py
+++ b/oarepo_ui/theme/webpack.py
@@ -73,7 +73,9 @@
"oarepo_ui_components": "./js/custom-components.js",
},
dependencies=dependencies,
- devDependencies={},
+ devDependencies={
+ "eslint-plugin-i18next":"^6.0.3"
+ },
aliases={
**aliases,
"@translations/oarepo_ui": "translations/oarepo_ui",
diff --git a/setup.cfg b/setup.cfg
index f4b9d980..16acde57 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
[metadata]
name = oarepo-ui
-version = 5.0.98
+version = 5.0.99
description = UI module for invenio 3.5+
long_description = file: README.md
long_description_content_type = text/markdown