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