Create new collections from my collections view

This commit is contained in:
2026-04-03 15:51:18 +02:00
parent b1bad80426
commit b48ab1e4de
11 changed files with 185 additions and 27 deletions

View File

@@ -0,0 +1,65 @@
<script setup>
import { ref, computed } from 'vue'
import { postCollection, extractFormErrorFromField } from './api.js'
const emit = defineEmits(['created'])
const collection = ref({
name: '',
})
const addingCollection = ref(false)
const errors = ref(null)
const error = computed(() => {
return extractFormErrorFromField('Name', errors.value)
})
const vFocus = {
mounted: (el) => el.focus(),
}
function onButtonClick() {
if (addingCollection.value) {
createCollection()
} else {
addingCollection.value = true
}
}
function createCollection() {
postCollection(collection.value).then((res) => {
if (res.ok) {
addingCollection.value = false
collection.value.name = ''
emit('created')
} else {
res.json().then((json) => (errors.value = json))
}
})
}
</script>
<template>
<div class="field has-addons">
<div v-if="addingCollection" class="control">
<input
:class="'input is-medium ' + (error ? 'is-danger' : '')"
v-focus
@keyup.enter="createCollection()"
type="text"
maxlength="300"
v-model="collection.name"
:placeholder="$t('collections.name')"
/>
<p v-if="error" class="help is-danger">{{ error }}</p>
</div>
<div class="control">
<button @click="onButtonClick" class="button is-medium mb-2">
<span class="icon" :title="$t('collections.add')">
<b-icon-check v-if="addingCollection" />
<b-icon-plus v-else />
</span>
</button>
</div>
</div>
</template>
<style scoped></style>

View File

@@ -3,6 +3,7 @@ import { ref, computed } from 'vue'
import { getCollections } from './api.js' import { getCollections } from './api.js'
import CollectionListElement from './CollectionListElement.vue' import CollectionListElement from './CollectionListElement.vue'
import Pagination from './Pagination.vue' import Pagination from './Pagination.vue'
import AddCollection from './AddCollection.vue'
const limit = 5 const limit = 5
const pageNumber = ref(1) const pageNumber = ref(1)
@@ -34,6 +35,7 @@ function pageChange(newPageNumber) {
<div> <div>
<div v-if="error">{{ $t('bookbrowser.error', { error: error.message }) }}</div> <div v-if="error">{{ $t('bookbrowser.error', { error: error.message }) }}</div>
<div v-else-if="data"> <div v-else-if="data">
<AddCollection @created="fetchData" />
<div class="collectionslist"> <div class="collectionslist">
<div class="my-2" v-for="collection in data.collections" :key="collection.id"> <div class="my-2" v-for="collection in data.collections" :key="collection.id">
<CollectionListElement v-bind="collection" /> <CollectionListElement v-bind="collection" />

View File

@@ -121,6 +121,10 @@ export async function postImportBook(id, language) {
return genericPayloadCall('/ws/importbook', { inventaireid: id, lang: language }, 'POST') return genericPayloadCall('/ws/importbook', { inventaireid: id, lang: language }, 'POST')
} }
export function postCollection(collection) {
return genericPayloadCall('/ws/collection', collection, 'POST')
}
export function putBook(id, book) { export function putBook(id, book) {
return genericPayloadCall('/ws/book/edit/' + id, book.value, 'PUT') return genericPayloadCall('/ws/book/edit/' + id, book.value, 'PUT')
} }

View File

@@ -89,5 +89,9 @@
"review": { "review": {
"title": "My review", "title": "My review",
"textplaceholder": "Write my review..." "textplaceholder": "Write my review..."
},
"collections": {
"add": "Add a collection",
"name": "Nom"
} }
} }

View File

@@ -89,5 +89,9 @@
"review": { "review": {
"title": "Ma critique", "title": "Ma critique",
"textplaceholder": "Écrire ma critique..." "textplaceholder": "Écrire ma critique..."
},
"collections": {
"add": "Ajouter une liste",
"name": "Nom"
} }
} }

View File

@@ -1,12 +1,8 @@
package apitest package apitest
import ( import (
"encoding/json"
"fmt"
"net/http" "net/http"
"net/http/httptest"
"strconv" "strconv"
"strings"
"testing" "testing"
"git.artlef.fr/bibliomane/internal/testutils" "git.artlef.fr/bibliomane/internal/testutils"
@@ -88,27 +84,7 @@ func TestPostBookHandler_AuthorTooLong(t *testing.T) {
} }
func testPostBookHandler(t *testing.T, bookJson string, expectedCode int) uint { func testPostBookHandler(t *testing.T, bookJson string, expectedCode int) uint {
router := testutils.TestSetup() status, id := testutils.TestPostCall(t, "/ws/book", bookJson)
w := httptest.NewRecorder() assert.Equal(t, expectedCode, status)
return id
token := testutils.ConnectDemoUser(router)
req, _ := http.NewRequest("POST", "/ws/book",
strings.NewReader(string(bookJson)))
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
router.ServeHTTP(w, req)
assert.Equal(t, expectedCode, w.Code)
if w.Code != http.StatusOK {
return 0
}
var parsed struct {
ID uint
}
err := json.Unmarshal(w.Body.Bytes(), &parsed)
if err != nil {
t.Error(err)
}
return parsed.ID
} }

View File

@@ -0,0 +1,31 @@
package apitest
import (
"net/http"
"testing"
"git.artlef.fr/bibliomane/internal/testutils"
"github.com/stretchr/testify/assert"
)
func TestPostCollectionHandler_Ok(t *testing.T) {
collectionJson :=
`{
"name": "My collection"
}`
status, _ := testPostCollectionHandler(t, collectionJson)
assert.Equal(t, http.StatusOK, status)
}
func TestPostCollectionHandler_NameTooLong(t *testing.T) {
collectionJson :=
`{
"name": "rsteerdemenschderraumschiffgebrauchlichtalsseinursprungvonkraftgestartseinlangefahrthinzwischensternartigraumaufdersuchenrsteerdemenschderraumschiffgebrauchlichtalsseinursprungvonkraftgestartseinlangefahrthinzwischensternartigraumaufdersuchenrsteerdemenschderraumschiffgebrauchlichtalsseinursprungvonkraftgestartseinlangefahrthinzwischensternartigraumaufdersuchen"
}`
status, _ := testPostCollectionHandler(t, collectionJson)
assert.Equal(t, http.StatusBadRequest, status)
}
func testPostCollectionHandler(t *testing.T, collectionJson string) (int, uint) {
return testutils.TestPostCall(t, "/ws/collection", collectionJson)
}

View File

@@ -35,6 +35,10 @@ type UserBookPutUpdate struct {
Review *string `json:"review"` Review *string `json:"review"`
} }
type CollectionFields struct {
Name string `json:"name" binding:"required,max=300"`
}
type FileInfoPost struct { type FileInfoPost struct {
FileID uint `json:"fileId"` FileID uint `json:"fileId"`
FilePath string `json:"filepath"` FilePath string `json:"filepath"`

View File

@@ -0,0 +1,41 @@
package routes
import (
"net/http"
"git.artlef.fr/bibliomane/internal/appcontext"
"git.artlef.fr/bibliomane/internal/dto"
"git.artlef.fr/bibliomane/internal/model"
"git.artlef.fr/bibliomane/internal/myvalidator"
"github.com/gin-gonic/gin"
)
func PostCollectionHandler(ac appcontext.AppContext) {
var collection dto.CollectionFields
err := ac.C.ShouldBindJSON(&collection)
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
user, fetchUserErr := ac.GetAuthenticatedUser()
if fetchUserErr != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
id, err := saveCollectionToDb(ac, &collection, &user)
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
ac.C.JSON(http.StatusOK, gin.H{"id": id})
}
func saveCollectionToDb(ac appcontext.AppContext, c *dto.CollectionFields, user *model.User) (uint, error) {
collection := model.Collection{
Name: c.Name,
User: *user,
}
err := ac.Db.Save(&collection).Error
return collection.ID, err
}

View File

@@ -81,6 +81,9 @@ func Setup(config *config.Config) *gin.Engine {
ws.GET("/collections", func(c *gin.Context) { ws.GET("/collections", func(c *gin.Context) {
routes.GetCollectionsHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config}) routes.GetCollectionsHandler(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})
})
ws.POST("/auth/signup", func(c *gin.Context) { ws.POST("/auth/signup", func(c *gin.Context) {
routes.PostSignupHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config}) routes.PostSignupHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
}) })

View File

@@ -104,3 +104,27 @@ func TestFetchModel[T any](t *testing.T, urlpath string, limit string, offset st
} }
return w.Code, result return w.Code, result
} }
func TestPostCall(t *testing.T, urlpath string, payload string) (int, uint) {
router := TestSetup()
w := httptest.NewRecorder()
token := ConnectDemoUser(router)
req, _ := http.NewRequest("POST", urlpath,
strings.NewReader(payload))
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
return w.Code, 0
}
var parsed struct {
ID uint
}
err := json.Unmarshal(w.Body.Bytes(), &parsed)
if err != nil {
t.Error(err)
}
return w.Code, parsed.ID
}