Skip to content

Commit

Permalink
Merge pull request #236 from oarepo/mirekys/fe-247-implement-reusable…
Browse files Browse the repository at this point in the history
…-suggestion-api-query-hookcomponent

Mirekys/fe 247 implement reusable suggestion api query hook
  • Loading branch information
mirekys authored Oct 8, 2024
2 parents 2a46689 + b6fd0f5 commit db8c927
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export const overridableComponentIds = [
"FormFields.container",
"CustomFields.container",
];

export const DEFAULT_SUGGESTION_SIZE = 20;
138 changes: 130 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,3 +1,5 @@
import * as React from "react";
import axios from "axios";
import { useEffect, useCallback, useState, useContext, useMemo } from "react";
import { FormConfigContext, FieldDataContext } from "./contexts";
import {
Expand All @@ -12,11 +14,15 @@ import _omit from "lodash/omit";
import _pick from "lodash/pick";
import _isEmpty from "lodash/isEmpty";
import _isObject from "lodash/isObject";
import _debounce from "lodash/debounce";
import _uniqBy from "lodash/uniqBy";
import { i18next } from "@translations/oarepo_ui/i18next";
import { relativeUrl } from "../util";
import { getTitleFromMultilingualObject, relativeUrl } from "../util";
import { decode } from "html-entities";
import sanitizeHtml from "sanitize-html";
import { getValidTagsForEditor } from "@js/oarepo_ui";
import { DEFAULT_SUGGESTION_SIZE } from "./constants";
import queryString from "query-string";

export const extractFEErrorMessages = (obj) => {
const errorMessages = [];
Expand Down Expand Up @@ -178,7 +184,7 @@ export const useDepositApiClient = ({
? new baseApiClient(createUrl, recordSerializer)
: new OARepoDepositApiClient(createUrl, recordSerializer);

async function save(saveWithoutDisplayingValidationErrors = false) {
async function save (saveWithoutDisplayingValidationErrors = false) {
let response;
let errorsObj = {};
const errorPaths = [];
Expand Down Expand Up @@ -250,7 +256,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 @@ -319,11 +325,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 Down Expand Up @@ -351,7 +357,7 @@ export const useDepositApiClient = ({
}
}

async function preview() {
async function preview () {
setSubmitting(true);
try {
const saveResult = await save();
Expand Down Expand Up @@ -419,10 +425,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 @@ -490,4 +496,120 @@ export const useSanitizeInput = () => {
};
};

export const useSuggestionApi = ({
initialSuggestions = [],
serializeSuggestions = (suggestions) =>
suggestions.map((item) => ({
text: getTitleFromMultilingualObject(item.title),
value: item.id,
key: item.id,
})),
debounceTime = 500,
preSearchChange = (x) => x,
suggestionAPIUrl,
suggestionAPIQueryParams = {},
suggestionAPIHeaders = {},
searchQueryParamName = "suggest",
}) => {
const _initialSuggestions = initialSuggestions
? serializeSuggestions(initialSuggestions)
: [];

const [suggestions, setSuggestions] = useState(_initialSuggestions);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [noResults, setNoResults] = useState(false);
const [query, setQuery] = useState("");
// Inspired by: https://dev.to/alexdrocks/using-lodash-debounce-with-react-hooks-for-an-async-data-fetching-input-2p4g
const [didMount, setDidMount] = useState(false);

const debouncedSearch = useMemo(
() =>
_debounce((cancelToken) => fetchSuggestions(cancelToken), debounceTime),
[debounceTime, query]
);

useEffect(() => {
return () => {
// Make sure to stop the invocation of the debounced function after unmounting
debouncedSearch.cancel();
};
}, [debouncedSearch]);

React.useEffect(() => {
if (!didMount) {
// required to not call Suggestion API on initial render
setDidMount(true);
return;
}

const cancelToken = axios.CancelToken.source();
debouncedSearch(cancelToken);

return () => {
cancelToken.cancel();
};
}, [query, suggestionAPIUrl, searchQueryParamName]); // suggestionAPIQueryParams, suggestionAPIHeaders]);

function fetchSuggestions (cancelToken) {
setLoading(true);
setNoResults(false);
setSuggestions(initialSuggestions);
setError(null);

axios
.get(suggestionAPIUrl, {
params: {
[searchQueryParamName]: query,
size: DEFAULT_SUGGESTION_SIZE,
...suggestionAPIQueryParams,
},
headers: suggestionAPIHeaders,
cancelToken: cancelToken.token,
// There is a bug in axios that prevents brackets from being encoded,
// remove the paramsSerializer when fixed.
// https://github.com/axios/axios/issues/3316
paramsSerializer: (params) =>
queryString.stringify(params, { arrayFormat: "repeat" }),
})
.then((res) => {
const searchHits = res?.data?.hits?.hits;
if (searchHits.length === 0) {
setNoResults(true);
}

const serializedSuggestions = serializeSuggestions(searchHits);
setSuggestions(_uniqBy(serializedSuggestions, "value"));
})
.catch((err) => {
setError(err);
})
.finally(() => {
setLoading(false);
});
}

const executeSearch = React.useCallback(
(searchQuery) => {
const newQuery = preSearchChange(searchQuery);
// If there is no query change, then keep prevState suggestions
if (query === newQuery) {
return;
}

setQuery(newQuery);
},
[query]
);

return {
suggestions,
error,
loading,
query,
noResults,
executeSearch,
};
};

export default useSanitizeInput;
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.2.13
version = 5.2.14
description = UI module for invenio 3.5+
long_description = file: README.md
long_description_content_type = text/markdown
Expand Down

0 comments on commit db8c927

Please sign in to comment.