Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into nrp-cli-12
Browse files Browse the repository at this point in the history
  • Loading branch information
mesemus committed Nov 18, 2023
2 parents 292fba9 + f007b17 commit ba7162a
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 22 deletions.
14 changes: 14 additions & 0 deletions oarepo_ui/resources/file_resource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from invenio_records_resources.resources import FileResource


class S3RedirectFileResource(FileResource):
"""
A workaround for the fact that the file resource with S3 backend does not
return HTTP 302 for pre-signed URLs.
"""
def read_content(self):
ret = super().read_content()
if ret[0].status_code == 302:
return ret[0], 302
else:
return ret
1 change: 1 addition & 0 deletions oarepo_ui/resources/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ def empty_record(self, resource_requestctx, **kwargs):
record = deepmerge.always_merger.merge(
record, copy.deepcopy(self.config.empty_record)
)
record['metadata']={}
self.run_components(
"empty_record", resource_requestctx=resource_requestctx, record=record
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,33 @@ const CustomMessage = ({ children, ...uiProps }) => {
};
export const FormFeedback = () => {
const { values } = useFormikContext();
const validationErrors = getIn(values, "validationErrors", {});
const beValidationErrors = getIn(values, "BEvalidationErrors", {});
const feValidationErrors = getIn(values, "FEvalidationErrors", {});
let httpError = getIn(values, "httpErrors", "");
if (httpError?.response?.data) {
httpError = httpError?.response?.data.message;
}
const successMessage = getIn(values, "successMessage", "");
if (!_isEmpty(validationErrors))
if (!_isEmpty(beValidationErrors))
return (
<CustomMessage negative color="orange">
<Message.Header>{validationErrors?.errorMessage}</Message.Header>
<Message.Header>{beValidationErrors?.errorMessage}</Message.Header>
<Message.List>
{validationErrors?.errors?.map((error, index) => (
<Message.Item key={`${error.field}-${index}`}>{`${titleCase(error.field)}:${
error.messages[0]
}`}</Message.Item>
{beValidationErrors?.errors?.map((error, index) => (
<Message.Item key={`${error.field}-${index}`}>{`${titleCase(
error.field
)}: ${error.messages[0]}`}</Message.Item>
))}
</Message.List>
</CustomMessage>
);
if (!_isEmpty(feValidationErrors))
return (
<CustomMessage negative color="orange">
<Message.Header>{feValidationErrors?.errorMessage}</Message.Header>
<Message.List>
{feValidationErrors?.errors?.map((error, index) => (
<Message.Item key={`${error.field}-${index}`}>{error}</Message.Item>
))}
</Message.List>
</CustomMessage>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ I18nRichInputField.propTypes = {
editorConfig: PropTypes.object,
languageOptions: PropTypes.array,
lngFieldWidth: PropTypes.number,
usedLanguages: PropTypes.array,
};

I18nRichInputField.defaultProps = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ I18nTextInputField.propTypes = {
optimized: PropTypes.bool,
languageOptions: PropTypes.array,
lngFieldWidth: PropTypes.number,
usedLanguages: PropTypes.array,
};

I18nTextInputField.defaultProps = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ArrayFieldItem,
useDefaultLocale,
useFormFieldValue,
useShowEmptyValue,
} from "@js/oarepo_ui";
import { i18next } from "@translations/oarepo_ui/i18next";
import { useFormikContext, getIn } from "formik";
Expand All @@ -17,31 +18,32 @@ export const MultilingualTextInput = ({
label,
labelIcon,
required,
emptyNewInput,
defaultNewValue,
rich,
editorConfig,
textFieldLabel,
textFieldIcon,
helpText,
addButtonLabel,
lngFieldWidth,
showEmptyValue,
...uiProps
}) => {
const { defaultLocale } = useDefaultLocale();
const { values } = useFormikContext();
const { usedSubValues, defaultNewValue } = useFormFieldValue({
const { usedSubValues, defaultNewValue: getNewValue } = useFormFieldValue({
defaultValue: defaultLocale,
fieldPath,
subValuesPath: "lang",
});
const value = getIn(values, fieldPath);
const usedLanguages = usedSubValues(value);
const newValue = defaultNewValue(emptyNewInput, usedLanguages);

useShowEmptyValue(fieldPath, defaultNewValue, showEmptyValue);
return (
<ArrayField
addButtonLabel={addButtonLabel}
defaultNewValue={newValue}
defaultNewValue={getNewValue(defaultNewValue, usedLanguages)}
fieldPath={fieldPath}
label={
<FieldLabel htmlFor={fieldPath} icon={labelIcon ?? ""} label={label} />
Expand Down Expand Up @@ -101,14 +103,18 @@ MultilingualTextInput.propTypes = {
helpText: PropTypes.string,
addButtonLabel: PropTypes.string,
lngFieldWidth: PropTypes.number,
rich: PropTypes.bool,
defaultNewValue: PropTypes.object,
showEmptyValue: PropTypes.bool,
};

MultilingualTextInput.defaultProps = {
emptyNewInput: {
defaultNewValue: {
lang: "",
value: "",
},
rich: false,
label: undefined,
addButtonLabel: i18next.t("Add another language"),
showEmptyValue: false,
};
89 changes: 81 additions & 8 deletions oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/forms/hooks.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,36 @@
import * as React from "react";
import { FormConfigContext } from "./contexts";
import { OARepoDepositApiClient, OARepoDepositSerializer } from "../api";
import { useFormikContext } from "formik";
import _get from "lodash/get";
import _set from "lodash/set";
import { useFormikContext, getIn } from "formik";
import _omit from "lodash/omit";
import _pick from "lodash/pick";
import _isEmpty from "lodash/isEmpty";
import _isObject from "lodash/isObject";
import { i18next } from "@translations/oarepo_ui/i18next";
import { relativeUrl } from "../util";

const extractFEErrorMessages = (obj) => {
const errorMessages = [];

const traverse = (obj) => {
if (typeof obj === "string") {
errorMessages.push(obj);
} else if (Array.isArray(obj)) {
obj.forEach((item) => traverse(item));
} else if (typeof obj === "object") {
for (const key in obj) {
traverse(obj[key]);
}
}
};

traverse(obj);
const uniqueErrorMessages = [...new Set(errorMessages)];
return uniqueErrorMessages;
};

export const useFormConfig = () => {
const context = React.useContext(FormConfigContext);
if (!context) {
Expand Down Expand Up @@ -45,7 +66,7 @@ export const useConfirmationModal = () => {
return { isModalOpen, handleCloseModal, handleOpenModal };
};

export const useFormFieldValue = ({ fieldPath, 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")) || []
Expand All @@ -55,12 +76,48 @@ export const useFormFieldValue = ({ fieldPath, subValuesPath, defaultValue, subV
return { usedSubValues, defaultNewValue }
}

export const useShowEmptyValue = (
fieldPath,
defaultNewValue,
showEmptyValue
) => {
const { values, setFieldValue } = useFormikContext();
const currentFieldValue = getIn(values, fieldPath, []);
React.useEffect(() => {
if (!showEmptyValue) return;
if (!_isEmpty(currentFieldValue)) return;
if (defaultNewValue === undefined) {
console.error(
"Default value for new input must be provided. Field: ",
fieldPath
);
return;
}
if (!fieldPath) {
console.error("Fieldpath must be provided");
return;
}
// to be used with invenio array fields that always push objects and add the __key property
if (!_isEmpty(defaultNewValue) && _isObject(defaultNewValue)) {
currentFieldValue.push({
__key: currentFieldValue.length,
...defaultNewValue,
});
setFieldValue(fieldPath, currentFieldValue);
} else if (typeof defaultNewValue === "string") {
currentFieldValue.push(defaultNewValue);
setFieldValue(fieldPath, currentFieldValue);
}
}, [showEmptyValue, setFieldValue, fieldPath, defaultNewValue]);
};

export const useDepositApiClient = (
baseApiClient,
serializer,
internalFieldsArray = [
"errors",
"validationErrors",
"BEvalidationErrors",
"FEvalidationErrors",
"httpErrors",
"successMessage",
],
Expand All @@ -78,7 +135,6 @@ export const useDepositApiClient = (
setFieldError,
setFieldValue,
} = formik;

const {
formConfig: { createUrl },
} = useFormConfig();
Expand Down Expand Up @@ -127,7 +183,7 @@ export const useDepositApiClient = (
setFieldError(error.field, error.messages[0])
);
// here I am setting the state to be used by FormFeedback componene that plugs into the formik's context.
setFieldValue("validationErrors", {
setFieldValue("BEvalidationErrors", {
errors: response.errors,
errorMessage: i18next.t(
"Draft saved with validation errors. Fields listed below that failed validation were not saved to the server"
Expand All @@ -154,10 +210,27 @@ export const useDepositApiClient = (
async function publish () {
// call save and if save returns false, exit
const saveResult = await save();
if (!saveResult) return;
if (!saveResult) {
setFieldValue(
"BEvalidationErrors.errorMessage",
i18next.t(
"Draft was saved but could not be published due to following validation errors"
)
);
return;
}
// imperative form validation, if fails exit
const validationErrors = await validateForm();
if (!_isEmpty(validationErrors)) return;
const FEvalidationErrors = await validateForm();
// show also front end validation errors grouped on the top similar to BE validation errors for consistency
if (!_isEmpty(FEvalidationErrors)) {
setFieldValue("FEvalidationErrors", {
errors: extractFEErrorMessages(FEvalidationErrors.metadata),
errorMessage: i18next.t(
"Draft was saved but could not be published due to following validation errors"
),
});
return;
}
setSubmitting(true);
let response;
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"Draft saved successfully.": "Pracovní záznam byl uložen.",
"Draft published successfully. Redirecting to record's detail page ...": "Pracovní záznam byl úspěšně publikován. Budete přesměrováni na stránku s detailem záznamu …",
"Draft deleted successfully. Redirecting to the main page ...": "Pracovní verze záznamu byla smazána. Za okamžik Vás přesměrujeme na hlavní stránku.",
"Draft was saved but could not be published due to following validation errors": "Koncept byl uložen, ale nemohl být publikován kvůli následujícím chybám ověření:",
"Start over": "Začít znovu",
"resultsPerPage": "Výsledků na stránku",
"Filters": "Filtry",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"Draft saved successfully.": "Draft saved successfully.",
"Draft published successfully. Redirecting to record's detail page ...": "Draft published successfully. Redirecting to record's detail page ...",
"Draft deleted successfully. Redirecting to the main page ...": "Draft deleted successfully. Redirecting to the main page ...",
"Draft was saved but could not be published due to following validation errors": "Draft was saved but could not be published due to following validation errors:",
"Start over": "Start over",
"resultsPerPage": "Results per page",
"Filters": "Filters",
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = oarepo-ui
version = 5.0.82
version = 5.0.83
description = UI module for invenio 3.5+
long_description = file: README.md
long_description_content_type = text/markdown
Expand Down
2 changes: 1 addition & 1 deletion tests/test_ui_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@


def test_ui_resource_create_new(app, record_ui_resource, record_service):
assert record_ui_resource.empty_record(None) == {"title": None}
assert record_ui_resource.empty_record(None) == {'metadata': {}, 'title': None}


def test_ui_resource_form_config(app, record_ui_resource):
Expand Down

0 comments on commit ba7162a

Please sign in to comment.