Collections: allow to drag and drop to change book position
This commit is contained in:
12
demodata.sql
12
demodata.sql
@@ -131,6 +131,7 @@ INSERT INTO collections(name, user_id) VALUES ('Littérature française',(SELECT
|
|||||||
INSERT INTO collections(name, user_id) VALUES ('Nouvelles',(SELECT id FROM users WHERE name = 'demo'));
|
INSERT INTO collections(name, user_id) VALUES ('Nouvelles',(SELECT id FROM users WHERE name = 'demo'));
|
||||||
INSERT INTO collections(name, user_id) VALUES ('Non fiction',(SELECT id FROM users WHERE name = 'demo2'));
|
INSERT INTO collections(name, user_id) VALUES ('Non fiction',(SELECT id FROM users WHERE name = 'demo2'));
|
||||||
INSERT INTO collections(name, user_id) VALUES ('Empty',(SELECT id FROM users WHERE name = 'demo'));
|
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 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 = '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);
|
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);
|
||||||
@@ -142,8 +143,17 @@ 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 = 'Nouvelles'), (SELECT id FROM books WHERE title = 'Dojoji et autres nouvelles'), 1);
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Nouvelles'), (SELECT id FROM books WHERE title = 'Dojoji et autres nouvelles'), 1);
|
||||||
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Nouvelles'), (SELECT id FROM books WHERE title = 'Le meurtre d''O-tsuya'), 2);
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Nouvelles'), (SELECT id FROM books WHERE title = 'Le meurtre d''O-tsuya'), 2);
|
||||||
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Nouvelles'), (SELECT id FROM books WHERE title = 'Le coup de pistolet'), 3);
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Nouvelles'), (SELECT id FROM books WHERE title = 'Le coup de pistolet'), 3);
|
||||||
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Nouvelles'), (SELECT id FROM books WHERE title = 'Duo'), 3);
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Nouvelles'), (SELECT id FROM books WHERE title = 'Duo'), 4);
|
||||||
|
|
||||||
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Non fiction'), (SELECT id FROM books WHERE title = 'Recherches philosophiques'), 1);
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Non fiction'), (SELECT id FROM books WHERE title = 'Recherches philosophiques'), 1);
|
||||||
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Non fiction'), (SELECT id FROM books WHERE title = 'De sang-froid'), 2);
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Non fiction'), (SELECT id FROM books WHERE title = 'De sang-froid'), 2);
|
||||||
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Non fiction'), (SELECT id FROM books WHERE title = 'The Life of Jesus'), 3);
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Non fiction'), (SELECT id FROM books WHERE title = 'The Life of Jesus'), 3);
|
||||||
|
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Lu récemment'), (SELECT id FROM books WHERE title = 'L''Homme sans qualités, tome 1'), 1);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Lu récemment'), (SELECT id FROM books WHERE title = 'Iliade'), 2);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Lu récemment'), (SELECT id FROM books WHERE title = 'Duo'), 3);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Lu récemment'), (SELECT id FROM books WHERE title = 'De sang-froid'), 4);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Lu récemment'), (SELECT id FROM books WHERE title = 'Le Pavillon d''or'), 5);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Lu récemment'), (SELECT id FROM books WHERE title = 'Recherches philosophiques'), 6);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Lu récemment'), (SELECT id FROM books WHERE title = 'Dojoji et autres nouvelles'), 7);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Lu récemment'), (SELECT id FROM books WHERE title = 'Le château'), 8);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { getCollection } from './api.js'
|
import { getCollection, postCollectionChangePosition } from './api.js'
|
||||||
import CollectionFormElement from './CollectionFormElement.vue'
|
import CollectionFormElement from './CollectionFormElement.vue'
|
||||||
import AddBookToCollection from './AddBookToCollection.vue'
|
import AddBookToCollection from './AddBookToCollection.vue'
|
||||||
import Pagination from './Pagination.vue'
|
import Pagination from './Pagination.vue'
|
||||||
@@ -17,6 +17,10 @@ const offset = computed(() => (pageNumber.value - 1) * limit)
|
|||||||
const data = ref(null)
|
const data = ref(null)
|
||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
|
|
||||||
|
const itemIdBeingGrabbed = ref(null)
|
||||||
|
|
||||||
|
const itemIdBeingOvered = ref(null)
|
||||||
|
|
||||||
let totalElementsNumber = computed(() =>
|
let totalElementsNumber = computed(() =>
|
||||||
typeof data != 'undefined' && data.value != null ? data.value['count'] : 0,
|
typeof data != 'undefined' && data.value != null ? data.value['count'] : 0,
|
||||||
)
|
)
|
||||||
@@ -35,15 +39,59 @@ function pageChange(newPageNumber) {
|
|||||||
function fetchCollection() {
|
function fetchCollection() {
|
||||||
pageChange(1)
|
pageChange(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onDragStart(event, id) {
|
||||||
|
event.dataTransfer.effectAllowed = 'move'
|
||||||
|
// Custom type to identify a collectionitem drag
|
||||||
|
event.dataTransfer.setData('collectionitem', '')
|
||||||
|
itemIdBeingGrabbed.value = id
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragover(event) {
|
||||||
|
if (event.dataTransfer.types.includes('collectionitem')) {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrop(id, position) {
|
||||||
|
if (id == itemIdBeingGrabbed.value) {
|
||||||
|
//nothing to do
|
||||||
|
return
|
||||||
|
}
|
||||||
|
postCollectionChangePosition(props.id, itemIdBeingGrabbed.value, position).then((res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
fetchCollection()
|
||||||
|
} else {
|
||||||
|
res.json().then((json) => {
|
||||||
|
error.value = json
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragend() {
|
||||||
|
itemIdBeingGrabbed.value = null
|
||||||
|
itemIdBeingOvered.value = null
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="error">{{ $t('bookform.error', { error: error.message }) }}</div>
|
<div v-if="error">{{ $t('collection.error', { error: error }) }}</div>
|
||||||
<div v-if="data">
|
<div v-if="data">
|
||||||
<h2 class="title">{{ data.name }}</h2>
|
<h2 class="title">{{ data.name }}</h2>
|
||||||
<AddBookToCollection :collection-id="props.id" @created="fetchCollection" />
|
<AddBookToCollection :collection-id="props.id" @created="fetchCollection" />
|
||||||
<div>
|
<div>
|
||||||
<CollectionFormElement v-for="item in data.items" :key="item.id" v-bind="item" />
|
<CollectionFormElement
|
||||||
|
@drop="onDrop(item.id, item.position)"
|
||||||
|
@dragstart="(e) => onDragStart(e, item.id)"
|
||||||
|
@dragend="onDragend"
|
||||||
|
@dragover="onDragover"
|
||||||
|
@dragenter="itemIdBeingOvered = item.id"
|
||||||
|
v-for="item in data.items"
|
||||||
|
:key="item.id"
|
||||||
|
:is-dragover="itemIdBeingOvered === item.id"
|
||||||
|
v-bind="item"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Pagination
|
<Pagination
|
||||||
class="mt-5"
|
class="mt-5"
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
import BookListElement from './BookListElement.vue'
|
import BookListElement from './BookListElement.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
isDragover: Boolean,
|
||||||
|
id: Number,
|
||||||
position: Number,
|
position: Number,
|
||||||
book: Array,
|
book: Array,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="collectionitembox">
|
<div :class="isDragover ? 'dragover' : ''" draggable="true" class="collectionitembox">
|
||||||
<BookListElement v-bind="props.book">
|
<BookListElement v-bind="props.book">
|
||||||
<div class="separator" />
|
<div class="separator" />
|
||||||
<div class="positionindicator centered is-narrow">
|
<div class="positionindicator centered is-narrow">
|
||||||
@@ -50,9 +52,15 @@ const props = defineProps({
|
|||||||
margin-right: 30px;
|
margin-right: 30px;
|
||||||
border-top-right-radius: var(--bulma-box-radius);
|
border-top-right-radius: var(--bulma-box-radius);
|
||||||
border-bottom-right-radius: var(--bulma-box-radius);
|
border-bottom-right-radius: var(--bulma-box-radius);
|
||||||
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|
||||||
.positionwidget:hover {
|
.positionwidget:active {
|
||||||
cursor: move;
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragover {
|
||||||
|
border: 3px solid var(--bulma-primary);
|
||||||
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export function postCollection(collection) {
|
|||||||
return genericPayloadCall('/ws/collection', collection, 'POST')
|
return genericPayloadCall('/ws/collection', collection, 'POST')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function postCollectionAddBook(collectionId, bookId) {
|
export function postCollectionAddBook(collectionId, position) {
|
||||||
return genericPayloadCall(
|
return genericPayloadCall(
|
||||||
'/ws/collection/' + collectionId + '/addbook',
|
'/ws/collection/' + collectionId + '/addbook',
|
||||||
{ bookId: bookId },
|
{ bookId: bookId },
|
||||||
@@ -138,6 +138,14 @@ export function postCollectionAddBook(collectionId, bookId) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function postCollectionChangePosition(collectionId, itemId, position) {
|
||||||
|
return genericPayloadCall(
|
||||||
|
'/ws/collection/' + collectionId + '/changeposition',
|
||||||
|
{ itemId: itemId, position: position },
|
||||||
|
'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')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ func CollectionItemsQueryToDto(itemsQueryResult []query.CollectionItemQueryResul
|
|||||||
}
|
}
|
||||||
dtoItems = append(dtoItems,
|
dtoItems = append(dtoItems,
|
||||||
dto.CollectionItemGet{
|
dto.CollectionItemGet{
|
||||||
|
ID: queryResult.ItemID,
|
||||||
Position: queryResult.Position,
|
Position: queryResult.Position,
|
||||||
Book: bookItem,
|
Book: bookItem,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
func TestFetchAllCollections_OK(t *testing.T) {
|
func TestFetchAllCollections_OK(t *testing.T) {
|
||||||
status, res := testFetchCollections(t, "10", "0")
|
status, res := testFetchCollections(t, "10", "0")
|
||||||
assert.Equal(t, http.StatusOK, status)
|
assert.Equal(t, http.StatusOK, status)
|
||||||
assert.Equal(t, int64(3), res.Count)
|
assert.Equal(t, int64(4), res.Count)
|
||||||
assert.Equal(t, 3, len(res.Collections))
|
assert.Equal(t, 4, len(res.Collections))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testFetchCollections(t *testing.T, limit string, offset string) (int, dto.CollectionItemsGet) {
|
func testFetchCollections(t *testing.T, limit string, offset string) (int, dto.CollectionItemsGet) {
|
||||||
|
|||||||
113
internal/apitest/post_collection_changeposition_test.go
Normal file
113
internal/apitest/post_collection_changeposition_test.go
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package apitest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.artlef.fr/bibliomane/internal/testutils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPostCollectionChangePositionHandler_PositionOk(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"itemId": 14,
|
||||||
|
"position": 2
|
||||||
|
}`
|
||||||
|
collectionId := "5"
|
||||||
|
status := testPostCollectionChangePositionHandler(collectionId, payload)
|
||||||
|
assert.Equal(t, http.StatusOK, status)
|
||||||
|
_, collection := testGetCollection(t, collectionId, "10", "0")
|
||||||
|
assert.Equal(t, uint(1), collection.Items[0].Position)
|
||||||
|
assert.Equal(t, uint(2), collection.Items[1].Position)
|
||||||
|
assert.Equal(t, uint(3), collection.Items[2].Position)
|
||||||
|
assert.Equal(t, uint(4), collection.Items[3].Position)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostCollectionChangePositionHandler_ChangeOtherElement(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"itemId": 17,
|
||||||
|
"position": 3
|
||||||
|
}`
|
||||||
|
collectionId := "5"
|
||||||
|
status := testPostCollectionChangePositionHandler(collectionId, payload)
|
||||||
|
assert.Equal(t, http.StatusOK, status)
|
||||||
|
_, collection := testGetCollection(t, collectionId, "10", "0")
|
||||||
|
assert.Equal(t, "Duo", collection.Items[3].Book.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostCollectionChangePositionHandler_LastPosition(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"itemId": 19,
|
||||||
|
"position": 546
|
||||||
|
}`
|
||||||
|
collectionId := "5"
|
||||||
|
status := testPostCollectionChangePositionHandler(collectionId, payload)
|
||||||
|
assert.Equal(t, http.StatusOK, status)
|
||||||
|
_, collection := testGetCollection(t, collectionId, "10", "0")
|
||||||
|
assert.Equal(t, "Recherches philosophiques", collection.Items[7].Book.Title)
|
||||||
|
assert.Equal(t, "Le château", collection.Items[6].Book.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostCollectionChangePositionHandler_FirstPosition(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"itemId": 16,
|
||||||
|
"position": 1
|
||||||
|
}`
|
||||||
|
collectionId := "5"
|
||||||
|
status := testPostCollectionChangePositionHandler(collectionId, payload)
|
||||||
|
assert.Equal(t, http.StatusOK, status)
|
||||||
|
_, collection := testGetCollection(t, collectionId, "10", "0")
|
||||||
|
assert.Equal(t, "Duo", collection.Items[0].Book.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostCollectionChangePositionHandler_WrongPosition(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"itemId": 9,
|
||||||
|
"position": 0
|
||||||
|
}`
|
||||||
|
collectionId := "5"
|
||||||
|
status := testPostCollectionChangePositionHandler(collectionId, payload)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostCollectionChangePositionHandler_MissingPosition(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"itemId": 9
|
||||||
|
}`
|
||||||
|
collectionId := "5"
|
||||||
|
status := testPostCollectionChangePositionHandler(collectionId, payload)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostCollectionChangePositionWrongCollection(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"itemId": 1,
|
||||||
|
"position": 9
|
||||||
|
}`
|
||||||
|
collectionId := "5"
|
||||||
|
status := testPostCollectionChangePositionHandler(collectionId, payload)
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPostCollectionChangePositionHandler(collectionId string, payload string) int {
|
||||||
|
router := testutils.TestSetup()
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
token := testutils.ConnectDemoUser(router)
|
||||||
|
req, _ := http.NewRequest("POST", "/ws/collection/"+collectionId+"/changeposition",
|
||||||
|
strings.NewReader(payload))
|
||||||
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
return w.Code
|
||||||
|
}
|
||||||
@@ -51,6 +51,11 @@ type CollectionBook struct {
|
|||||||
BookID uint `json:"bookId" binding:"required"`
|
BookID uint `json:"bookId" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CollectionItemPosition struct {
|
||||||
|
Position uint `json:"position" binding:"required,gte=1"`
|
||||||
|
ItemID uint `json:"itemId" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
type FileInfoPost struct {
|
type FileInfoPost struct {
|
||||||
FileID uint `json:"fileId"`
|
FileID uint `json:"fileId"`
|
||||||
FilePath string `json:"filepath"`
|
FilePath string `json:"filepath"`
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ type CollectionGet struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CollectionItemGet struct {
|
type CollectionItemGet struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
Position uint `json:"position"`
|
Position uint `json:"position"`
|
||||||
Book BookItemGet `json:"book"`
|
Book BookItemGet `json:"book"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ RegistrationDisabled = "Registration has been disabled on this instance."
|
|||||||
UserAlreadyExists = "An user with this name already exists."
|
UserAlreadyExists = "An user with this name already exists."
|
||||||
ErrorWhenCreatingUserFromStr = "Error when creating user from string %s"
|
ErrorWhenCreatingUserFromStr = "Error when creating user from string %s"
|
||||||
Unauthorized = "You are not allowed to access this document."
|
Unauthorized = "You are not allowed to access this document."
|
||||||
|
ItemDoesNotBelongToCollection = "Item does not belong to the collection."
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ RegistrationDisabled = "La création de nouveaux comptes a été désactivée su
|
|||||||
UserAlreadyExists = "Un utilisateur avec le même nom existe déjà."
|
UserAlreadyExists = "Un utilisateur avec le même nom existe déjà."
|
||||||
ErrorWhenCreatingUserFromStr = "Erreur lors de la création de l'utilisateur %s"
|
ErrorWhenCreatingUserFromStr = "Erreur lors de la création de l'utilisateur %s"
|
||||||
Unauthorized = "Vous n'êtes pas autorisé à accéder à cette page."
|
Unauthorized = "Vous n'êtes pas autorisé à accéder à cette page."
|
||||||
|
ItemDoesNotBelongToCollection = "Cet élément n'appartient pas à la liste."
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ func fetchCollections(db *gorm.DB, userId uint) *gorm.DB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CollectionItemQueryResult struct {
|
type CollectionItemQueryResult struct {
|
||||||
|
ItemID uint
|
||||||
Position uint
|
Position uint
|
||||||
ID uint
|
ID uint
|
||||||
Title string
|
Title string
|
||||||
@@ -113,7 +114,7 @@ func FetchCollectionBooksCount(db *gorm.DB, userId uint, collectionId uint) (int
|
|||||||
|
|
||||||
func fetchCollectionBooksQuery(db *gorm.DB, userId uint, collectionId uint) *gorm.DB {
|
func fetchCollectionBooksQuery(db *gorm.DB, userId uint, collectionId uint) *gorm.DB {
|
||||||
query := db.Model(&model.CollectionItem{})
|
query := db.Model(&model.CollectionItem{})
|
||||||
query = query.Select("collection_items.position, " + selectBookItem())
|
query = query.Select("collection_items.position, collection_items.ID as item_id, " + selectBookItem())
|
||||||
query = query.Joins("left join collections on (collection_items.collection_id = collections.id)")
|
query = query.Joins("left join collections on (collection_items.collection_id = collections.id)")
|
||||||
query = query.Joins("left join books on (books.id = collection_items.book_id)")
|
query = query.Joins("left join books on (books.id = collection_items.book_id)")
|
||||||
query = joinAuthors(query)
|
query = joinAuthors(query)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"git.artlef.fr/bibliomane/internal/model"
|
"git.artlef.fr/bibliomane/internal/model"
|
||||||
"git.artlef.fr/bibliomane/internal/myvalidator"
|
"git.artlef.fr/bibliomane/internal/myvalidator"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func PostCollectionBookHandler(ac appcontext.AppContext) {
|
func PostCollectionBookHandler(ac appcontext.AppContext) {
|
||||||
@@ -56,7 +57,16 @@ func PostCollectionBookHandler(ac appcontext.AppContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
item := model.CollectionItem{Position: 0, BookID: book.ID, CollectionID: collection.ID}
|
//reorder other items
|
||||||
|
q := ac.Db.Model(&model.CollectionItem{})
|
||||||
|
q = q.Where("collection_id = ?", collection.ID)
|
||||||
|
err = q.UpdateColumn("position", gorm.Expr("position + 1")).Error
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
item := model.CollectionItem{Position: 1, BookID: book.ID, CollectionID: collection.ID}
|
||||||
ac.Db.Save(&item)
|
ac.Db.Save(&item)
|
||||||
ac.C.String(http.StatusOK, "Success")
|
ac.C.String(http.StatusOK, "Success")
|
||||||
}
|
}
|
||||||
|
|||||||
106
internal/routes/collectionchangepositionpost.go
Normal file
106
internal/routes/collectionchangepositionpost.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
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"
|
||||||
|
"git.artlef.fr/bibliomane/internal/query"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PostCollectionChangePositionHandler(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 collectionBookPosition dto.CollectionItemPosition
|
||||||
|
err = ac.C.ShouldBindJSON(&collectionBookPosition)
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var item model.CollectionItem
|
||||||
|
err = ac.Db.First(&item, collectionBookPosition.ItemID).Error
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if collection.ID != item.CollectionID {
|
||||||
|
err := myvalidator.HttpError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Err: errors.New(i18nresource.GetTranslatedMessage(&ac, "ItemDoesNotBelongToCollection")),
|
||||||
|
}
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
count, err := query.FetchCollectionBooksCount(ac.Db, user.ID, item.CollectionID)
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newPosition := collectionBookPosition.Position
|
||||||
|
if int64(newPosition) > count {
|
||||||
|
newPosition = uint(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.Position == collectionBookPosition.Position {
|
||||||
|
//nothing to do
|
||||||
|
ac.C.String(http.StatusOK, "Success")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lowerPosition := item.Position + 1
|
||||||
|
higherPosition := item.Position - 1
|
||||||
|
operationToDo := ""
|
||||||
|
if item.Position < collectionBookPosition.Position {
|
||||||
|
higherPosition = collectionBookPosition.Position
|
||||||
|
operationToDo = "position - 1"
|
||||||
|
} else {
|
||||||
|
lowerPosition = collectionBookPosition.Position
|
||||||
|
operationToDo = "position + 1"
|
||||||
|
}
|
||||||
|
|
||||||
|
q := ac.Db.Model(&model.CollectionItem{})
|
||||||
|
q = q.Where("collection_id = ? AND position BETWEEN ? AND ?", collection.ID, lowerPosition, higherPosition)
|
||||||
|
err = q.UpdateColumn("position", gorm.Expr(operationToDo)).Error
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
item.Position = collectionBookPosition.Position
|
||||||
|
ac.Db.Save(&item)
|
||||||
|
ac.C.String(http.StatusOK, "Success")
|
||||||
|
}
|
||||||
@@ -87,6 +87,9 @@ func Setup(config *config.Config) *gin.Engine {
|
|||||||
ws.POST("/collection/:id/addbook", func(c *gin.Context) {
|
ws.POST("/collection/:id/addbook", func(c *gin.Context) {
|
||||||
routes.PostCollectionBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
routes.PostCollectionBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||||
})
|
})
|
||||||
|
ws.POST("/collection/:id/changeposition", func(c *gin.Context) {
|
||||||
|
routes.PostCollectionChangePositionHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||||
|
})
|
||||||
ws.POST("/collection", func(c *gin.Context) {
|
ws.POST("/collection", func(c *gin.Context) {
|
||||||
routes.PostCollectionHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
routes.PostCollectionHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user