Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reworking requests #235

Merged
merged 14 commits into from
Oct 29, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@
<td>{{ file.size | filesizeformat }}</td>
<td>
{% if file.links.preview %}
<button class="ui button">
<button class="ui button transparent">
<i
class="ui icon zoom openPreviewIcon"
data-preview-link="{{ file.links.preview }}"
></i>
</button>
{% endif %}
<button class="ui button">
<button class="ui button transparent">
<a href="{{ file.links.content }}"
><i class="ui icon download"></i
></a>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect } from "react";
import PropTypes from "prop-types";
import { BaseForm } from "../BaseForm";
import { FormFeedback } from "../FormFeedback";
Expand All @@ -7,11 +7,17 @@ import { SaveButton } from "../SaveButton";
import { PublishButton } from "../PublishButton";
import { PreviewButton } from "../PreviewButton";
import { Grid, Ref, Sticky, Card, Header } from "semantic-ui-react";
import { useFormConfig, getTitleFromMultilingualObject } from "@js/oarepo_ui";
import {
useFormConfig,
getTitleFromMultilingualObject,
serializeErrors,
decodeUnicodeBase64,
} from "@js/oarepo_ui";
import { buildUID } from "react-searchkit";
import Overridable from "react-overridable";
import { CustomFields } from "react-invenio-forms";
import { getIn, useFormikContext } from "formik";
import { i18next } from "@translations/oarepo_ui/i18next";

const FormTitle = () => {
const { values } = useFormikContext();
Expand All @@ -32,6 +38,35 @@ export const BaseFormLayout = ({ formikProps }) => {
const {
formConfig: { custom_fields: customFields },
} = useFormConfig();
// on chrome there is an annoying issue where after deletion you are redirected, and then
// if you click back on browser <-, it serves you the deleted page, which does not exist from the cache.
// on firefox it does not happen.
useEffect(() => {
const handleUnload = () => {};

const handleBeforeUnload = () => {};

window.addEventListener("unload", handleUnload);
window.addEventListener("beforeunload", handleBeforeUnload);

return () => {
window.removeEventListener("unload", handleUnload);
window.removeEventListener("beforeunload", handleBeforeUnload);
};
}, []);

const urlHash = window.location.hash.substring(1);
let errorData;
if (urlHash) {
const decodedData = decodeUnicodeBase64(urlHash);
errorData = JSON.parse(decodedData);
window.history.replaceState(
null,
null,
window.location.pathname + window.location.search
mirekys marked this conversation as resolved.
Show resolved Hide resolved
);
}

return (
<BaseForm
onSubmit={() => {}}
Expand All @@ -40,6 +75,15 @@ export const BaseFormLayout = ({ formikProps }) => {
validateOnChange: false,
validateOnBlur: false,
enableReinitialize: true,
initialErrors:
errorData?.errors?.length > 0
? serializeErrors(
errorData.errors,
i18next.t(
"Your draft has validation errors. Please correct them and try again:"
)
)
: {},
...formikProps,
}}
>
Expand Down Expand Up @@ -109,7 +153,7 @@ export const BaseFormLayout = ({ formikProps }) => {
</Grid.Column>
{/* TODO:see if there is a way to provide URL here, seems that UI links are empty in the form */}
{/* <Grid.Column width={16} className="pt-10">
<DeleteButton redirectUrl="/other/" />
<DeleteButton redirectUrl="/me/records" />
</Grid.Column> */}
</Grid>
</Card.Content>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const DeleteButton = React.memo(
onClick={openModal}
icon="delete"
labelPosition="left"
content={i18next.t("Delete")}
content={i18next.t("Delete draft")}
type="button"
disabled={isSubmitting}
loading={isSubmitting}
Expand All @@ -59,7 +59,7 @@ export const DeleteButton = React.memo(
}}
icon="delete"
labelPosition="left"
content={i18next.t("Delete")}
content={i18next.t("Delete draft")}
type="button"
/>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const EDTFDatePickerWrapper = ({
handleClear={handleClear}
fieldPath={fieldPath}
clearButtonClassName={clearButtonClassName}
autoComplete="off"
{...customInputProps}
/>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const InputElement = forwardRef(
clearButtonClassName,
handleClear,
onKeyDown,
autoComplete,
},
ref
) => {
Expand All @@ -30,6 +31,7 @@ export const InputElement = forwardRef(
placeholder={placeholder}
className={className}
id={fieldPath}
autoComplete={autoComplete}
icon={
value ? (
<Icon
Expand All @@ -54,6 +56,7 @@ InputElement.propTypes = {
className: PropTypes.string,
placeholder: PropTypes.string,
onKeyDown: PropTypes.func,
autoComplete: PropTypes.string,
};

InputElement.defaultProps = {
Expand Down
73 changes: 46 additions & 27 deletions oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/forms/hooks.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import * as React from "react";
import axios from "axios";
import { useEffect, useCallback, useState, useContext, useMemo } from "react";
import {
useEffect,
useCallback,
useState,
useContext,
useMemo,
useRef,
} from "react";
import { FormConfigContext, FieldDataContext } from "./contexts";
import {
OARepoDepositApiClient,
Expand Down Expand Up @@ -68,7 +75,7 @@ export const useFieldData = () => {
const context = useContext(FieldDataContext);
if (!context) {
throw new Error(
"useFormConfig must be used inside FieldDataContext .Provider"
"useFormConfig must be used inside FieldDataContext.Provider"
);
}
return context;
Expand All @@ -92,8 +99,19 @@ export const useVocabularyOptions = (vocabularyType) => {

export const useConfirmationModal = () => {
const [isOpen, setIsOpen] = useState(false);
const isMounted = useRef(null);
isMounted.current = true;

useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);

const close = useCallback(() => setIsOpen(false), []);
const close = useCallback(() => {
if (!isMounted.current) return;
setIsOpen(false);
}, []);
const open = useCallback(() => setIsOpen(true), []);

return { isOpen, close, open };
Expand Down Expand Up @@ -158,7 +176,7 @@ export const useShowEmptyValue = (
export const useDepositApiClient = ({
baseApiClient,
serializer,
internalFieldsArray = ["errors"],
internalFieldsArray = ["errors", "expanded"],
keysToRemove = ["__key"],
} = {}) => {
const formik = useFormikContext();
Expand All @@ -184,7 +202,11 @@ export const useDepositApiClient = ({
? new baseApiClient(createUrl, recordSerializer)
: new OARepoDepositApiClient(createUrl, recordSerializer);

async function save (saveWithoutDisplayingValidationErrors = false) {
async function save({
saveWithoutDisplayingValidationErrors = false,
errorMessage = null,
successMessage = null,
} = {}) {
let response;
let errorsObj = {};
const errorPaths = [];
Expand Down Expand Up @@ -218,17 +240,20 @@ export const useDepositApiClient = ({
if (response.errors.length > 0) {
errorsObj["BEvalidationErrors"] = {
errors: response.errors,
errorMessage: i18next.t(
"Draft saved with validation errors. Fields listed below that failed validation were not saved to the server"
),
errorMessage:
errorMessage ||
i18next.t(
"Draft saved with validation errors. Fields listed below that failed validation were not saved to the server"
),
errorPaths,
};
}

return false;
}
if (!saveWithoutDisplayingValidationErrors)
errorsObj["successMessage"] = i18next.t("Draft saved successfully.");
errorsObj["successMessage"] =
successMessage || i18next.t("Draft saved successfully.");
return response;
} catch (error) {
// handle 400 errors. Normally, axios would put messages in error.response. But for example
Expand Down Expand Up @@ -256,7 +281,7 @@ export const useDepositApiClient = ({
}
}

async function publish ({ validate = false } = {}) {
async function publish({ validate = false } = {}) {
// call save and if save returns false, exit
const saveResult = await save();

Expand Down Expand Up @@ -325,11 +350,11 @@ export const useDepositApiClient = ({
}
}

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"
Expand All @@ -342,7 +367,7 @@ export const useDepositApiClient = ({
setFieldError(
"successMessage",
i18next.t(
"Draft deleted successfully. Redirecting to the main page ..."
"Draft deleted successfully. Redirecting to your dashboard ..."
)
);
return response;
Expand All @@ -357,20 +382,14 @@ export const useDepositApiClient = ({
}
}

async function preview () {
async function preview() {
setSubmitting(true);
try {
const saveResult = await save();
const saveResult = await save({
saveWithoutDisplayingValidationErrors: true,
});

if (!saveResult) {
setFieldError(
"BEvalidationErrors.errorMessage",
i18next.t(
"Your draft was saved. If you wish to preview it, please correct the following validation errors and click preview again:"
)
);
return;
} else {
if (saveResult?.links?.self_html) {
const url = saveResult.links.self_html;
setFieldError(
"successMessage",
Expand Down Expand Up @@ -425,10 +444,10 @@ export const useDepositFileApiClient = (baseApiClient) => {
? new baseApiClient()
: new OARepoDepositFileApiClient();

async function read (draft) {
async function read(draft) {
return await apiClient.readDraftFiles(draft);
}
async function _delete (file) {
async function _delete(file) {
setValues(_omit(values, ["errors"]));
setSubmitting(true);
try {
Expand Down Expand Up @@ -551,7 +570,7 @@ export const useSuggestionApi = ({
};
}, [query, suggestionAPIUrl, searchQueryParamName]); // suggestionAPIQueryParams, suggestionAPIHeaders]);

function fetchSuggestions (cancelToken) {
function fetchSuggestions(cancelToken) {
setLoading(true);
setNoResults(false);
setSuggestions(initialSuggestions);
Expand Down
27 changes: 27 additions & 0 deletions oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/forms/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import Overridable, {
overrideStore,
} from "react-overridable";
import { BaseFormLayout } from "./components/BaseFormLayout";
import { setIn } from "formik";

export function parseFormAppConfig(rootElementId = "form-app") {
const rootEl = document.getElementById(rootElementId);
Expand Down Expand Up @@ -200,3 +201,29 @@ export const getValidTagsForEditor = (tags, attr) => {

return result.join(",");
};

export const serializeErrors = (
errors,
message = i18next.t(
"Draft saved with validation errors. Fields listed below that failed validation were not saved to the server"
)
) => {
if (errors?.length > 0) {
let errorsObj = {};
const errorPaths = [];
for (const error of errors) {
errorsObj = setIn(errorsObj, error.field, error.messages.join(" "));
mirekys marked this conversation as resolved.
Show resolved Hide resolved
errorPaths.push(error.field);
}

errorsObj["BEvalidationErrors"] = {
errors: errors,
errorMessage: message,
errorPaths,
};

return errorsObj;
} else {
return {};
}
};
22 changes: 22 additions & 0 deletions oarepo_ui/theme/assets/semantic-ui/js/oarepo_ui/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import _uniqBy from "lodash/uniqBy";
import * as Yup from "yup";
import { i18next } from "@translations/oarepo_ui/i18next";
import { format } from "date-fns";
import axios from "axios";

export const getInputFromDOM = (elementName) => {
const element = document.getElementsByName(elementName);
Expand Down Expand Up @@ -273,3 +274,24 @@ export const goBack = (fallBackURL = "/") => {
window.location.href = fallBackURL;
}
};

// until we start using v4 of react-invenio-forms. They switched to vnd zenodo accept header
const baseAxiosConfiguration = {
withCredentials: true,
xsrfCookieName: "csrftoken",
xsrfHeaderName: "X-CSRFToken",
headers: {
Accept: "application/vnd.inveniordm.v1+json",
"Content-Type": "application/json",
},
};

export const http = axios.create(baseAxiosConfiguration);

export const encodeUnicodeBase64 = (str) => {
return btoa(encodeURIComponent(str));
mirekys marked this conversation as resolved.
Show resolved Hide resolved
};

export const decodeUnicodeBase64 = (base64) => {
return decodeURIComponent(atob(base64));
};
Loading