Commit 695fd5ce authored by Mattias Lundmark's avatar Mattias Lundmark

Merge branch 'DEICH-1769' into 'master'

Deich 1769 (Favourites)

See merge request !2
parents b994384e 9f6c8f46
Pipeline #1652 passed with stages
in 2 minutes and 47 seconds
This diff is collapsed.
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18.2 18.2"><defs><style>.cls-1{fill:#ecd399;}</style></defs><title>Ikoner</title><circle cx="9.1" cy="9.1" r="9.1"/><path class="cls-1" d="M12.06,13.7a.508.508,0,0,1-.359-.149l-2.6-2.6-2.6,2.6a.507.507,0,0,1-.866-.359V5.318a.507.507,0,0,1,.507-.507h5.92a.507.507,0,0,1,.507.507v7.874a.508.508,0,0,1-.507.508ZM9.1,9.726a.506.506,0,0,1,.359.148l2.094,2.094V5.825H6.647v6.143L8.741,9.874A.506.506,0,0,1,9.1,9.726Z"/></svg>
\ No newline at end of file
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18.2 18.2"><defs><style>.cls-1{fill:#ecd399;}</style></defs><title>Ikoner</title><circle cx="9.1" cy="9.1" r="9.1"/><path class="cls-1" d="M12.06,13.7a.508.508,0,0,1-.359-.149l-2.6-2.6-2.6,2.6a.507.507,0,0,1-.866-.359V5.318a.507.507,0,0,1,.507-.507h5.92a.507.507,0,0,1,.507.507v7.874a.508.508,0,0,1-.507.508Z"/></svg>
\ No newline at end of file
......@@ -32,6 +32,7 @@ module.exports = (app) => {
// We renewed the session; retry original HTTP call (once)
return isofetch(url, opts)
} else {
console.log(res)
throw new Error("Cannot obtain Koha API session")
}
})
......
......@@ -36,23 +36,23 @@ module.exports = (app) => {
}
})
app.post("/api/v1/profile/history", jsonParser, (request, response) => {
app.post("/api/v1/profile/history", jsonParser, async (request, response) => {
const params = {
offset: request.body.offset || 0,
limit: request.body.limit || 20
}
fetch(`http://koha:8081/api/v1/patrons/${request.session.borrowerNumber}/history?${querystring.stringify(params)}`)
.then(res => {
if (res.status === 200) {
return res.json()
} else {
throw Error(res.statusText)
}
}).then(json => response.status(200).send(json))
.catch(error => {
console.log(error)
response.sendStatus(500)
})
try {
const res = await fetch(`http://koha:8081/api/v1/patrons/${request.session.borrowerNumber}/history?${querystring.stringify(params)}`)
if (res.status === 200) {
const json = await res.json()
response.status(200).send(json)
} else {
throw Error(res.statusText)
}
} catch (error) {
console.log(error)
response.sendStatus(500)
}
})
app.delete("/api/v1/profile/history", jsonParser, (request, response) => {
......@@ -162,21 +162,92 @@ module.exports = (app) => {
})
})
app.get("/api/v1/profile/loans", (request, response) => {
fetch(`http://koha:8081/api/v1/patrons/${request.session.borrowerNumber}/loansandreservations`)
.then(res => {
if (res.status === 200) {
return res.json()
} else {
throw Error(res.statusText)
}
}).then(json => {
app.get("/api/v1/profile/loans", async (request, response) => {
try {
const res = await fetch(`http://koha:8081/api/v1/patrons/${request.session.borrowerNumber}/loansandreservations`)
if (res.status === 200) {
const json = await res.json()
response.send(json)
} else {
throw Error(res.statusText)
}
} catch (error) {
console.log(error)
response.sendStatus(500)
}
})
app.get("/api/v1/profile/favourites", async (request, response) => {
try {
const res = await fetch(`http://xkoha:8081/api/v1/patrons/${request.session.borrowerNumber}/favourites`)
if (res.status === 200) {
const json = await res.json()
response.send(json)
} else {
throw Error(response.statusText)
}
} catch (error) {
console.log(error)
response.sendStatus(500)
}
})
app.delete("/api/v1/profile/favourites", jsonParser, async (request, response) => {
try {
const favourite = {
id: request.body.id
}
const res = await fetch(`http://xkoha:8081/api/v1/patrons/${request.session.borrowerNumber}/favourites`, {
method: "DELETE",
headers: {
"Content-type": "application/json"
},
body: JSON.stringify(favourite)
})
.catch(error => {
console.log(error)
response.sendStatus(500)
response.sendStatus(res.status)
} catch (error) {
console.log(error)
response.sendStatus(500)
}
})
app.delete("/api/v1/profile/favourites/tag", jsonParser, async (request, response) => {
try {
const favourite = {
tag: request.body.tag
}
const res = await fetch(`http://xkoha:8081/api/v1/patrons/${request.session.borrowerNumber}/favourites`, {
method: "DELETE",
headers: {
"Content-type": "application/json"
},
body: JSON.stringify(favourite)
})
response.sendStatus(res.status)
} catch (error) {
console.log(error)
response.sendStatus(500)
}
})
app.post("/api/v1/profile/favourites", jsonParser, async (request, response) => {
try {
const favourite = {
biblionumber: request.body.biblionumber,
tag: request.body.tag
}
const res = await fetch(`http://xkoha:8081/api/v1/patrons/${request.session.borrowerNumber}/favourites`, {
method: "POST",
headers: {
"Content-type": "application/json"
},
body: JSON.stringify(favourite)
})
response.sendStatus(res.status)
} catch (error) {
console.log(error)
response.sendStatus(500)
}
})
app.post("/api/v1/profile/settings", jsonParser, (request, response) => {
......
import fetch from "isomorphic-fetch"
import * as types from "../constants/ActionTypes"
import { requireLoginBeforeAction } from "./LoginActions"
import Errors from "../constants/Errors"
import { showModal, hideModal } from "./ModalActions"
import { action } from "./GenericActions"
import ModalComponents from "../constants/ModalComponents"
import * as ProfileActions from "./ProfileActions"
import { actions as toastrActions } from "react-redux-toastr"
export const receiveFavourites = data => action(types.RECEIVE_FAVOURITES, { favourites: data })
export function savePublicationToFavourites (tag, biblioNumber) {
return async (dispatch, getState) => {
try {
const url = "/api/v1/profile/favourites"
const favouritesResponse = await fetch(url, {
method: "POST",
credentials: "same-origin",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ biblionumber: biblioNumber, tag: tag })
})
if (favouritesResponse.status === 201) {
dispatch(savePublicationToFavouritesSuccess())
dispatch(ProfileActions.fetchProfileLoans())
dispatch(hideModal())
dispatch(toastrActions.add({
type: "success",
title: "Lagt til i",
message: tag,
options: {}
}))
dispatch(fetchFavourites())
} else {
dispatch(savePublicationToFavouritesFailure(favouritesResponse.status))
}
} catch (error) {
dispatch(savePublicationToFavouritesFailure(error))
}
}
}
export function deletePublicationFromFavourites (id) {
return async (dispatch, getState) => {
try {
const url = "/api/v1/profile/favourites"
const favouritesResponse = await fetch(url, {
method: "DELETE",
credentials: "same-origin",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ id: id })
})
if (favouritesResponse.status === 200) {
dispatch(deletePublicationFromFavouritesSuccess())
dispatch(ProfileActions.fetchProfileLoans())
} else {
dispatch(deletePublicationFromFavouritesFailure(favouritesResponse.status))
}
} catch (error) {
dispatch(deletePublicationFromFavouritesFailure(error))
}
}
}
export function requestDeleteFavouritesTag (tag) {
return requireLoginBeforeAction(showModal(ModalComponents.DELETE_FAVOURITES_TAG_MODAL, { tag: tag, isSuccess: true }))
}
export function deleteFavouritesTag (tag) {
return async (dispatch, getState) => {
try {
const url = "/api/v1/profile/favourites/tag"
const favouritesResponse = await fetch(url, {
method: "DELETE",
credentials: "same-origin",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ tag: tag })
})
if (favouritesResponse.status === 200) {
dispatch(deleteFavouritesTagSuccess())
dispatch(ProfileActions.fetchProfileLoans())
} else {
dispatch(deleteFavouritesTagFailure(favouritesResponse.status))
}
} catch (error) {
dispatch(deleteFavouritesTagFailure(error))
}
}
}
export function fetchFavouritesFailure (error) {
return dispatch => {
dispatch({
type: types.FETCH_FAVOURITES_FAILURE,
payload: { message: error },
error: true
})
}
}
export function savePublicationToFavouritesFailure (error) {
return dispatch => {
dispatch({
type: types.SAVE_TO_FAVOURITES_FAILURE,
payload: { message: error },
error: true
})
}
}
export function savePublicationToFavouritesSuccess () {
return dispatch => {
dispatch({
type: types.SAVE_TO_FAVOURITES_SUCCESS,
error: false
})
}
}
export function deletePublicationFromFavouritesFailure (error) {
return dispatch => {
dispatch({
type: types.DELETE_FROM_FAVOURITES_FAILURE,
payload: { message: error },
error: true
})
}
}
export function deletePublicationFromFavouritesSuccess () {
return dispatch => {
dispatch({
type: types.DELETE_FROM_FAVOURITES_SUCCESS,
error: false
})
}
}
export function deleteFavouritesTagFailure (error) {
return dispatch => {
dispatch({
type: types.DELETE_FAVOURITES_TAG_FAILURE,
payload: { message: error },
error: true
})
}
}
export function deleteFavouritesTagSuccess () {
return dispatch => {
dispatch({
type: types.DELETE_FAVOURITES_TAG_SUCCESS,
error: false
})
}
}
/*
Fething a list of saved favourites. Create a set with the biblioNumbers to be used in searchresults for marking already saved favourites.
*/
export function fetchFavourites (args) {
const url = "/api/v1/profile/favourites"
return async (dispatch, getState) => {
try {
const favouritesResponse = await fetch(url, {
method: "GET",
credentials: "same-origin",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(args)
})
if (favouritesResponse.status === 200) {
const favourites = await favouritesResponse.json()
const savedFavourites = new Set()
if (Array.isArray(favourites)) {
favourites.forEach(favourite => {
savedFavourites.add(favourite.biblioNumber)
})
}
dispatch(receiveFavourites(savedFavourites))
} else {
dispatch(fetchFavouritesFailure(Errors.favourites.GENERIC_FETCH_FAVOURITES_ERROR))
}
} catch (error) {
dispatch(fetchFavouritesFailure(error))
}
}
}
export function addToFavourites (biblioNumber) {
return (dispatch, getState) => {
dispatch(requireLoginBeforeAction(addToFavouritesModal(biblioNumber)))
}
}
function addToFavouritesModal (biblioNumber) {
return (dispatch, getState) => {
dispatch(ProfileActions.fetchProfileLoans())
dispatch(showModal(ModalComponents.ADD_TO_FAVOURITES_MODAL, { biblioNumber: biblioNumber }))
}
}
import fetch from "isomorphic-fetch"
import * as types from "../constants/ActionTypes"
import { requireLoginBeforeAction } from "./LoginActions"
import Errors from "../constants/Errors"
import { action } from "./GenericActions"
import merge from "../utils/mergeArraysOfObjects"
export function startFetchHistory (args) {
return requireLoginBeforeAction(fetchHistory(args))
}
export function fetchHistoryFailure (error) {
return dispatch => {
dispatch({
......@@ -123,7 +118,7 @@ export function fetchHistory (args) {
dispatch(updateHistory())
})
} else {
dispatch(fetchHistoryFailure(Errors.loan.GENERIC_FETCH_HISTORY_ERROR))
dispatch(fetchHistoryFailure(Errors.history.GENERIC_FETCH_HISTORY_ERROR))
}
}).catch(error => dispatch(fetchHistoryFailure(error)))
}
......
......@@ -7,6 +7,7 @@ import ModalComponents from "../constants/ModalComponents"
import Errors from "../constants/Errors"
import { action } from "./GenericActions"
import { fetchProfileInfo } from "./ProfileActions"
import { fetchFavourites } from "./FavouriteActions"
export const requestLogin = (username) => action(types.REQUEST_LOGIN, { username })
......@@ -74,6 +75,7 @@ export function login (username, password, captchaResponse, successActions = [])
})
.then(json => {
dispatch(loginSuccess(username, json.borrowerNumber, json.borrowerName, json.category))
dispatch(fetchFavourites())
return dispatch(fetchProfileInfo())
})
.then(() => {
......@@ -134,7 +136,12 @@ export function updateLoginStatus () {
},
credentials: "same-origin"
}).then(response => response.json())
.then(json => dispatch(receiveLoginStatus(json.isLoggedIn, json.borrowerNumber, json.borrowerName, json.homeBranch, json.category)))
.then(json => {
dispatch(receiveLoginStatus(json.isLoggedIn, json.borrowerNumber, json.borrowerName, json.homeBranch, json.category))
if (json.isLoggedIn) {
dispatch(fetchFavourites())
}
})
.catch(error => dispatch(loginStatusFailure(error)))
}
}
......
import React from "react"
import PropTypes from "prop-types"
import { connect } from "react-redux"
import { injectIntl } from "react-intl"
import { bindActionCreators } from "redux"
import * as FavouriteActions from "../actions/FavouriteActions"
import ListItemMetadata from "./ListItemMetadata"
import ListItemThumb from "./ListItemThumb"
import ClickableElement from "./ClickableElement"
class FavouriteItem extends React.Component {
constructor (props) {
super(props)
this.deleteFavourites = this.deleteFavourites.bind(this)
}
deleteFavourites (id) {
this.props.favouriteActions.deletePublicationFromFavourites(id)
}
render () {
const { favouriteItem } = this.props
return (
<article key={favouriteItem.id}
className="single-entry">
<ListItemThumb item={favouriteItem} />
<ListItemMetadata item={favouriteItem} />
<div className="flex-col placeholder-column" />
<div className="flex-col placeholder-column" />
<div className="flex-col placeholder-column" />
<div className="flex-col delete-favourites-entry">
<ClickableElement onClickAction={this.deleteFavourites} onClickArguments={favouriteItem.id}>
<img src="/images/delete-cross.svg" />
</ClickableElement>
</div>
</article>
)
}
}
function mapStateToProps (state) {
return {
}
}
FavouriteItem.propTypes = {
favouriteItem: PropTypes.object.isRequired,
favouriteActions: PropTypes.object.isRequired
}
function mapDispatchToProps (dispatch) {
return {
dispatch: dispatch,
favouriteActions: bindActionCreators(FavouriteActions, dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(injectIntl(FavouriteItem))
import React from "react"
import PropTypes from "prop-types"
import FavouriteItem from "./FavouriteItem"
class FavouriteItems extends React.Component {
render () {
const items = this.props.favouriteItems
.filter((item) => {
if (item.biblioNumber) {
return true
} else {
return false
}
})
.map((el, i) => <FavouriteItem key={i} favouriteItem={el} />)
return (
<div className="loan">
{items}
</div>
)
}
}
FavouriteItems.propTypes = {
favouriteItems: PropTypes.array.isRequired
}
export default FavouriteItems
......@@ -20,7 +20,6 @@ class HistoryItem extends React.Component {
render () {
const { historyItem, historyToDelete } = this.props
console.log(historyItem)
return (
<article key={historyItem.id}
className="single-entry"
......
......@@ -20,6 +20,8 @@ class ListItemMainContributors extends React.Component {
</div>
</h2>
)
} else {
return null
}
} else if (item.author) {
return <h2><span className={(item.isPurresak && item.returnedDate) ? "disabled" : ""}>{item.author}</span></h2>
......
......@@ -10,7 +10,7 @@ class ListItemThumb extends React.Component {
const { item } = this.props
if (item.publicationImage) {
return (
<div className="flex-col img-thumb">
<div className="flex-col img-thumb book-cover">
{item.relativePublicationPath
? <Link className={(item.isPurresak && item.returnedDate) ? "publication-title-disabled" : "publication-title"} to={item.relativePublicationPath}>
<img src={item.publicationImage} alt={item.volinfo ? `${item.title} (${item.volinfo})` : item.title} />
......@@ -23,7 +23,7 @@ class ListItemThumb extends React.Component {
)
} else if (typeof item.mediaType !== "undefined") {
return (
<div className="flex-col img-thumb">
<div className="flex-col img-thumb book-cover missing">
{item.relativePublicationPath
? <Link className={(item.isPurresak && item.returnedDate) ? "publication-title-disabled" : "publication-title"} to={item.relativePublicationPath}>
<img src={Constants.mediaTypeIconsMap[ Constants.mediaTypeIcons[ item.mediaType ] ]} />
......
......@@ -3,6 +3,7 @@ import React from "react"
import { injectIntl, intlShape, defineMessages, FormattedMessage } from "react-intl"
import { withRouter } from "react-router-dom"
import ClickableElement from "../components/ClickableElement"
import SearchResultAddToFavourites from "../components/SearchResultAddToFavourites"
import Constants from "../constants/Constants"
import title from "../utils/title"
import { connect } from "react-redux"
......@@ -12,20 +13,26 @@ class Publication extends React.Component {
const coverAltText = this.props.intl.formatMessage(messages.coverImageOf, { title: title(publication) })
if (publication.image) {
return (
<div className="book-cover">
<ClickableElement onClickAction={this.props.expandSubResource}
onClickArguments={[ this.props.history, this.props.publication.id, true ]}>
<img src={publication.image} alt={coverAltText} />
</ClickableElement>
<div className="remember-me-relative-position" style={publication.image ? { "position": "relative" } : {}}>
<div className="book-cover">
<ClickableElement onClickAction={this.props.expandSubResource}
onClickArguments={[ this.props.history, this.props.publication.id, true ]}>
<img src={publication.image} alt={coverAltText} />
</ClickableElement>
<SearchResultAddToFavourites publication={publication} />
</div>
</div>
)
} else if (typeof publication.mediaTypes[ 0 ] !== "undefined") {
return (
<div className="book-cover missing">
<ClickableElement onClickAction={this.props.expandSubResource}
onClickArguments={[ this.props.history, this.props.publication.id, true ]}>
<img src={Constants.mediaTypeIconsMap[ Constants.mediaTypeIcons[ publication.mediaTypes[ 0 ] ] ]} />
</ClickableElement>
<div className="remember-me-relative-position" style={publication.image ? { "position": "relative" } : {}}>
<div className="book-cover missing">