Collection: new widget to add book to collection
This commit is contained in:
95
front/src/AddBookToCollection.vue
Normal file
95
front/src/AddBookToCollection.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { getSearchBooks, postCollectionAddBook, extractFormErrorFromField } from './api.js'
|
||||
|
||||
const props = defineProps({
|
||||
collectionId: Number,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['created'])
|
||||
|
||||
const book = ref({
|
||||
id: 0,
|
||||
title: '',
|
||||
})
|
||||
const addingBook = ref(false)
|
||||
const data = ref(null)
|
||||
const error = ref(null)
|
||||
const titleError = computed(() => {
|
||||
return extractFormErrorFromField('Title', error.value)
|
||||
})
|
||||
const vFocus = {
|
||||
mounted: (el) => el.focus(),
|
||||
}
|
||||
|
||||
const limit = 5
|
||||
|
||||
function fetchBooks() {
|
||||
if (!book || book.value.title.length < 3) {
|
||||
return
|
||||
}
|
||||
const lang = navigator.language.substring(0, 2)
|
||||
getSearchBooks(data, error, book.value.title, lang, 0, limit, 0)
|
||||
}
|
||||
|
||||
function addBook(bookId) {
|
||||
postCollectionAddBook(props.collectionId, bookId).then((res) => {
|
||||
if (res.ok) {
|
||||
addingBook.value = false
|
||||
book.value.id = 0
|
||||
book.value.title = ''
|
||||
data.value = null
|
||||
error.value = null
|
||||
emit('created')
|
||||
} else {
|
||||
res.json().then((json) => {
|
||||
console.log(json)
|
||||
error.value = json
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="field has-addons">
|
||||
<div v-if="addingBook" class="control">
|
||||
<input
|
||||
:class="'input is-large ' + (titleError ? 'is-danger' : '')"
|
||||
v-focus
|
||||
@keyup="fetchBooks()"
|
||||
type="text"
|
||||
maxlength="300"
|
||||
v-model="book.title"
|
||||
:placeholder="$t('addbook.title')"
|
||||
/>
|
||||
<p v-if="titleError" class="help is-danger">{{ titleError }}</p>
|
||||
<ul v-if="data" class="popupresults has-background-dark">
|
||||
<li v-for="book in data.books" @click="addBook(book.id)" class="bookresult p-2">
|
||||
{{ book.title }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="!addingBook" class="control">
|
||||
<button @click="addingBook = true" class="button is-large mb-2">
|
||||
<span class="icon" :title="$t('collections.add')">
|
||||
<b-icon-plus />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.popupresults {
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.bookresult {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.bookresult:hover {
|
||||
background-color: var(--bulma-text-40);
|
||||
}
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { getCollection } from './api.js'
|
||||
import CollectionFormBookItem from './CollectionFormBookItem.vue'
|
||||
import AddBookToCollection from './AddBookToCollection.vue'
|
||||
import Pagination from './Pagination.vue'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -27,14 +28,20 @@ getCollection(data, error, props.id, limit, offset.value)
|
||||
function pageChange(newPageNumber) {
|
||||
pageNumber.value = newPageNumber
|
||||
data.value = null
|
||||
error.value = null
|
||||
getCollection(data, error, props.id, limit, offset.value)
|
||||
}
|
||||
|
||||
function fetchCollection() {
|
||||
pageChange(1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="error">{{ $t('bookform.error', { error: error.message }) }}</div>
|
||||
<div v-if="data">
|
||||
<h2 class="title">{{ data.name }}</h2>
|
||||
<AddBookToCollection :collection-id="props.id" @created="fetchCollection" />
|
||||
<div>
|
||||
<CollectionFormBookItem v-for="book in data.books" :key="book.id" v-bind="book" />
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,15 @@ function fetchData(searchTerm, authorId) {
|
||||
error.value = null
|
||||
if (searchTerm != null) {
|
||||
const lang = navigator.language.substring(0, 2)
|
||||
getSearchBooks(data, error, searchTerm, lang, forceSearchInventaire.value, limit, offset.value)
|
||||
getSearchBooks(
|
||||
data,
|
||||
error,
|
||||
searchTerm,
|
||||
lang,
|
||||
forceSearchInventaire.value ? 2 : 1,
|
||||
limit,
|
||||
offset.value,
|
||||
)
|
||||
} else if (authorId != null) {
|
||||
getAuthorBooks(data, error, authorId, limit, offset.value)
|
||||
}
|
||||
|
||||
@@ -130,6 +130,14 @@ export function postCollection(collection) {
|
||||
return genericPayloadCall('/ws/collection', collection, 'POST')
|
||||
}
|
||||
|
||||
export function postCollectionAddBook(collectionId, bookId) {
|
||||
return genericPayloadCall(
|
||||
'/ws/collection/' + collectionId + '/addbook',
|
||||
{ bookId: bookId },
|
||||
'POST',
|
||||
)
|
||||
}
|
||||
|
||||
export function putBook(id, book) {
|
||||
return genericPayloadCall('/ws/book/edit/' + id, book.value, 'PUT')
|
||||
}
|
||||
@@ -219,10 +227,9 @@ export function genericPayloadCall(apiRoute, object, method) {
|
||||
}
|
||||
|
||||
export function extractFormErrorFromField(fieldName, errors) {
|
||||
if (errors == null) {
|
||||
if (errors == null || typeof errors == 'undefined' || !Array.isArray(errors)) {
|
||||
return ''
|
||||
}
|
||||
console.log(errors.value)
|
||||
const titleErr = errors.find((e) => e['field'] === fieldName)
|
||||
if (typeof titleErr !== 'undefined') {
|
||||
return titleErr.error
|
||||
|
||||
64
internal/apitest/post_collection_addbook_test.go
Normal file
64
internal/apitest/post_collection_addbook_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package apitest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.artlef.fr/bibliomane/internal/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPostCollectionBookHandler_Ok(t *testing.T) {
|
||||
payload :=
|
||||
`{
|
||||
"bookId": 9
|
||||
}`
|
||||
collectionId := "1"
|
||||
status := testPostCollectionBookHandler(t, collectionId, payload)
|
||||
assert.Equal(t, http.StatusOK, status)
|
||||
_, collection := testGetCollection(t, collectionId, "10", "0")
|
||||
assert.Equal(t, int64(7), collection.Count)
|
||||
}
|
||||
|
||||
func TestPostCollectionBookHandler_CollectionNotFound(t *testing.T) {
|
||||
payload :=
|
||||
`{
|
||||
"bookId": 9
|
||||
}`
|
||||
status := testPostCollectionBookHandler(t, "12", payload)
|
||||
assert.Equal(t, http.StatusNotFound, status)
|
||||
}
|
||||
|
||||
func TestPostCollectionBookHandler_BookNotFound(t *testing.T) {
|
||||
payload :=
|
||||
`{
|
||||
"bookId": 14654
|
||||
}`
|
||||
status := testPostCollectionBookHandler(t, "1", payload)
|
||||
assert.Equal(t, http.StatusNotFound, status)
|
||||
}
|
||||
|
||||
func TestPostCollectionBookHandler_Unauthorized(t *testing.T) {
|
||||
payload :=
|
||||
`{
|
||||
"bookId": 9
|
||||
}`
|
||||
status := testPostCollectionBookHandler(t, "3", payload)
|
||||
assert.Equal(t, http.StatusUnauthorized, status)
|
||||
}
|
||||
|
||||
func testPostCollectionBookHandler(t *testing.T, collectionId string, payload string) int {
|
||||
router := testutils.TestSetup()
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
token := testutils.ConnectDemoUser(router)
|
||||
req, _ := http.NewRequest("POST", "/ws/collection/"+collectionId+"/addbook",
|
||||
strings.NewReader(payload))
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
return w.Code
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package apitest
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -15,14 +16,14 @@ import (
|
||||
)
|
||||
|
||||
func TestSearchBook_MultipleBooks(t *testing.T) {
|
||||
result := testSearchBook(t, "san", "", "")
|
||||
result := testSearchBook(t, "san", "", "", dto.NoInventaireSearch)
|
||||
|
||||
assert.Equal(t, int64(2), result.Count)
|
||||
assert.Equal(t, 2, len(result.Books))
|
||||
}
|
||||
|
||||
func TestSearchBook_OneBookNotUserBook(t *testing.T) {
|
||||
result := testSearchBook(t, "iliade", "", "")
|
||||
result := testSearchBook(t, "iliade", "", "", dto.NoInventaireSearch)
|
||||
assert.Equal(t, int64(1), result.Count)
|
||||
assert.Equal(t,
|
||||
[]dto.BookItemGet{{
|
||||
@@ -38,7 +39,7 @@ func TestSearchBook_OneBookNotUserBook(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSearchBook_OneBookRead(t *testing.T) {
|
||||
result := testSearchBook(t, "dieux", "", "")
|
||||
result := testSearchBook(t, "dieux", "", "", dto.NoInventaireSearch)
|
||||
assert.Equal(t, int64(1), result.Count)
|
||||
assert.Equal(t,
|
||||
[]dto.BookItemGet{{
|
||||
@@ -55,7 +56,7 @@ func TestSearchBook_OneBookRead(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSearchBook_OneBookStartRead(t *testing.T) {
|
||||
result := testSearchBook(t, "Recherches", "", "")
|
||||
result := testSearchBook(t, "Recherches", "", "", dto.NoInventaireSearch)
|
||||
assert.Equal(t, int64(1), result.Count)
|
||||
assert.Equal(t,
|
||||
[]dto.BookItemGet{{
|
||||
@@ -72,7 +73,7 @@ func TestSearchBook_OneBookStartRead(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSearchBook_ISBN(t *testing.T) {
|
||||
result := testSearchBook(t, "9782070337903", "", "")
|
||||
result := testSearchBook(t, "9782070337903", "", "", dto.NoInventaireSearch)
|
||||
assert.Equal(t, int64(1), result.Count)
|
||||
assert.Equal(t,
|
||||
[]dto.BookItemGet{{
|
||||
@@ -88,7 +89,7 @@ func TestSearchBook_ISBN(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSearchBook_ISBNInventaire(t *testing.T) {
|
||||
result := testSearchBook(t, "9782253158400", "", "")
|
||||
result := testSearchBook(t, "9782253158400", "", "", dto.InventaireIfNothingFound)
|
||||
assert.Equal(t, int64(1), result.Count)
|
||||
assert.Equal(t,
|
||||
[]dto.BookItemGet{{
|
||||
@@ -107,17 +108,17 @@ func TestSearchBook_ISBNInventaire(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSearchBook_Limit(t *testing.T) {
|
||||
result := testSearchBook(t, "a", "10", "")
|
||||
result := testSearchBook(t, "a", "10", "", dto.NoInventaireSearch)
|
||||
assert.Equal(t, 10, len(result.Books))
|
||||
}
|
||||
|
||||
func TestSearchBook_Offset(t *testing.T) {
|
||||
result := testSearchBook(t, "sa", "", "2")
|
||||
result := testSearchBook(t, "sa", "", "2", dto.NoInventaireSearch)
|
||||
assert.Equal(t, int64(5), result.Count)
|
||||
assert.Equal(t, 3, len(result.Books))
|
||||
}
|
||||
|
||||
func testSearchBook(t *testing.T, searchterm string, limit string, offset string) dto.BookItemsGet {
|
||||
func testSearchBook(t *testing.T, searchterm string, limit string, offset string, inventaireSearchType dto.InventaireSearchType) dto.BookItemsGet {
|
||||
router := testutils.TestSetup()
|
||||
|
||||
u, err := url.Parse("/ws/search/" + searchterm)
|
||||
@@ -137,6 +138,7 @@ func testSearchBook(t *testing.T, searchterm string, limit string, offset string
|
||||
|
||||
q := u.Query()
|
||||
q.Set("lang", "fr")
|
||||
q.Set("inventaire", strconv.Itoa(int(inventaireSearchType)))
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
token := testutils.ConnectDemoUser(router)
|
||||
|
||||
@@ -5,9 +5,17 @@ type AuthorGet struct {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type InventaireSearchType int
|
||||
|
||||
const (
|
||||
NoInventaireSearch InventaireSearchType = iota
|
||||
InventaireIfNothingFound
|
||||
ForceInventaireSearch
|
||||
)
|
||||
|
||||
type BookSearchGetParam struct {
|
||||
Lang string `form:"lang" binding:"max=5"`
|
||||
Inventaire bool `form:"inventaire"`
|
||||
Inventaire InventaireSearchType `form:"inventaire"`
|
||||
}
|
||||
|
||||
type BookFields struct {
|
||||
@@ -39,6 +47,10 @@ type CollectionFields struct {
|
||||
Name string `json:"name" binding:"required,max=300"`
|
||||
}
|
||||
|
||||
type CollectionBook struct {
|
||||
BookID uint `json:"bookId" binding:"required"`
|
||||
}
|
||||
|
||||
type FileInfoPost struct {
|
||||
FileID uint `json:"fileId"`
|
||||
FilePath string `json:"filepath"`
|
||||
|
||||
@@ -57,6 +57,9 @@ func ReturnErrorsAsJsonResponse(ac *appcontext.AppContext, err error) {
|
||||
ac.C.JSON(httpError.StatusCode, gin.H{"error": httpError.Err.Error()})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
ac.C.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
}
|
||||
ac.C.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ func GetSearchBooksHandler(ac appcontext.AppContext) {
|
||||
return
|
||||
}
|
||||
var returnedBooks dto.BookItemsGet
|
||||
if !params.Inventaire {
|
||||
if params.Inventaire != dto.ForceInventaireSearch {
|
||||
books, err := query.FetchBookSearchGet(ac.Db, user.ID, searchterm, limit, offset)
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
@@ -51,7 +51,7 @@ func GetSearchBooksHandler(ac appcontext.AppContext) {
|
||||
}
|
||||
returnedBooks = dto.BookItemsGet{Count: count, Inventaire: false, Books: books}
|
||||
}
|
||||
if params.Inventaire || len(returnedBooks.Books) == 0 {
|
||||
if (params.Inventaire == dto.InventaireIfNothingFound && len(returnedBooks.Books) == 0) || (params.Inventaire == dto.ForceInventaireSearch) {
|
||||
returnedBooksPtr, err := searchInInventaireAPI(ac.Config.InventaireUrl, searchterm, limit, offset, params)
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
|
||||
62
internal/routes/collectionaddbookpost.go
Normal file
62
internal/routes/collectionaddbookpost.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.artlef.fr/bibliomane/internal/appcontext"
|
||||
"git.artlef.fr/bibliomane/internal/dto"
|
||||
"git.artlef.fr/bibliomane/internal/i18nresource"
|
||||
"git.artlef.fr/bibliomane/internal/model"
|
||||
"git.artlef.fr/bibliomane/internal/myvalidator"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func PostCollectionBookHandler(ac appcontext.AppContext) {
|
||||
collectionId, err := strconv.ParseUint(ac.C.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
ac.C.JSON(http.StatusBadRequest, gin.H{"error": err})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := ac.GetAuthenticatedUser()
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
|
||||
var collection model.Collection
|
||||
err = ac.Db.First(&collection, collectionId).Error
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
|
||||
if collection.UserID != user.ID {
|
||||
err := myvalidator.HttpError{
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
Err: errors.New(i18nresource.GetTranslatedMessage(&ac, "Unauthorized")),
|
||||
}
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
|
||||
var collectionBook dto.CollectionBook
|
||||
err = ac.C.ShouldBindJSON(&collectionBook)
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
|
||||
var book model.Book
|
||||
err = ac.Db.First(&book, collectionBook.BookID).Error
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
|
||||
collection.Books = append(collection.Books, book)
|
||||
ac.Db.Save(&collection)
|
||||
ac.C.String(http.StatusOK, "Success")
|
||||
}
|
||||
@@ -84,6 +84,9 @@ func Setup(config *config.Config) *gin.Engine {
|
||||
ws.GET("/collection/:id", func(c *gin.Context) {
|
||||
routes.GetCollectionHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||
})
|
||||
ws.POST("/collection/:id/addbook", func(c *gin.Context) {
|
||||
routes.PostCollectionBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||
})
|
||||
ws.POST("/collection", func(c *gin.Context) {
|
||||
routes.PostCollectionHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user