@@ -50,9 +52,15 @@ const props = defineProps({
margin-right: 30px;
border-top-right-radius: var(--bulma-box-radius);
border-bottom-right-radius: var(--bulma-box-radius);
+ cursor: grab;
}
-.positionwidget:hover {
- cursor: move;
+.positionwidget:active {
+ cursor: grabbing;
+}
+
+.dragover {
+ border: 3px solid var(--bulma-primary);
+ border-radius: 10px;
}
diff --git a/front/src/api.js b/front/src/api.js
index edffebd..a1db6fa 100644
--- a/front/src/api.js
+++ b/front/src/api.js
@@ -130,7 +130,7 @@ export function postCollection(collection) {
return genericPayloadCall('/ws/collection', collection, 'POST')
}
-export function postCollectionAddBook(collectionId, bookId) {
+export function postCollectionAddBook(collectionId, position) {
return genericPayloadCall(
'/ws/collection/' + collectionId + '/addbook',
{ 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) {
return genericPayloadCall('/ws/book/edit/' + id, book.value, 'PUT')
}
diff --git a/internal/adapter/adapter.go b/internal/adapter/adapter.go
index bef0656..1f7d4ab 100644
--- a/internal/adapter/adapter.go
+++ b/internal/adapter/adapter.go
@@ -28,6 +28,7 @@ func CollectionItemsQueryToDto(itemsQueryResult []query.CollectionItemQueryResul
}
dtoItems = append(dtoItems,
dto.CollectionItemGet{
+ ID: queryResult.ItemID,
Position: queryResult.Position,
Book: bookItem,
})
diff --git a/internal/apitest/fetchallcollections_test.go b/internal/apitest/fetchallcollections_test.go
index d01fa93..4eadc6d 100644
--- a/internal/apitest/fetchallcollections_test.go
+++ b/internal/apitest/fetchallcollections_test.go
@@ -12,8 +12,8 @@ import (
func TestFetchAllCollections_OK(t *testing.T) {
status, res := testFetchCollections(t, "10", "0")
assert.Equal(t, http.StatusOK, status)
- assert.Equal(t, int64(3), res.Count)
- assert.Equal(t, 3, len(res.Collections))
+ assert.Equal(t, int64(4), res.Count)
+ assert.Equal(t, 4, len(res.Collections))
}
func testFetchCollections(t *testing.T, limit string, offset string) (int, dto.CollectionItemsGet) {
diff --git a/internal/apitest/post_collection_changeposition_test.go b/internal/apitest/post_collection_changeposition_test.go
new file mode 100644
index 0000000..711ca3f
--- /dev/null
+++ b/internal/apitest/post_collection_changeposition_test.go
@@ -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
+}
diff --git a/internal/dto/in.go b/internal/dto/in.go
index 10fc867..c2c320d 100644
--- a/internal/dto/in.go
+++ b/internal/dto/in.go
@@ -51,6 +51,11 @@ type CollectionBook struct {
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 {
FileID uint `json:"fileId"`
FilePath string `json:"filepath"`
diff --git a/internal/dto/out.go b/internal/dto/out.go
index 42cc1fc..8769c7a 100644
--- a/internal/dto/out.go
+++ b/internal/dto/out.go
@@ -51,6 +51,7 @@ type CollectionGet struct {
}
type CollectionItemGet struct {
+ ID uint `json:"id"`
Position uint `json:"position"`
Book BookItemGet `json:"book"`
}
diff --git a/internal/i18nresource/locale.en.toml b/internal/i18nresource/locale.en.toml
index 0132d7a..d27ef44 100644
--- a/internal/i18nresource/locale.en.toml
+++ b/internal/i18nresource/locale.en.toml
@@ -10,3 +10,4 @@ RegistrationDisabled = "Registration has been disabled on this instance."
UserAlreadyExists = "An user with this name already exists."
ErrorWhenCreatingUserFromStr = "Error when creating user from string %s"
Unauthorized = "You are not allowed to access this document."
+ItemDoesNotBelongToCollection = "Item does not belong to the collection."
diff --git a/internal/i18nresource/locale.fr.toml b/internal/i18nresource/locale.fr.toml
index c2be96d..d23962d 100644
--- a/internal/i18nresource/locale.fr.toml
+++ b/internal/i18nresource/locale.fr.toml
@@ -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à."
ErrorWhenCreatingUserFromStr = "Erreur lors de la création de l'utilisateur %s"
Unauthorized = "Vous n'êtes pas autorisé à accéder à cette page."
+ItemDoesNotBelongToCollection = "Cet élément n'appartient pas à la liste."
diff --git a/internal/query/querycollections.go b/internal/query/querycollections.go
index 7d6a51d..a19a365 100644
--- a/internal/query/querycollections.go
+++ b/internal/query/querycollections.go
@@ -81,6 +81,7 @@ func fetchCollections(db *gorm.DB, userId uint) *gorm.DB {
}
type CollectionItemQueryResult struct {
+ ItemID uint
Position uint
ID uint
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 {
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 books on (books.id = collection_items.book_id)")
query = joinAuthors(query)
diff --git a/internal/routes/collectionaddbookpost.go b/internal/routes/collectionaddbookpost.go
index a656b11..a2633d8 100644
--- a/internal/routes/collectionaddbookpost.go
+++ b/internal/routes/collectionaddbookpost.go
@@ -11,6 +11,7 @@ import (
"git.artlef.fr/bibliomane/internal/model"
"git.artlef.fr/bibliomane/internal/myvalidator"
"github.com/gin-gonic/gin"
+ "gorm.io/gorm"
)
func PostCollectionBookHandler(ac appcontext.AppContext) {
@@ -56,7 +57,16 @@ func PostCollectionBookHandler(ac appcontext.AppContext) {
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.C.String(http.StatusOK, "Success")
}
diff --git a/internal/routes/collectionchangepositionpost.go b/internal/routes/collectionchangepositionpost.go
new file mode 100644
index 0000000..030729f
--- /dev/null
+++ b/internal/routes/collectionchangepositionpost.go
@@ -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")
+}
diff --git a/internal/setup/setup.go b/internal/setup/setup.go
index 3b78a4c..e20cebc 100644
--- a/internal/setup/setup.go
+++ b/internal/setup/setup.go
@@ -87,6 +87,9 @@ func Setup(config *config.Config) *gin.Engine {
ws.POST("/collection/:id/addbook", func(c *gin.Context) {
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) {
routes.PostCollectionHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
})