Collections browser: add a button to remove collections
This commit is contained in:
@@ -133,6 +133,8 @@ INSERT INTO collections(name, user_id) VALUES ('Non fiction',(SELECT id FROM use
|
||||
INSERT INTO collections(name, user_id) VALUES ('Empty',(SELECT id FROM users WHERE name = 'demo'));
|
||||
INSERT INTO collections(name, user_id) VALUES ('Lu récemment',(SELECT id FROM users WHERE name = 'demo'));
|
||||
INSERT INTO collections(name, user_id) VALUES ('Brouillon',(SELECT id FROM users WHERE name = 'demo'));
|
||||
INSERT INTO collections(name, user_id) VALUES ('Traduit de l''anglais',(SELECT id FROM users WHERE name = 'demo'));
|
||||
INSERT INTO collections(name, user_id) VALUES ('À supprimer',(SELECT id FROM users WHERE name = 'demo'));
|
||||
|
||||
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Littérature française'), (SELECT id FROM books WHERE title = 'Nord'), 1);
|
||||
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Littérature française'), (SELECT id FROM books WHERE title = 'Gargantua'), 2);
|
||||
@@ -165,3 +167,7 @@ INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT i
|
||||
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Brouillon'), (SELECT id FROM books WHERE title = 'Un barrage contre le Pacifique'), 4);
|
||||
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Brouillon'), (SELECT id FROM books WHERE title = 'Rigodon'), 5);
|
||||
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Brouillon'), (SELECT id FROM books WHERE title = 'Les dieux ont soif'), 6);
|
||||
|
||||
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Traduit de l''anglais'), (SELECT id FROM books WHERE title = 'Sa majesté des mouches'), 1);
|
||||
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Traduit de l''anglais'), (SELECT id FROM books WHERE title = 'Le complot contre l''Amérique'), 2);
|
||||
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Traduit de l''anglais'), (SELECT id FROM books WHERE title = 'De sang-froid'), 3);
|
||||
|
||||
@@ -180,7 +180,6 @@ function onPointerMove(e) {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
font-size: 36px;
|
||||
|
||||
}
|
||||
|
||||
.align-right {
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<script setup>
|
||||
import { getImagePathOrDefault } from './api.js'
|
||||
import { useTemplateRef, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const emit = defineEmits(['delete'])
|
||||
|
||||
const closeButtonDesktop = useTemplateRef('closeDesktop')
|
||||
const closeButtonMobile = useTemplateRef('closeMobile')
|
||||
|
||||
const props = defineProps({
|
||||
id: Number,
|
||||
name: String,
|
||||
@@ -11,10 +17,16 @@ const props = defineProps({
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
function goToCollection() {
|
||||
const collectionId = props.id
|
||||
function onClick(e) {
|
||||
if (
|
||||
(closeButtonDesktop.value && closeButtonDesktop.value.contains(e.target)) ||
|
||||
(closeButtonMobile.value && closeButtonMobile.value.contains(e.target))
|
||||
) {
|
||||
emit('delete')
|
||||
} else {
|
||||
router.push(`/collection/${props.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
function setBookOpacityClass(index) {
|
||||
const length = props.books.length
|
||||
@@ -33,11 +45,16 @@ function setBookOpacityClass(index) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="collectioncontainer has-background-dark p-2" @click="goToCollection">
|
||||
<div class="collectioncontainer has-background-dark p-2" @click="onClick">
|
||||
<div class="collectionheader">
|
||||
<h2 class="subtitle">
|
||||
<h2 class="namecontainer subtitle">
|
||||
{{ props.name }}
|
||||
</h2>
|
||||
<div class="is-hidden-desktop align-right">
|
||||
<div ref="closeMobile" @click="$emit('delete')" class="centered closebtn clickable">
|
||||
<b-icon-x />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collectionpreviewbooks" v-if="props.books && props.books.length > 0">
|
||||
<div
|
||||
@@ -53,6 +70,9 @@ function setBookOpacityClass(index) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="closeDesktop" class="is-hidden-touch centered closebtn clickable">
|
||||
<b-icon-x />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -75,6 +95,11 @@ img {
|
||||
transition: ease-in-out 0.02s;
|
||||
}
|
||||
|
||||
.namecontainer {
|
||||
flex: 1;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.collectionheader {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -117,6 +142,17 @@ img {
|
||||
opacity: 10%;
|
||||
}
|
||||
|
||||
.closebtn {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
img {
|
||||
max-height: 75px;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { getCollections } from './api.js'
|
||||
import { getCollections, deleteCollection } from './api.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
import CollectionListElement from './CollectionListElement.vue'
|
||||
import Pagination from './Pagination.vue'
|
||||
@@ -35,6 +35,18 @@ function pageChange(newPageNumber) {
|
||||
function goToCollection(id) {
|
||||
router.push(`/collection/${id}`)
|
||||
}
|
||||
|
||||
function removeList(id) {
|
||||
deleteCollection(id).then((res) => {
|
||||
if (res.ok) {
|
||||
fetchData()
|
||||
} else {
|
||||
res.json().then((json) => {
|
||||
error.value = json
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -44,7 +56,7 @@ function goToCollection(id) {
|
||||
<AddCollection @created="goToCollection" />
|
||||
<div class="collectionslist">
|
||||
<div class="my-2" v-for="collection in data.collections" :key="collection.id">
|
||||
<CollectionListElement v-bind="collection" />
|
||||
<CollectionListElement @delete="removeList(collection.id)" v-bind="collection" />
|
||||
</div>
|
||||
</div>
|
||||
<Pagination
|
||||
|
||||
@@ -150,6 +150,10 @@ export function deleteCollectionItem(itemId) {
|
||||
return deleteApiCall('/ws/collection/item/' + itemId)
|
||||
}
|
||||
|
||||
export function deleteCollection(id) {
|
||||
return deleteApiCall('/ws/collection/' + id)
|
||||
}
|
||||
|
||||
export function putBook(id, book) {
|
||||
return genericPayloadCall('/ws/book/edit/' + id, book.value, 'PUT')
|
||||
}
|
||||
|
||||
51
internal/apitest/delete_collection_test.go
Normal file
51
internal/apitest/delete_collection_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package apitest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.artlef.fr/bibliomane/internal/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDeleteCollectionHandler_EmptyCollection(t *testing.T) {
|
||||
collectionId := "8"
|
||||
status := testDeleteCollectionHandler(collectionId)
|
||||
assert.Equal(t, http.StatusOK, status)
|
||||
getStatus, _ := testGetCollection(t, collectionId, "10", "0")
|
||||
assert.Equal(t, http.StatusNotFound, getStatus)
|
||||
}
|
||||
|
||||
func TestDeleteCollectionHandler_NotEmptyCollection(t *testing.T) {
|
||||
collectionId := "7"
|
||||
status := testDeleteCollectionHandler(collectionId)
|
||||
assert.Equal(t, http.StatusOK, status)
|
||||
getStatus, _ := testGetCollection(t, collectionId, "10", "0")
|
||||
assert.Equal(t, http.StatusNotFound, getStatus)
|
||||
}
|
||||
|
||||
func TestDeleteCollectionHandler_NonExistingCollection(t *testing.T) {
|
||||
collectionId := "425"
|
||||
status := testDeleteCollectionHandler(collectionId)
|
||||
assert.Equal(t, http.StatusNotFound, status)
|
||||
}
|
||||
|
||||
func TestDeleteCollectionHandler_ForbiddenCollection(t *testing.T) {
|
||||
collectionId := "3"
|
||||
status := testDeleteCollectionHandler(collectionId)
|
||||
assert.Equal(t, http.StatusUnauthorized, status)
|
||||
}
|
||||
|
||||
func testDeleteCollectionHandler(id string) int {
|
||||
router := testutils.TestSetup()
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
token := testutils.ConnectDemoUser(router)
|
||||
req, _ := http.NewRequest("DELETE", "/ws/collection/"+id, nil)
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
return w.Code
|
||||
}
|
||||
@@ -7,5 +7,4 @@ type Collection struct {
|
||||
Name string
|
||||
User User
|
||||
UserID uint
|
||||
Items []CollectionItem
|
||||
}
|
||||
|
||||
66
internal/routes/collectiondelete.go
Normal file
66
internal/routes/collectiondelete.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.artlef.fr/bibliomane/internal/appcontext"
|
||||
"git.artlef.fr/bibliomane/internal/i18nresource"
|
||||
"git.artlef.fr/bibliomane/internal/model"
|
||||
"git.artlef.fr/bibliomane/internal/myvalidator"
|
||||
"git.artlef.fr/bibliomane/internal/query"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func DeleteCollectionHandler(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
|
||||
}
|
||||
|
||||
err = myvalidator.ValidateId(ac.Db, uint(collectionId), &model.Collection{})
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
|
||||
collection, err := query.FetchCollectionHeader(ac.Db, uint(collectionId))
|
||||
|
||||
if collection.UserID != user.ID {
|
||||
err := myvalidator.HttpError{
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
Err: errors.New(i18nresource.GetTranslatedMessage(&ac, "Unauthorized")),
|
||||
}
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
|
||||
collectionDb := model.Collection{}
|
||||
collectionDb.ID = uint(collectionId)
|
||||
|
||||
err = ac.Db.Where("collection_id = ?", collectionId).Delete(&model.CollectionItem{}).Error
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
err = ac.Db.Delete(&collectionDb).Error
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
err = ac.Db.Delete(&collectionDb).Error
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
|
||||
ac.C.JSON(http.StatusOK, "Success")
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"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"
|
||||
"git.artlef.fr/bibliomane/internal/query"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -21,6 +22,12 @@ func GetCollectionHandler(ac appcontext.AppContext) {
|
||||
return
|
||||
}
|
||||
|
||||
err = myvalidator.ValidateId(ac.Db, uint(collectionId), &model.Collection{})
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := ac.GetAuthenticatedUser()
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
|
||||
@@ -102,6 +102,9 @@ func Setup(config *config.Config) *gin.Engine {
|
||||
ws.POST("/upload/cover", func(c *gin.Context) {
|
||||
routes.PostUploadBookCoverHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||
})
|
||||
ws.DELETE("/collection/:id", func(c *gin.Context) {
|
||||
routes.DeleteCollectionHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||
})
|
||||
ws.DELETE("/collection/item/:id", func(c *gin.Context) {
|
||||
routes.DeleteCollectionBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user