Skip to content

Commit

Permalink
ui: set up and use redux in new holdingpen
Browse files Browse the repository at this point in the history
  • Loading branch information
karolina-siemieniuk-morawska committed Jul 31, 2024
1 parent 8a8a92c commit 0b9681c
Show file tree
Hide file tree
Showing 31 changed files with 835 additions and 486 deletions.
4 changes: 2 additions & 2 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions ui/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ function App({ userRoles, dispatch, guideModalVisibility }) {
path={HOLDINGPEN_NEW}
component={HoldingpenNew$}
authorizedRoles={SUPERUSER_OR_CATALOGER}
holdingpen
/>
<Route path={LITERATURE} component={Literature} />
<Route path={AUTHORS} component={Authors} />
Expand Down
17 changes: 15 additions & 2 deletions ui/src/actions/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';

Expand All @@ -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';
230 changes: 230 additions & 0 deletions ui/src/actions/holdingpen.ts
Original file line number Diff line number Diff line change
@@ -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<Action>) => Promise<void> {
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<Action>
) => Promise<void> {
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<Action>) => Promise<void> {
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<Action>) => Promise<void> {
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<Action>) => Promise<void> {
return async (dispatch) => {
dispatch(updateQuery(query));
};
}
20 changes: 9 additions & 11 deletions ui/src/common/PrivateRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,11 @@ interface PrivateRouteProps extends ComponentPropsWithoutRef<any> {
userRoles: List<string>;
authorizedRoles: List<string>;
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,
Expand All @@ -35,26 +31,28 @@ function PrivateRoute({
);
}

const resolveLoggedIn = props.isHoldinpen
? props.loggedInToHoldinpen && props.loggedIn
const resolveLoggedIn = props.holdingpen
? props.loggedInToHoldingpen && props.loggedIn
: props.loggedIn;

return (
<RouteOrRedirect
redirectTo={props.isHoldingpen ? HOLDINGPEN_LOGIN_NEW : USER_LOGIN}
condition={resolveLoggedIn}
redirectTo={props.holdingpen ? HOLDINGPEN_LOGIN_NEW : USER_LOGIN}
condition={resolveLoggedIn || false}
component={props.component}
{...props}
/>
);
}

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']),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ exports[`SearchPagination renders with all props set 1`] = `
pageSize={10}
pageSizeOptions={
Array [
"10",
"25",
"50",
"100",
Expand All @@ -35,6 +36,7 @@ exports[`SearchPagination renders with only required props set 1`] = `
pageSize={25}
pageSizeOptions={
Array [
"10",
"25",
"50",
"100",
Expand Down
2 changes: 1 addition & 1 deletion ui/src/common/components/SearchResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const SearchResults = ({
);
}
return (
<div className="mv2" key={result?.data?.id}>
<div className="mv2" key={result?.get('id')}>
{renderItem(result)}
</div>
);
Expand Down
Loading

0 comments on commit 0b9681c

Please sign in to comment.