From 0b9681c2612bc17e7d1962fa09d177d9cae5183b Mon Sep 17 00:00:00 2001 From: karolina-siemieniuk-morawska <55505399+karolina-siemieniuk-morawska@users.noreply.github.com> Date: Wed, 17 Jul 2024 13:08:22 +0200 Subject: [PATCH] ui: set up and use redux in new holdingpen * ref: cern-sis/issues-inspire#481 --- ui/package.json | 4 +- ui/src/App.jsx | 1 + ui/src/actions/actionTypes.ts | 17 +- ui/src/actions/holdingpen.ts | 230 +++++++++++++++ ui/src/common/PrivateRoute.tsx | 20 +- .../SearchPagination/SearchPagination.tsx | 4 +- .../SearchPagiantion.test.jsx.snap | 2 + ui/src/common/components/SearchResults.tsx | 2 +- ui/src/common/layouts/Header/HeaderMenu.tsx | 58 ++-- .../layouts/Header/HeaderMenuContainer.tsx | 5 + .../__snapshots__/HeaderMenu.test.jsx.snap | 1 + ui/src/common/routes.ts | 1 - ui/src/fixtures/store.js | 2 + .../holdingpen-new/__tests__/index.test.tsx | 21 +- .../components/AuthorResultItem.tsx | 40 +-- .../holdingpen-new/components/Breadcrumbs.tsx | 15 +- .../holdingpen-new/components/LoginPage.tsx | 77 +++++ .../DashboardPageContainer.tsx | 15 +- .../AuthorDetailPageContainer.tsx | 150 +++++----- .../LoginPageContainer/LoginPageContainer.tsx | 116 +------- .../SearchPageContainer.tsx | 278 ++++++++++-------- .../__tests__/DetailPageContainer.test.tsx | 21 +- ui/src/holdingpen-new/index.tsx | 41 +-- ui/src/holdingpen-new/notifications.ts | 10 + ui/src/holdingpen-new/utils/utils.ts | 40 +-- .../literature/__tests__/citeArticle.test.js | 12 +- .../__tests__/CiteAllAction.test.jsx | 12 +- ui/src/reducers/holdingpen.js | 64 ++++ ui/src/reducers/index.js | 7 +- ui/src/types/index.ts | 4 +- ui/yarn.lock | 51 ++-- 31 files changed, 835 insertions(+), 486 deletions(-) create mode 100644 ui/src/actions/holdingpen.ts create mode 100644 ui/src/holdingpen-new/components/LoginPage.tsx create mode 100644 ui/src/holdingpen-new/notifications.ts create mode 100644 ui/src/reducers/holdingpen.js diff --git a/ui/package.json b/ui/package.json index e70b228a0..6042487e1 100644 --- a/ui/package.json +++ b/ui/package.json @@ -45,7 +45,7 @@ "@typescript-eslint/eslint-plugin": "^5.40.1", "@typescript-eslint/parser": "^5.40.1", "antd": "4.24.8", - "axios": "^0.18.0", + "axios": "^1.7.2", "axios-hooks": "^1.9.0", "classnames": "^2.2.6", "connected-react-router": "^6.4.0", @@ -109,7 +109,7 @@ "@types/uuid": "^9.0.1", "@types/yup": "^0.29.14", "@wojtekmaj/enzyme-adapter-react-17": "^0.8.0", - "axios-mock-adapter": "^1.15.0", + "axios-mock-adapter": "^1.22.0", "bundlemon": "^2.0.0-rc.1", "enzyme": "^3.3.0", "enzyme-to-json": "^3.3.3", diff --git a/ui/src/App.jsx b/ui/src/App.jsx index 26cdc8899..9881c65ee 100644 --- a/ui/src/App.jsx +++ b/ui/src/App.jsx @@ -98,6 +98,7 @@ function App({ userRoles, dispatch, guideModalVisibility }) { path={HOLDINGPEN_NEW} component={HoldingpenNew$} authorizedRoles={SUPERUSER_OR_CATALOGER} + holdingpen /> diff --git a/ui/src/actions/actionTypes.ts b/ui/src/actions/actionTypes.ts index 1b402d959..cbcaa30cc 100644 --- a/ui/src/actions/actionTypes.ts +++ b/ui/src/actions/actionTypes.ts @@ -63,7 +63,7 @@ export const USER_SET_ORCID_PUSH_SETTING_ERROR = export const SETTINGS_CHANGE_EMAIL_REQUEST = 'SETTINGS_CHANGE_EMAIL_REQUEST'; export const SETTINGS_CHANGE_EMAIL_ERROR = 'SETTINGS_CHANGE_EMAIL_ERROR'; export const SETTINGS_CHANGE_EMAIL_SUCCESS = 'SETTINGS_CHANGE_EMAIL_SUCCESS'; - + export const SUBMIT_REQUEST = 'SUBMIT_REQUEST'; export const SUBMIT_ERROR = 'SUBMIT_ERROR'; export const SUBMIT_SUCCESS = 'SUBMIT_SUCCESS'; @@ -115,7 +115,8 @@ export const JOURNAL_ERROR = 'JOURNAL_ERROR'; export const UI_CLOSE_BANNER = 'UI_CLOSE_BANNER'; export const UI_CHANGE_GUIDE_MODAL_VISIBILITY = 'UI_CHANGE_GUIDE_MODAL_VISIBILITY'; -export const UI_SCROLL_VIEWPORT_TO_PREVIOUS_REFERENCE = 'UI_SCROLL_VIEWPORT_TO_PREVIOUS_REFERENCE'; +export const UI_SCROLL_VIEWPORT_TO_PREVIOUS_REFERENCE = + 'UI_SCROLL_VIEWPORT_TO_PREVIOUS_REFERENCE'; export const CLEAR_STATE = 'CLEAR_STATE'; @@ -131,3 +132,15 @@ export const LITERATURE_SET_ASSIGN_LITERATURE_ITEM_DRAWER_VISIBILITY = 'LITERATURE_SET_ASSIGN_LITERATURE_ITEM_DRAWER_VISIBILITY'; export const LITERATURE_SET_CURATE_DRAWER_VISIBILITY = 'LITERATURE_SET_CURATE_DRAWER_VISIBILITY'; + +export const HOLDINGPEN_LOGIN_ERROR = 'HOLDINGPEN_LOGIN_ERROR'; +export const HOLDINGPEN_LOGIN_SUCCESS = 'HOLDINGPEN_LOGIN_SUCCESS'; +export const HOLDINGPEN_LOGOUT_SUCCESS = 'HOLDINGPEN_LOGOUT_SUCCESS'; +export const HOLDINGPEN_SEARCH_REQUEST = 'HOLDINGPEN_SEARCH_REQUEST'; +export const HOLDINGPEN_SEARCH_ERROR = 'HOLDINGPEN_SEARCH_ERROR'; +export const HOLDINGPEN_SEARCH_SUCCESS = 'HOLDINGPEN_SEARCH_SUCCESS'; +export const HOLDINGPEN_SEARCH_QUERY_UPDATE = 'HOLDINGPEN_SEARCH_QUERY_UPDATE'; +export const HOLDINGPEN_SEARCH_QUERY_RESET = 'HOLDINGPEN_SEARCH_QUERY_RESET'; +export const HOLDINGPEN_AUTHOR_REQUEST = 'HOLDINGPEN_AUTHOR_REQUEST'; +export const HOLDINGPEN_AUTHOR_ERROR = 'HOLDINGPEN_AUTHOR_ERROR'; +export const HOLDINGPEN_AUTHOR_SUCCESS = 'HOLDINGPEN_AUTHOR_SUCCESS'; diff --git a/ui/src/actions/holdingpen.ts b/ui/src/actions/holdingpen.ts new file mode 100644 index 000000000..d6259f6f1 --- /dev/null +++ b/ui/src/actions/holdingpen.ts @@ -0,0 +1,230 @@ +/* eslint-disable no-underscore-dangle */ +import { push } from 'connected-react-router'; +import { Action, ActionCreator } from 'redux'; +import axios from 'axios'; + +import { httpErrorToActionPayload } from '../common/utils'; +import { + HOLDINGPEN_LOGIN_ERROR, + HOLDINGPEN_LOGIN_SUCCESS, + HOLDINGPEN_LOGOUT_SUCCESS, + HOLDINGPEN_SEARCH_REQUEST, + HOLDINGPEN_SEARCH_ERROR, + HOLDINGPEN_SEARCH_SUCCESS, + HOLDINGPEN_SEARCH_QUERY_UPDATE, + HOLDINGPEN_AUTHOR_ERROR, + HOLDINGPEN_AUTHOR_REQUEST, + HOLDINGPEN_AUTHOR_SUCCESS, +} from './actionTypes'; +import { + BACKOFFICE_API, + BACKOFFICE_LOGIN, + BACKOFFICE_SEARCH_API, + HOLDINGPEN_NEW, + HOLDINGPEN_LOGIN_NEW, +} from '../common/routes'; +import { Credentials } from '../types'; +import storage from '../common/storage'; +import { notifyLoginError } from '../holdingpen-new/notifications'; +import { refreshToken } from '../holdingpen-new/utils/utils'; + +const httpClient = axios.create(); + +// Request interceptor for API calls +httpClient.interceptors.request.use( + async (config) => { + const token = storage.getSync('holdingpen.token'); + + config.headers = { + 'Content-Type': 'application/json', + ...(token && { Authorization: `Bearer ${token}` }), + }; + + return config; + }, + (error) => Promise.reject(error) +); + +// Response interceptor for API calls +httpClient.interceptors.response.use( + (response) => response, + async (error) => { + if (axios.isAxiosError(error)) { + if ( + error.response?.status === 403 && + (!(error.config as any)._retry as boolean) + ) { + (error.config as any)._retry = true; + try { + const accessToken = await refreshToken(); + axios.defaults.headers.common.Authorization = `Bearer ${accessToken}`; + return httpClient(error.config!); + } catch (tokenError) { + return Promise.reject(tokenError); + } + } + } + return Promise.reject(error); + } +); + +export function holdingpenLoginSuccess() { + return { + type: HOLDINGPEN_LOGIN_SUCCESS, + }; +} + +function holdingpenLoginError(error: { error: Error }) { + return { + type: HOLDINGPEN_LOGIN_ERROR, + payload: error, + }; +} + +function holdingpenLogoutSuccess() { + return { + type: HOLDINGPEN_LOGOUT_SUCCESS, + }; +} + +function searching() { + return { + type: HOLDINGPEN_SEARCH_REQUEST, + }; +} + +function searchSuccess(data: any) { + return { + type: HOLDINGPEN_SEARCH_SUCCESS, + payload: { data }, + }; +} + +function searchError(errorPayload: { error: Error }) { + return { + type: HOLDINGPEN_SEARCH_ERROR, + payload: { ...errorPayload }, + }; +} + +function fetchingAuthor() { + return { + type: HOLDINGPEN_AUTHOR_REQUEST, + }; +} + +function fetchAuthorSuccess(data: any) { + return { + type: HOLDINGPEN_AUTHOR_SUCCESS, + payload: { data }, + }; +} + +function fetchAuthorError(errorPayload: { error: Error }) { + return { + type: HOLDINGPEN_AUTHOR_ERROR, + payload: { ...errorPayload }, + }; +} + +function updateQuery(data: any) { + return { + type: HOLDINGPEN_SEARCH_QUERY_UPDATE, + payload: data, + }; +} + +export function holdingpenLogin( + credentials: Credentials +): (dispatch: ActionCreator) => Promise { + return async (dispatch) => { + try { + const response = await httpClient.post( + BACKOFFICE_LOGIN, + { + email: credentials.email, + password: credentials.password, + }, + { + headers: { + 'Content-Type': 'application/json', + }, + } + ); + + if (response.status === 200) { + const { access, refresh } = await response.data; + storage.set('holdingpen.token', access); + storage.set('holdingpen.refreshToken', refresh); + + dispatch(holdingpenLoginSuccess()); + dispatch(push(HOLDINGPEN_NEW)); + } + } catch (err) { + const { error } = httpErrorToActionPayload(err); + notifyLoginError(error?.detail); + dispatch(holdingpenLoginError({ error })); + } + }; +} + +export function holdingpenLogout(): ( + dispatch: ActionCreator +) => Promise { + return async (dispatch) => { + try { + storage.remove('holdingpen.token'); + storage.remove('holdingpen.refreshToken'); + + dispatch(holdingpenLogoutSuccess()); + dispatch(push(HOLDINGPEN_LOGIN_NEW)); + } catch (error) { + dispatch(holdingpenLogoutSuccess()); + } + }; +} + +export function fetchSearchResults(query: { + page: number; + size: number; +}): (dispatch: ActionCreator) => Promise { + return async (dispatch) => { + dispatch(searching()); + + const resolveQuery = `${BACKOFFICE_SEARCH_API}?page=${query.page}&size=${query.size}`; + + try { + const response = await httpClient.get(`${resolveQuery}`); + dispatch(searchSuccess(response?.data)); + } catch (err) { + const error = httpErrorToActionPayload(err); + dispatch(searchError(error)); + } + }; +} + +export function fetchAuthor( + id: string +): (dispatch: ActionCreator) => Promise { + return async (dispatch) => { + dispatch(fetchingAuthor()); + const resolveQuery = `${BACKOFFICE_API}/${id}`; + + try { + const response = await httpClient.get(`${resolveQuery}`); + dispatch(fetchAuthorSuccess(response?.data)); + } catch (err) { + const error = httpErrorToActionPayload(err); + dispatch(fetchAuthorError(error)); + } + }; +} + +export function searchQueryUpdate(query: { + page: number; + size: number; +}): (dispatch: ActionCreator) => Promise { + return async (dispatch) => { + dispatch(updateQuery(query)); + }; +} diff --git a/ui/src/common/PrivateRoute.tsx b/ui/src/common/PrivateRoute.tsx index f4e31edf0..491d12698 100644 --- a/ui/src/common/PrivateRoute.tsx +++ b/ui/src/common/PrivateRoute.tsx @@ -11,15 +11,11 @@ interface PrivateRouteProps extends ComponentPropsWithoutRef { userRoles: List; authorizedRoles: List; component?: JSX.Element | string | any; - isHoldinpen?: boolean; - loggedInToHoldinpen?: boolean; + holdingpen?: boolean; + loggedInToHoldingpen?: boolean; } -function PrivateRoute({ - isHoldinpen = false, - loggedInToHoldinpen = false, - ...props -}: PrivateRouteProps) { +function PrivateRoute({ ...props }: PrivateRouteProps) { if (props.loggedIn && props.authorizedRoles) { const isUserAuthorized = isAuthorized( props.userRoles, @@ -35,14 +31,14 @@ function PrivateRoute({ ); } - const resolveLoggedIn = props.isHoldinpen - ? props.loggedInToHoldinpen && props.loggedIn + const resolveLoggedIn = props.holdingpen + ? props.loggedInToHoldingpen && props.loggedIn : props.loggedIn; return ( @@ -50,11 +46,13 @@ function PrivateRoute({ } PrivateRoute.defaultProps = { + holdingpen: false, authorizedRoles: null, }; const stateToProps = (state: RootStateOrAny) => ({ loggedIn: state.user.get('loggedIn'), + loggedInToHoldingpen: state.holdingpen.get('loggedIn'), userRoles: state.user.getIn(['data', 'roles']), }); diff --git a/ui/src/common/components/SearchPagination/SearchPagination.tsx b/ui/src/common/components/SearchPagination/SearchPagination.tsx index f33acd838..405b9499e 100644 --- a/ui/src/common/components/SearchPagination/SearchPagination.tsx +++ b/ui/src/common/components/SearchPagination/SearchPagination.tsx @@ -3,7 +3,7 @@ import { Pagination } from 'antd'; import './SearchPagination.less'; -const PAGE_SIZE_OPTIONS = ['25', '50', '100', '250']; +const PAGE_SIZE_OPTIONS = ['10', '25', '50', '100', '250']; const SearchPagination = ({ page, @@ -17,7 +17,7 @@ const SearchPagination = ({ total?: number; pageSize?: number; onPageChange?: (page: number, pageSize: number) => void; - onSizeChange?: (current: number, size: number) => void; + onSizeChange: (current: number, size: number) => void; hideSizeChange?: boolean; }) => { return ( diff --git a/ui/src/common/components/SearchPagination/__tests__/__snapshots__/SearchPagiantion.test.jsx.snap b/ui/src/common/components/SearchPagination/__tests__/__snapshots__/SearchPagiantion.test.jsx.snap index 0bc8715a2..501441e8e 100644 --- a/ui/src/common/components/SearchPagination/__tests__/__snapshots__/SearchPagiantion.test.jsx.snap +++ b/ui/src/common/components/SearchPagination/__tests__/__snapshots__/SearchPagiantion.test.jsx.snap @@ -9,6 +9,7 @@ exports[`SearchPagination renders with all props set 1`] = ` pageSize={10} pageSizeOptions={ Array [ + "10", "25", "50", "100", @@ -35,6 +36,7 @@ exports[`SearchPagination renders with only required props set 1`] = ` pageSize={25} pageSizeOptions={ Array [ + "10", "25", "50", "100", diff --git a/ui/src/common/components/SearchResults.tsx b/ui/src/common/components/SearchResults.tsx index fa16902f6..aff53622d 100644 --- a/ui/src/common/components/SearchResults.tsx +++ b/ui/src/common/components/SearchResults.tsx @@ -31,7 +31,7 @@ const SearchResults = ({ ); } return ( -
+
{renderItem(result)}
); diff --git a/ui/src/common/layouts/Header/HeaderMenu.tsx b/ui/src/common/layouts/Header/HeaderMenu.tsx index fa90660c3..16a3cae2d 100644 --- a/ui/src/common/layouts/Header/HeaderMenu.tsx +++ b/ui/src/common/layouts/Header/HeaderMenu.tsx @@ -31,14 +31,18 @@ interface MenuItem { const HeaderMenu = ({ loggedIn, + loggedInToHoldingpen, onLogoutClick, isCatalogerLoggedIn, profileControlNumber, + onLogout, }: { loggedIn: boolean; onLogoutClick: MouseEventHandler; isCatalogerLoggedIn?: boolean; profileControlNumber?: string; + onLogout: any; + loggedInToHoldingpen: boolean; }) => { const USER_PROFILE_URL = `/authors/${profileControlNumber}`; @@ -46,7 +50,7 @@ const HeaderMenu = ({ { key: 'submit.literature', label: [ - + Literature , ], @@ -54,7 +58,7 @@ const HeaderMenu = ({ { key: 'submit.author', label: [ - + Author , ], @@ -62,7 +66,7 @@ const HeaderMenu = ({ { key: 'submit.job', label: [ - + Job , ], @@ -70,7 +74,7 @@ const HeaderMenu = ({ { key: 'submit.seminar', label: [ - + Seminar , ], @@ -78,7 +82,7 @@ const HeaderMenu = ({ { key: 'submit.conference', label: [ - + Conference , ], @@ -91,7 +95,7 @@ const HeaderMenu = ({ { key: 'submit.institution', label: [ - + Institution , ], @@ -99,7 +103,7 @@ const HeaderMenu = ({ { key: 'submit.experiment', label: [ - + Experiment , ], @@ -107,7 +111,7 @@ const HeaderMenu = ({ { key: 'submit.journal', label: [ - + Journal , ], @@ -124,7 +128,7 @@ const HeaderMenu = ({ { key: 'help.search-tips', label: [ - + Search Tips @@ -134,7 +138,7 @@ const HeaderMenu = ({ { key: 'help.tour', label: [ - + Take the tour @@ -144,8 +148,8 @@ const HeaderMenu = ({ { key: 'help.help-center', label: [ - - + + Help Center , @@ -165,9 +169,13 @@ const HeaderMenu = ({ { key: 'my-profile', label: profileControlNumber - ? [My profile] + ? [ + + My profile + , + ] : [ - + @@ -176,7 +184,11 @@ const HeaderMenu = ({ }, { key: 'settings', - label: [Settings], + label: [ + + Settings + , + ], }, { key: 'logout', @@ -185,12 +197,20 @@ const HeaderMenu = ({ onClick={onLogoutClick} dataTestId="logout" color="white" - key='logout' + key="logout" > Logout , ], }, + loggedInToHoldingpen && { + key: 'logout-holdingpen', + label: [ + onLogout()}> + Logout Holdingpen + , + ], + }, ]; if (loggedIn) { @@ -208,7 +228,11 @@ const HeaderMenu = ({ ...menuItems, { key: 'login', - label: Login, + label: ( + + Login + + ), }, ]; } diff --git a/ui/src/common/layouts/Header/HeaderMenuContainer.tsx b/ui/src/common/layouts/Header/HeaderMenuContainer.tsx index 9d3e5916e..fc16f4586 100644 --- a/ui/src/common/layouts/Header/HeaderMenuContainer.tsx +++ b/ui/src/common/layouts/Header/HeaderMenuContainer.tsx @@ -2,11 +2,13 @@ import { connect, RootStateOrAny } from 'react-redux'; import { Action, ActionCreator } from 'redux'; import { userLogout } from '../../../actions/user'; +import { holdingpenLogout } from '../../../actions/holdingpen'; import { isCataloger } from '../../authorization'; import HeaderMenu from './HeaderMenu'; const stateToProps = (state: RootStateOrAny) => ({ loggedIn: state.user.get('loggedIn'), + loggedInToHoldingpen: state.holdingpen.get('loggedIn'), isCatalogerLoggedIn: isCataloger(state.user.getIn(['data', 'roles'])), profileControlNumber: state.user.getIn(['data', 'profile_control_number']), }); @@ -15,6 +17,9 @@ const dispatchToProps = (dispatch: ActionCreator) => ({ onLogoutClick() { dispatch(userLogout()); }, + onLogout() { + dispatch(holdingpenLogout()); + }, }); export default connect(stateToProps, dispatchToProps)(HeaderMenu); diff --git a/ui/src/common/layouts/Header/__tests__/__snapshots__/HeaderMenu.test.jsx.snap b/ui/src/common/layouts/Header/__tests__/__snapshots__/HeaderMenu.test.jsx.snap index c9eed0073..e1ab19e53 100644 --- a/ui/src/common/layouts/Header/__tests__/__snapshots__/HeaderMenu.test.jsx.snap +++ b/ui/src/common/layouts/Header/__tests__/__snapshots__/HeaderMenu.test.jsx.snap @@ -169,6 +169,7 @@ exports[`HeaderMenu renders when logged in 1`] = ` , ], }, + undefined, ], "key": "account", "label": "Account", diff --git a/ui/src/common/routes.ts b/ui/src/common/routes.ts index 569711d20..a70169de0 100644 --- a/ui/src/common/routes.ts +++ b/ui/src/common/routes.ts @@ -29,7 +29,6 @@ export const HOLDINGPEN_INSPECT = `${HOLDINGPEN}/inspect`; export const HOLDINGPEN_NEW = '/holdingpen-new'; export const HOLDINGPEN_LOGIN_NEW = `${HOLDINGPEN_NEW}/login`; -export const HOLDINGPEN_DASHBOARD_NEW = `${HOLDINGPEN_NEW}/dashboard`; export const HOLDINGPEN_SEARCH_NEW = `${HOLDINGPEN_NEW}/search`; export const BACKOFFICE_LOGIN = 'https://backoffice.dev.inspirebeta.net/api/token/'; diff --git a/ui/src/fixtures/store.js b/ui/src/fixtures/store.js index 675c3d598..c5823d8ea 100644 --- a/ui/src/fixtures/store.js +++ b/ui/src/fixtures/store.js @@ -10,6 +10,7 @@ import { initialState as inspect } from '../reducers/inspect'; import { initialState as exceptions } from '../reducers/exceptions'; import { initialState as ui } from '../reducers/ui'; import { initialState as bibliographyGenerator } from '../reducers/bibliographyGenerator'; +import { initialState as holdingpen } from '../reducers/holdingpen'; import { thunkMiddleware } from '../store'; import { initialState as initialRecordState } from '../reducers/recordsFactory'; @@ -41,6 +42,7 @@ export function getState() { institutions: initialRecordState, experiments: initialRecordState, journals: initialRecordState, + holdingpen, }; } diff --git a/ui/src/holdingpen-new/__tests__/index.test.tsx b/ui/src/holdingpen-new/__tests__/index.test.tsx index 6aeb078bb..681382193 100644 --- a/ui/src/holdingpen-new/__tests__/index.test.tsx +++ b/ui/src/holdingpen-new/__tests__/index.test.tsx @@ -9,10 +9,7 @@ import Holdingpen from '..'; import DashboardPageContainer from '../containers/DashboardPageContainer/DashboardPageContainer'; import SearchPageContainer from '../containers/SearchPageContainer/SearchPageContainer'; import DetailPageContainer from '../containers/DetailPageContainer/DetailPageContainer'; -import { - HOLDINGPEN_DASHBOARD_NEW, - HOLDINGPEN_SEARCH_NEW, -} from '../../common/routes'; +import { HOLDINGPEN_SEARCH_NEW, HOLDINGPEN_NEW } from '../../common/routes'; describe('Holdingpen', () => { const store = getStoreWithState({ @@ -22,12 +19,15 @@ describe('Holdingpen', () => { roles: ['cataloger'], }, }), + holdingpen: fromJS({ + loggedIn: true, + }), }); it('renders initial state', () => { const { container } = render( - + @@ -38,11 +38,8 @@ describe('Holdingpen', () => { it('navigates to DashboardPageContainer when /holdingpen-new/dashboard', () => { const { getByTestId } = render( - - + + ); @@ -53,9 +50,9 @@ describe('Holdingpen', () => { it('navigates to DetailPageContainer when /holdingpen-new/:id', () => { const { getByTestId } = render( - + diff --git a/ui/src/holdingpen-new/components/AuthorResultItem.tsx b/ui/src/holdingpen-new/components/AuthorResultItem.tsx index 65592ed77..bf53fa14a 100644 --- a/ui/src/holdingpen-new/components/AuthorResultItem.tsx +++ b/ui/src/holdingpen-new/components/AuthorResultItem.tsx @@ -57,66 +57,70 @@ const renderWorkflowStatus = (status: string) => { ) : null; }; -const AuthorResultItem = ({ item }: { item: any }) => { - const { - _workflow: workflow, - metadata, - _extra_data: extraData, - } = item?.data || {}; +const AuthorResultItem = ({ id, item }: { id: string; item: any }) => { + const workflow = item?.get('_workflow'); + const metadata = item?.get('metadata'); + const extraData = item?.get('_extra_data'); return ( -
+
Author - {extraData?.user_action && ( + {extraData?.get('user_action') && ( - {resolveDecision(extraData?.user_action)?.text} + {resolveDecision(extraData?.get('user_action'))?.text} )}
- {metadata?.name?.value} + + {metadata?.getIn(['name', 'value'])} +
- {renderWorkflowStatus(workflow?.status)} + {renderWorkflowStatus(workflow?.get('status'))}

{new Date( - metadata?.acquisition_source?.datetime + metadata?.getIn(['acquisition_source', 'datetime']) ).toLocaleDateString()}

-

{metadata?.acquisition_source?.source}

-

{metadata?.acquisition_source?.email}

+

+ {metadata?.getIn(['acquisition_source', 'source'])} +

+

+ {metadata?.getIn(['acquisition_source', 'email'])} +

- {metadata?.arxiv_categories?.map((category: string) => ( + {metadata?.get('arxiv_categories')?.map((category: string) => (
{category}
diff --git a/ui/src/holdingpen-new/components/Breadcrumbs.tsx b/ui/src/holdingpen-new/components/Breadcrumbs.tsx index 3d7160cd4..755d4a204 100644 --- a/ui/src/holdingpen-new/components/Breadcrumbs.tsx +++ b/ui/src/holdingpen-new/components/Breadcrumbs.tsx @@ -1,11 +1,15 @@ import React from 'react'; import { Breadcrumb, Input } from 'antd'; import { HomeOutlined } from '@ant-design/icons'; +import { push } from 'connected-react-router'; +import { Action, ActionCreator } from 'redux'; +import { connect } from 'react-redux'; import './Breadcrumbs.less'; import { HOLDINGPEN_NEW, HOLDINGPEN_SEARCH_NEW } from '../../common/routes'; interface BreadcrumbItemProps { + dispatch: ActionCreator; title1: string; href1: string; title2?: string; @@ -14,6 +18,7 @@ interface BreadcrumbItemProps { } const Breadcrumbs: React.FC = ({ + dispatch, title1, href1, title2, @@ -49,10 +54,10 @@ const Breadcrumbs: React.FC = ({ enterButton placeholder="Search Holdingpen" onPressEnter={() => { - window.location.assign(HOLDINGPEN_SEARCH_NEW); + dispatch(push(HOLDINGPEN_SEARCH_NEW)); }} onSearch={() => { - window.location.assign(HOLDINGPEN_SEARCH_NEW); + dispatch(push(HOLDINGPEN_SEARCH_NEW)); }} className="search-bar-small" /> @@ -61,4 +66,8 @@ const Breadcrumbs: React.FC = ({ ); }; -export default Breadcrumbs; +const dispatchToProps = (dispatch: ActionCreator) => ({ + dispatch, +}); + +export default connect(null, dispatchToProps)(Breadcrumbs); diff --git a/ui/src/holdingpen-new/components/LoginPage.tsx b/ui/src/holdingpen-new/components/LoginPage.tsx new file mode 100644 index 000000000..87969e003 --- /dev/null +++ b/ui/src/holdingpen-new/components/LoginPage.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { Row, Card, Button, Input } from 'antd'; +import { Field, Form, Formik } from 'formik'; +import DocumentHead from '../../common/components/DocumentHead'; +import { Credentials } from '../../types'; + +const LocalLoginPage = ({ + onLoginFormSubmit, +}: { + onLoginFormSubmit: ((values: {}) => void | Promise) & Function; +}) => { + function renderFormInput({ + form, + field, + ...rest + }: { + form: any; + field: JSX.Element; + rest: any; + }) { + return ; + } + + function renderLoginForm() { + return ( +
+ + + + + + + +
+ ); + } + + return ( + <> + + + +

+ Please log in with your Backoffice account +

+ onLoginFormSubmit(creds)} + initialValues={{ email: null, password: null }} + > + {renderLoginForm} + +
+
+ + ); +}; + +export default LocalLoginPage; diff --git a/ui/src/holdingpen-new/containers/DashboardPageContainer/DashboardPageContainer.tsx b/ui/src/holdingpen-new/containers/DashboardPageContainer/DashboardPageContainer.tsx index 09724f019..01d7bff40 100644 --- a/ui/src/holdingpen-new/containers/DashboardPageContainer/DashboardPageContainer.tsx +++ b/ui/src/holdingpen-new/containers/DashboardPageContainer/DashboardPageContainer.tsx @@ -10,6 +10,9 @@ import { } from '@ant-design/icons'; import { Card, Input, Select, Tabs } from 'antd'; import { Link } from 'react-router-dom'; +import { push } from 'connected-react-router'; +import { Action, ActionCreator } from 'redux'; +import { connect } from 'react-redux'; import './DashboardPageContainer.less'; import { tasks, actions } from './mockData'; @@ -17,7 +20,7 @@ import Breadcrumbs from '../../components/Breadcrumbs'; import { HOLDINGPEN_SEARCH_NEW } from '../../../common/routes'; interface DashboardPageContainerProps { - data?: any; + dispatch: ActionCreator; } const TEXT_CENTER: Record = { @@ -68,7 +71,9 @@ const getIcon = (action: string) => { } }; -const DashboardPage: React.FC = () => { +const DashboardPageContainer: React.FC = ({ + dispatch, +}) => { const tabItems = [ { label:

Tasks

, @@ -181,10 +186,10 @@ const DashboardPage: React.FC = () => { addonBefore={selectBefore} enterButton onPressEnter={() => { - window.location.assign(HOLDINGPEN_SEARCH_NEW); + dispatch(push(HOLDINGPEN_SEARCH_NEW)); }} onSearch={() => { - window.location.assign(HOLDINGPEN_SEARCH_NEW); + dispatch(push(HOLDINGPEN_SEARCH_NEW)); }} />
@@ -195,4 +200,4 @@ const DashboardPage: React.FC = () => { ); }; -export default DashboardPage; +export default connect(null)(DashboardPageContainer); diff --git a/ui/src/holdingpen-new/containers/DetailPageContainer/AuthorDetailPageContainer.tsx b/ui/src/holdingpen-new/containers/DetailPageContainer/AuthorDetailPageContainer.tsx index 9f2335209..f29b243be 100644 --- a/ui/src/holdingpen-new/containers/DetailPageContainer/AuthorDetailPageContainer.tsx +++ b/ui/src/holdingpen-new/containers/DetailPageContainer/AuthorDetailPageContainer.tsx @@ -1,5 +1,6 @@ +/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable no-underscore-dangle */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { Row, Col, Button, Table } from 'antd'; import { @@ -12,16 +13,21 @@ import { PlayCircleOutlined, CloseOutlined, } from '@ant-design/icons'; +import { Action, ActionCreator } from 'redux'; +import { connect, RootStateOrAny } from 'react-redux'; +import { Map } from 'immutable'; import './DetailPageContainer.less'; import Breadcrumbs from '../../components/Breadcrumbs'; import ContentBox from '../../../common/components/ContentBox'; import CollapsableForm from '../../../submissions/common/components/CollapsableForm'; import LoadingOrChildren from '../../../common/components/LoadingOrChildren'; -import { getSearchResults } from '../../utils/utils'; +import { fetchAuthor } from '../../../actions/holdingpen'; interface AuthorDetailPageContainerProps { - item: any; + dispatch: ActionCreator; + author: Map; + loading: boolean; } const columnsInstitutions = [ @@ -115,30 +121,28 @@ const columnsAdvisors = [ }, ]; -const AuthorDetailPageContainer: React.FC< - AuthorDetailPageContainerProps -> = () => { - const [result, setResult] = useState({}); +const AuthorDetailPageContainer: React.FC = ({ + dispatch, + author, + loading, +}) => { const { id } = useParams<{ id: string }>(); useEffect(() => { - (async () => { - setResult(await getSearchResults(id)); - })(); - }, [id]); + dispatch(fetchAuthor(id)); + }, []); - const { - _workflow: workflow, - metadata, - _extra_data: extraData, - } = result?.data || {}; + const workflow = author?.getIn(['data', '_workflow']) as Map; + const metadata = author?.getIn(['data', 'metadata']) as Map; + const extraData = author?.getIn(['data', '_extra_data']) as Map; const OPEN_SECTIONS = [ - metadata?.positions && 'institutions', - metadata?.project_membership && 'projects', - metadata?.urls && 'links', - (metadata?.arxiv_categories || metadata?.advisors) && 'other', - extraData?._error_msg && 'errors', + metadata?.get('positions') && 'institutions', + metadata?.get('project_membership') && 'projects', + metadata?.get('urls') && 'links', + (metadata?.get('arxiv_categories') || metadata?.get('.advisors')) && + 'other', + extraData?.get('_error_msg') && 'errors', 'delete', ].filter(Boolean); @@ -150,23 +154,26 @@ const AuthorDetailPageContainer: React.FC< - + - {workflow?.status && ( + {workflow?.get('status') && (

- {workflow?.status} - {workflow?.status !== 'COMPLETED' + {workflow?.get('status')} + {workflow?.get('status') !== 'COMPLETED' ? ` on: "${ - extraData?._message || extraData?._last_task_name + extraData?.get('_message') || + extraData?.get('_last_task_name') }"` : ''}

@@ -177,26 +184,29 @@ const AuthorDetailPageContainer: React.FC< -

{metadata?.name?.value}

- {metadata?.name?.preferred_name && ( +

{metadata?.getIn(['name', 'value'])}

+ {metadata?.getIn(['name', 'preferred_name']) && (

- Preferred name: {metadata?.name?.preferred_name} + Preferred name:{' '} + {metadata?.getIn(['name', 'preferred_name'])}

)} - {metadata?.status && ( + {metadata?.get('status') && (

- Status: {metadata?.status} + Status: {metadata?.get('status')}

)} - {metadata?.acquisition_source?.orcid && ( + {metadata?.getIn(['acquisition_source', 'orcid']) && (

ORCID:{' '} - {metadata?.acquisition_source?.orcid} + {metadata?.getIn(['acquisition_source', 'orcid'])}

)} @@ -208,7 +218,7 @@ const AuthorDetailPageContainer: React.FC< > @@ -222,27 +232,25 @@ const AuthorDetailPageContainer: React.FC< >
`${record?.name}+${Math.random()}`} /> - {metadata?.urls && ( + {metadata?.get('urls') && ( - {metadata?.urls?.map( - (link: { value: string; description: string }) => ( -

- - {link?.description && ( - - {link?.description}: - - )}{' '} - {link?.value} -

- ) - )} + {metadata?.get('urls')?.map((link: Map) => ( +

+ + {link?.get('description') && ( + + {link?.get('description')}: + + )}{' '} + {link?.get('value')} +

+ ))}
)} @@ -251,9 +259,10 @@ const AuthorDetailPageContainer: React.FC<

Subject areas

({ term }) - )} + dataSource={metadata + ?.get('arxiv_categories') + ?.toJS() + ?.map((term: string) => ({ term }))} pagination={false} size="small" rowKey={(record) => @@ -265,7 +274,7 @@ const AuthorDetailPageContainer: React.FC<

Advisors

@@ -275,10 +284,10 @@ const AuthorDetailPageContainer: React.FC< - {extraData?._error_msg && ( + {extraData?.get('_error_msg') && (
- {extraData?._error_msg} + {extraData?.get('_error_msg')}
)} @@ -313,16 +322,20 @@ const AuthorDetailPageContainer: React.FC< fullHeight={false} subTitle="Submission" > - Submitted by {metadata?.acquisition_source?.email} on{' '} + Submitted by{' '} + {metadata?.getIn(['acquisition_source', 'email'])} on{' '} {new Date( - metadata?.acquisition_source?.datetime + metadata?.getIn([ + 'acquisition_source', + 'datetime', + ]) as Date ).toLocaleDateString()} . {/* TODO: find out how notes are stored in workflow */} - {metadata?.notes && ( + {metadata?.get('notes') && ( - {extraData?.ticket_id && ( + {extraData?.get('ticket_id') && ( - See related ticket #{extraData?.ticket_id} + See related ticket #{extraData?.get('ticket_id')} )} @@ -358,7 +371,7 @@ const AuthorDetailPageContainer: React.FC< - - ); - } - - return ( - <> - - - -

- Please log in with your Backoffice account -

- onLoginFormSubmit(creds)} - initialValues={{ username: null, password: null }} - > - {renderLoginForm} - -
-
- - ); -}; - -export default LocalLoginPage; +export default connect(null, dispatchToProps)(LoginPage); diff --git a/ui/src/holdingpen-new/containers/SearchPageContainer/SearchPageContainer.tsx b/ui/src/holdingpen-new/containers/SearchPageContainer/SearchPageContainer.tsx index 78cb86865..059ec24d4 100644 --- a/ui/src/holdingpen-new/containers/SearchPageContainer/SearchPageContainer.tsx +++ b/ui/src/holdingpen-new/containers/SearchPageContainer/SearchPageContainer.tsx @@ -1,7 +1,9 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { Row, Col, Card, Checkbox, Select } from 'antd'; -import { List } from 'immutable'; +import { connect, RootStateOrAny } from 'react-redux'; +import { Action, ActionCreator } from 'redux'; +import { List, Map } from 'immutable'; import './SearchPageContainer.less'; import { facets } from '../../mocks/mockSearchData'; @@ -14,39 +16,42 @@ import SearchPagination from '../../../common/components/SearchPagination'; import PublicationsSelectAllContainer from '../../../authors/containers/PublicationsSelectAllContainer'; import UnclickableTag from '../../../common/components/UnclickableTag'; import AuthorResultItem from '../../components/AuthorResultItem'; -import { getSearchResults, refreshToken } from '../../utils/utils'; +import { + fetchSearchResults, + searchQueryUpdate, +} from '../../../actions/holdingpen'; +import EmptyOrChildren from '../../../common/components/EmptyOrChildren'; interface SearchPageContainerProps { - data?: any; + dispatch: ActionCreator; + results: List; + loading: boolean; + totalResults: number; + query: Map; } -const renderResultItem = (item: any) => { - return ; +const renderResultItem = (item: Map) => { + return ( + + ); }; -const SearchPageContainer: React.FC = () => { - const [loading, setLoading] = useState(true); - const [searchResults, setSearchResults] = useState([]); - const [count, setCount] = useState(0); - const [page, setPage] = useState(1); - const [size, setSize] = useState(10); - - const resolveLoading = () => { - setTimeout(() => setLoading(false), 2500); - }; - - resolveLoading(); - +const SearchPageContainer: React.FC = ({ + dispatch, + results, + loading, + totalResults, + query, +}) => { useEffect(() => { - (async () => { - const data = await getSearchResults({ page: 1, size: 10 }); - const filteredData = data?.results?.filter( - (result: any) => result?.data?.id - ); - setSearchResults(filteredData || []); - setCount(data?.count || 0); - })(); - }, [page, size]); + dispatch( + fetchSearchResults(query.toJS() as { page: number; size: number }) + ); + }, [query]); return (
= () => { > -
- - -

Results per page

- +

Sort by

+ +
+ + dispatch(searchQueryUpdate({ page, size })) + } + onSizeChange={(_page, size) => + dispatch(searchQueryUpdate({ page: 1, size })) + } + page={query?.get('page') || 1} + total={totalResults} + pageSize={query?.get('size') || 10} /> - {facets.map( - (facet: { - category: string; - filters: { name: string; doc_count: number }[]; - }) => ( -
- -

- Filter by {facet.category} -

-
- {facet.filters.map((filter) => ( - -
- - {filter.name} - - - - {filter.doc_count} - - - ))} - - ) - )} - - - - - - - - - - - - Action & Status - - - Submission Info - - Subject Areas - - - -
-
- - + + ); }; -export default SearchPageContainer; +const stateToProps = (state: RootStateOrAny) => ({ + results: state.holdingpen.get('searchResults'), + loading: state.holdingpen.get('loading'), + totalResults: state.holdingpen.get('totalResults'), + query: state.holdingpen.get('query'), +}); + +export default connect(stateToProps)(SearchPageContainer); diff --git a/ui/src/holdingpen-new/containers/__tests__/DetailPageContainer.test.tsx b/ui/src/holdingpen-new/containers/__tests__/DetailPageContainer.test.tsx index 7c65969b0..165f73380 100644 --- a/ui/src/holdingpen-new/containers/__tests__/DetailPageContainer.test.tsx +++ b/ui/src/holdingpen-new/containers/__tests__/DetailPageContainer.test.tsx @@ -1,15 +1,32 @@ import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { Provider } from 'react-redux'; import { render } from '@testing-library/react'; +import { getStore } from '../../../fixtures/store'; + import DetailPageContainer from '../DetailPageContainer/DetailPageContainer'; +import { HOLDINGPEN_NEW } from '../../../common/routes'; describe('DetailPageContainer', () => { it('renders without crashing', () => { - render(); + render( + + + {' '} + + + ); }); it('renders the DetailPage component', () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + + {' '} + + + ); const detailPage = getByTestId('holdingpen-detail-page'); expect(detailPage).toBeInTheDocument(); }); diff --git a/ui/src/holdingpen-new/index.tsx b/ui/src/holdingpen-new/index.tsx index 8094e49a7..1a56537ba 100644 --- a/ui/src/holdingpen-new/index.tsx +++ b/ui/src/holdingpen-new/index.tsx @@ -1,12 +1,11 @@ import React from 'react'; -import { Redirect } from 'react-router-dom'; +import { Route } from 'react-router-dom'; +import { connect, RootStateOrAny } from 'react-redux'; -import DashboardPage from './containers/DashboardPageContainer/DashboardPageContainer'; -import DetailPageContainer from './containers/DetailPageContainer/DetailPageContainer'; +import DashboardPageContainer from './containers/DashboardPageContainer/DashboardPageContainer'; import SearchPageContainer from './containers/SearchPageContainer/SearchPageContainer'; import { HOLDINGPEN_NEW, - HOLDINGPEN_DASHBOARD_NEW, HOLDINGPEN_SEARCH_NEW, HOLDINGPEN_LOGIN_NEW, } from '../common/routes'; @@ -14,53 +13,39 @@ import SafeSwitch from '../common/components/SafeSwitch'; import AuthorDetailPageContainer from './containers/DetailPageContainer/AuthorDetailPageContainer'; import DocumentHead from '../common/components/DocumentHead'; import LoginPageContainer from './containers/LoginPageContainer/LoginPageContainer'; -import storage from '../common/storage'; import PrivateRoute from '../common/PrivateRoute'; const META_DESCRIPTION = 'Tool for curators to manage submissions and harvests'; const TITLE = 'Holdingpen'; const Holdingpen = () => { - const loggedIn = !!storage.getSync('holdingpen.token'); - return ( <>
- - -
@@ -68,4 +53,8 @@ const Holdingpen = () => { ); }; -export default Holdingpen; +const stateToProps = (state: RootStateOrAny) => ({ + loggedIn: state.holdingpen.get('loggedIn'), +}); + +export default connect(stateToProps)(Holdingpen); diff --git a/ui/src/holdingpen-new/notifications.ts b/ui/src/holdingpen-new/notifications.ts new file mode 100644 index 000000000..6d5fe6be7 --- /dev/null +++ b/ui/src/holdingpen-new/notifications.ts @@ -0,0 +1,10 @@ +import React from 'react'; +import { notification } from 'antd'; + +export function notifyLoginError(error: string) { + notification.error({ + message: 'Login unsuccessful', + description: error, + duration: 2, + }); +} diff --git a/ui/src/holdingpen-new/utils/utils.ts b/ui/src/holdingpen-new/utils/utils.ts index 1426b485e..6e37c5db8 100644 --- a/ui/src/holdingpen-new/utils/utils.ts +++ b/ui/src/holdingpen-new/utils/utils.ts @@ -1,10 +1,5 @@ -import { - BACKOFFICE_LOGIN, - HOLDINGPEN_LOGIN_NEW, - BACKOFFICE_SEARCH_API, - BACKOFFICE_API, -} from '../../common/routes'; import storage from '../../common/storage'; +import { BACKOFFICE_LOGIN, HOLDINGPEN_LOGIN_NEW } from '../../common/routes'; export const refreshToken = async () => { try { @@ -31,36 +26,3 @@ export const refreshToken = async () => { return null; }; - -export const getSearchResults = async ( - query: { page: number; size: number } | string -) => { - const token = storage.getSync('holdingpen.token'); - - const resolveQuery = - typeof query !== 'string' - ? `${BACKOFFICE_SEARCH_API}?page=${query.page}&size=${query.size}` - : `${BACKOFFICE_API}/${query}`; - - const fetchResults = async (token: string): Promise => { - const res = await fetch(`${resolveQuery}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - if (res.status === 403) { - // eslint-disable-next-line no-param-reassign - token = await refreshToken(); - if (token) { - return fetchResults(token); - } - return { results: [], count: 0 }; - } - - const data = await res.json(); - return data || { results: [], count: 0 }; - }; - - return fetchResults(token); -}; diff --git a/ui/src/literature/__tests__/citeArticle.test.js b/ui/src/literature/__tests__/citeArticle.test.js index c57390a92..fd4f1c176 100644 --- a/ui/src/literature/__tests__/citeArticle.test.js +++ b/ui/src/literature/__tests__/citeArticle.test.js @@ -13,9 +13,7 @@ describe('citeArticle', () => { it('sends request with Accept header based on format and returns text', async () => { const citeUrl = '/literature/12345'; const format = 'application/x-test'; - mockHttp - .onGet(citeUrl, null, { Accept: 'application/x-test' }) - .replyOnce(200, 'Test'); + mockHttp.onGet(citeUrl).replyOnce(200, 'Test'); const content = await citeArticle(format, 12345); expect(content).toEqual('Test'); }); @@ -23,9 +21,7 @@ describe('citeArticle', () => { it('returns a status code that is not 2xx without data', async () => { const citeUrl = '/literature/12345'; const format = 'application/x-test'; - mockHttp - .onGet(citeUrl, null, { Accept: 'application/x-test' }) - .replyOnce(500); + mockHttp.onGet(citeUrl).replyOnce(500); await expect(citeArticle(format, 12345)).rejects.toThrow( new Error('Request failed with status code 500') ); @@ -34,9 +30,7 @@ describe('citeArticle', () => { it('returns a network error', async () => { const citeUrl = '/literature/12345'; const format = 'application/x-test'; - mockHttp - .onGet(citeUrl, null, { Accept: 'application/x-test' }) - .networkError(); + mockHttp.onGet(citeUrl).networkError(); await expect(citeArticle(format, 12345)).rejects.toThrow( new Error('Network Error') ); diff --git a/ui/src/literature/components/__tests__/CiteAllAction.test.jsx b/ui/src/literature/components/__tests__/CiteAllAction.test.jsx index 7d49cdb58..066db4d72 100644 --- a/ui/src/literature/components/__tests__/CiteAllAction.test.jsx +++ b/ui/src/literature/components/__tests__/CiteAllAction.test.jsx @@ -46,11 +46,7 @@ describe('CiteAllAction', () => { it('calls downloadTextAsFile with correct data when option is clicked', async () => { mockHttp .onGet( - `/literature?sort=mostcited&q=query&page=1&size=${MAX_CITEABLE_RECORDS}`, - null, - { - Accept: 'application/vnd+inspire.latex.eu+x-latex', - } + `/literature?sort=mostcited&q=query&page=1&size=${MAX_CITEABLE_RECORDS}` ) .replyOnce(200, 'Test'); const wrapper = shallow( @@ -72,11 +68,7 @@ describe('CiteAllAction', () => { it('calls downloadTextAsFile with correct data omitting page and size when option is clicked', async () => { mockHttp .onGet( - `/literature?sort=mostrecent&q=query&page=1&size=${MAX_CITEABLE_RECORDS}`, - null, - { - Accept: 'application/vnd+inspire.latex.eu+x-latex', - } + `/literature?sort=mostrecent&q=query&page=1&size=${MAX_CITEABLE_RECORDS}` ) .replyOnce(200, 'Test'); const wrapper = shallow( diff --git a/ui/src/reducers/holdingpen.js b/ui/src/reducers/holdingpen.js new file mode 100644 index 000000000..663f5a863 --- /dev/null +++ b/ui/src/reducers/holdingpen.js @@ -0,0 +1,64 @@ +import { fromJS } from 'immutable'; + +import { + HOLDINGPEN_LOGIN_ERROR, + HOLDINGPEN_LOGIN_SUCCESS, + HOLDINGPEN_LOGOUT_SUCCESS, + HOLDINGPEN_SEARCH_REQUEST, + HOLDINGPEN_SEARCH_ERROR, + HOLDINGPEN_SEARCH_SUCCESS, + HOLDINGPEN_AUTHOR_REQUEST, + HOLDINGPEN_AUTHOR_ERROR, + HOLDINGPEN_AUTHOR_SUCCESS, + HOLDINGPEN_SEARCH_QUERY_UPDATE, + HOLDINGPEN_SEARCH_QUERY_RESET, +} from '../actions/actionTypes'; + +export const initialState = fromJS({ + loggedIn: false, + query: { page: 1, size: 10 }, + searchResults: [], + totalResults: 0, + loading: false, + author: [], +}); + +const HOLDINGPENReducer = (state = initialState, action) => { + switch (action.type) { + case HOLDINGPEN_LOGIN_ERROR: + case HOLDINGPEN_LOGOUT_SUCCESS: + return initialState; + case HOLDINGPEN_LOGIN_SUCCESS: + return state.set('loggedIn', true); + case HOLDINGPEN_SEARCH_REQUEST: + return state.set('loading', true); + case HOLDINGPEN_SEARCH_ERROR: + return state + .set('loading', false) + .set('searchResults', initialState.get('searchResults')) + .set('totalResults', initialState.get('totalResults')); + case HOLDINGPEN_SEARCH_SUCCESS: + return state + .set('loading', false) + .set('searchResults', fromJS(action.payload.data.results)) + .set('totalResults', action.payload.data.count); + case HOLDINGPEN_AUTHOR_REQUEST: + return state.set('loading', true); + case HOLDINGPEN_AUTHOR_ERROR: + return state + .set('loading', false) + .set('author', initialState.get('author')); + case HOLDINGPEN_AUTHOR_SUCCESS: + return state + .set('loading', false) + .set('author', fromJS(action.payload.data)); + case HOLDINGPEN_SEARCH_QUERY_UPDATE: + return state.set('query', fromJS(action.payload)); + case HOLDINGPEN_SEARCH_QUERY_RESET: + return state.set('query', fromJS({ page: 1, size: 10 })); + default: + return state; + } +}; + +export default HOLDINGPENReducer; diff --git a/ui/src/reducers/index.js b/ui/src/reducers/index.js index 31ca92e64..9833e31cd 100644 --- a/ui/src/reducers/index.js +++ b/ui/src/reducers/index.js @@ -18,6 +18,9 @@ import journals from './journals'; import bibliographyGenerator from './bibliographyGenerator'; import settings from './settings'; import ui, { initialState as uiInitialState } from './ui'; +import holdingpen, { + initialState as holdingpenInitialState, +} from './holdingpen'; import { LITERATURE_NS, LITERATURE_REFERENCES_NS } from '../search/constants'; export default function createRootReducer(history) { @@ -39,7 +42,8 @@ export default function createRootReducer(history) { seminars, experiments, bibliographyGenerator, - journals + journals, + holdingpen, }); } @@ -56,4 +60,5 @@ export const REDUCERS_TO_PERSISTS = [ initialState: searchInitialState, statePath: ['namespaces', LITERATURE_NS, 'query', 'size'], }, + { name: 'holdingpen', initialState: holdingpenInitialState }, ]; diff --git a/ui/src/types/index.ts b/ui/src/types/index.ts index 8e4a3b988..6b552f988 100644 --- a/ui/src/types/index.ts +++ b/ui/src/types/index.ts @@ -35,6 +35,6 @@ export interface User { } export interface Credentials { - email?: string; - password?: string; + email?: string | null; + password?: string | null; } diff --git a/ui/yarn.lock b/ui/yarn.lock index 70b7d862b..b15cfde25 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -3523,22 +3523,14 @@ axios-hooks@^1.9.0: "@babel/runtime" "7.10.2" lru-cache "5.1.1" -axios-mock-adapter@^1.15.0: - version "1.21.2" - resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.21.2.tgz#87a48f80aa89bb1ab1ad630fa467975e30aa4721" - integrity sha512-jzyNxU3JzB2XVhplZboUcF0YDs7xuExzoRSHXPHr+UQajaGmcTqvkkUADgkVI2WkGlpZ1zZlMVdcTMU0ejV8zQ== +axios-mock-adapter@^1.22.0: + version "1.22.0" + resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.22.0.tgz#0f3e6be0fc9b55baab06f2d49c0b71157e7c053d" + integrity sha512-dmI0KbkyAhntUR05YY96qg2H6gg0XMl2+qTW0xmYg6Up+BFBAJYRLROMXRdDEL06/Wqwa0TJThAYvFtSFdRCZw== dependencies: fast-deep-equal "^3.1.3" is-buffer "^2.0.5" -axios@^0.18.0: - version "0.18.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.1.tgz#ff3f0de2e7b5d180e757ad98000f1081b87bcea3" - integrity sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g== - dependencies: - follow-redirects "1.5.10" - is-buffer "^2.0.2" - axios@^0.27.2: version "0.27.2" resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" @@ -3547,6 +3539,15 @@ axios@^0.27.2: follow-redirects "^1.14.9" form-data "^4.0.0" +axios@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" + integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1" @@ -5296,13 +5297,6 @@ debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4: dependencies: ms "2.1.2" -debug@=3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - debug@^3.2.6, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -6822,18 +6816,16 @@ fn-name@~2.0.1: resolved "https://registry.yarnpkg.com/fn-name/-/fn-name-2.0.1.tgz#5214d7537a4d06a4a301c0cc262feb84188002e7" integrity sha512-oIDB1rXf3BUnn00bh2jVM0byuqr94rBh6g7ZfdKcbmp1we2GQtPzKdloyvBXHs+q3fvxB8EqX5ecFba3RwCSjA== -follow-redirects@1.5.10: - version "1.5.10" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" - integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== - dependencies: - debug "=3.1.0" - follow-redirects@^1.0.0, follow-redirects@^1.14.9: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -7921,7 +7913,7 @@ is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-buffer@^2.0.2, is-buffer@^2.0.5: +is-buffer@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== @@ -11682,6 +11674,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"