Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed99ee772a | |||
| 2f5fc3d0a3 | |||
| b48c42c40c | |||
| 08a273b500 | |||
| 1ae76ed525 | |||
| 7d867af654 | |||
| 11a23d174e | |||
| e746e67e89 | |||
| b47b09eb85 |
14
demodata.sql
14
demodata.sql
@@ -132,6 +132,9 @@ INSERT INTO collections(name, user_id) VALUES ('Nouvelles',(SELECT id FROM users
|
||||
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 ('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);
|
||||
@@ -157,3 +160,14 @@ 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 = '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);
|
||||
|
||||
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Brouillon'), (SELECT id FROM books WHERE title = 'Nord'), 1);
|
||||
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Brouillon'), (SELECT id FROM books WHERE title = 'Gargantua'), 2);
|
||||
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Brouillon'), (SELECT id FROM books WHERE title = 'Duo'), 3);
|
||||
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);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bibliomane",
|
||||
"version": "0.7.1",
|
||||
"version": "0.8.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
||||
@@ -130,6 +130,7 @@ async function importInventaireEdition(inventaireid) {
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
<div class="columns no-padding box container has-background-dark">
|
||||
<slot name="left"></slot>
|
||||
<div class="media column no-margin clickable" @click="openBook">
|
||||
<div class="media-left">
|
||||
<figure class="image mb-3">
|
||||
@@ -164,7 +165,7 @@ async function importInventaireEdition(inventaireid) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<slot></slot>
|
||||
<slot name="right"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -208,5 +209,12 @@ img {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: 100px;
|
||||
max-width: 100px;
|
||||
height: auto;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { getCollection, postCollectionChangePosition } from './api.js'
|
||||
import { computed, ref, useTemplateRef } from 'vue'
|
||||
import { getCollection, postCollectionChangePosition, deleteCollectionItem } from './api.js'
|
||||
import CollectionFormElement from './CollectionFormElement.vue'
|
||||
import AddBookToCollection from './AddBookToCollection.vue'
|
||||
import Pagination from './Pagination.vue'
|
||||
@@ -17,6 +17,8 @@ const offset = computed(() => (pageNumber.value - 1) * limit)
|
||||
const data = ref(null)
|
||||
const error = ref(null)
|
||||
|
||||
const itemRefs = useTemplateRef('items')
|
||||
|
||||
const itemIdBeingGrabbed = ref(null)
|
||||
|
||||
const itemIdBeingOvered = ref(null)
|
||||
@@ -40,25 +42,42 @@ function fetchCollection() {
|
||||
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
|
||||
function checkGrabbedPosition(itemId, y) {
|
||||
const itemBeingMoved = itemRefs.value.find((it) => it.id == itemId)
|
||||
const itemMovedY = itemBeingMoved.$el.offsetTop + y + itemBeingMoved.$el.offsetHeight / 2
|
||||
itemRefs.value.forEach((it) => {
|
||||
if (it.$props.id == itemIdBeingGrabbed.value) {
|
||||
return
|
||||
}
|
||||
let lowerDetectionY = it.$el.offsetTop + it.$el.offsetHeight / 2
|
||||
let upperDetectionY = it.$el.offsetTop + (3 * it.$el.offsetHeight) / 2
|
||||
if (it.$props.position < itemBeingMoved.$props.position) {
|
||||
lowerDetectionY = it.$el.offsetTop
|
||||
upperDetectionY = it.$el.offsetTop + it.$el.offsetHeight / 2
|
||||
}
|
||||
if (lowerDetectionY < itemMovedY && itemMovedY < upperDetectionY) {
|
||||
itemIdBeingOvered.value = it.$props.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onStopGrab() {
|
||||
if (itemIdBeingOvered.value != null) {
|
||||
const position = data.value.items.find((it) => it.id == itemIdBeingOvered.value).position
|
||||
changePosition(itemIdBeingGrabbed.value, position)
|
||||
}
|
||||
itemIdBeingGrabbed.value = null
|
||||
itemIdBeingOvered.value = null
|
||||
}
|
||||
|
||||
function isDragoverFromAbove(position) {
|
||||
if (itemIdBeingGrabbed.value == null) {
|
||||
return false
|
||||
}
|
||||
const grabbedItemPosition = data.value.items.find(
|
||||
(it) => it.id == itemIdBeingGrabbed.value,
|
||||
).position
|
||||
return position > grabbedItemPosition
|
||||
}
|
||||
|
||||
function changePosition(id, position) {
|
||||
@@ -73,9 +92,16 @@ function changePosition(id, position) {
|
||||
})
|
||||
}
|
||||
|
||||
function onDragend() {
|
||||
itemIdBeingGrabbed.value = null
|
||||
itemIdBeingOvered.value = null
|
||||
function deleteItem(id) {
|
||||
deleteCollectionItem(id).then((res) => {
|
||||
if (res.ok) {
|
||||
getCollection(data, error, props.id, limit, offset.value)
|
||||
} else {
|
||||
res.json().then((json) => {
|
||||
error.value = json
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -84,20 +110,21 @@ function onDragend() {
|
||||
<div v-if="data">
|
||||
<h2 class="title">{{ data.name }}</h2>
|
||||
<AddBookToCollection :collection-id="props.id" @created="fetchCollection" />
|
||||
<div>
|
||||
<TransitionGroup name="list" tag="div">
|
||||
<CollectionFormElement
|
||||
@drop="onDrop(item.id, item.position)"
|
||||
@dragstart="(e) => onDragStart(e, item.id)"
|
||||
@dragend="onDragend"
|
||||
@dragover="onDragover"
|
||||
@dragenter="itemIdBeingOvered = item.id"
|
||||
@positionchange="(pos) => changePosition(item.id, pos)"
|
||||
@startgrab="itemIdBeingGrabbed = item.id"
|
||||
@grabbing="(y) => checkGrabbedPosition(item.id, y)"
|
||||
@stopgrab="onStopGrab"
|
||||
@delete="deleteItem(item.id)"
|
||||
v-for="item in data.items"
|
||||
:key="item.id"
|
||||
:is-dragover="itemIdBeingOvered === item.id"
|
||||
v-bind="item"
|
||||
ref="items"
|
||||
:is-dragover="item.id == itemIdBeingOvered"
|
||||
:is-dragover-from-above="isDragoverFromAbove(item.position)"
|
||||
/>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
<Pagination
|
||||
class="mt-5"
|
||||
:pageNumber="pageNumber"
|
||||
@@ -108,4 +135,16 @@ function onDragend() {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style></style>
|
||||
<style scoped>
|
||||
.list-move, /* apply transition to moving elements */
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,13 +3,14 @@ import { ref } from 'vue'
|
||||
import BookListElement from './BookListElement.vue'
|
||||
|
||||
const props = defineProps({
|
||||
isDragover: Boolean,
|
||||
id: Number,
|
||||
position: Number,
|
||||
book: Array,
|
||||
isDragover: Boolean,
|
||||
isDragoverFromAbove: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits('positionchange')
|
||||
const emit = defineEmits(['positionchange', 'startgrab', 'stopgrab', 'grabbing', 'delete'])
|
||||
|
||||
const vFocus = {
|
||||
mounted: (el) => el.focus(),
|
||||
@@ -18,6 +19,10 @@ const vFocus = {
|
||||
const isInputtingPosition = ref(false)
|
||||
const inputtedPosition = ref('')
|
||||
|
||||
const initialGrabPosition = ref(null)
|
||||
|
||||
const draggedPosition = ref(null)
|
||||
|
||||
function onPositionInput() {
|
||||
if (inputtedPosition.value != '' && !isNaN(inputtedPosition.value)) {
|
||||
const parsedPosition = parseInt(inputtedPosition.value)
|
||||
@@ -32,12 +37,56 @@ function clearPositionInput() {
|
||||
isInputtingPosition.value = false
|
||||
inputtedPosition.value = ''
|
||||
}
|
||||
|
||||
function clearGrabVariables() {
|
||||
initialGrabPosition.value = null
|
||||
draggedPosition.value = null
|
||||
}
|
||||
|
||||
function onPointerUp() {
|
||||
clearGrabVariables()
|
||||
emit('stopgrab')
|
||||
}
|
||||
|
||||
function onPointerDown(e) {
|
||||
initialGrabPosition.value = e.pageY
|
||||
e.preventDefault()
|
||||
emit('startgrab')
|
||||
}
|
||||
|
||||
function onPointerMove(e) {
|
||||
if (initialGrabPosition.value == null) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
draggedPosition.value = e.pageY - initialGrabPosition.value
|
||||
emit('grabbing', draggedPosition.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="isDragover ? 'dragover' : ''" draggable="true" class="collectionitembox">
|
||||
<div>
|
||||
<div v-if="isDragover && !isDragoverFromAbove" class="dragover" />
|
||||
<div
|
||||
:style="
|
||||
draggedPosition
|
||||
? 'transform: translateY(' + draggedPosition + 'px);position:relative;z-index:3'
|
||||
: ''
|
||||
"
|
||||
ref="collectionitembox"
|
||||
class="collectionitembox"
|
||||
@pointermove="onPointerMove"
|
||||
@pointerup="onPointerUp"
|
||||
@pointerleave="clearGrabVariables"
|
||||
>
|
||||
<BookListElement v-bind="props.book">
|
||||
<div class="separator" />
|
||||
<div class="centered">
|
||||
<template v-slot:left>
|
||||
<div class="is-hidden-desktop align-right">
|
||||
<div @click="$emit('delete')" class="centered closebtn clickable">
|
||||
<b-icon-x />
|
||||
</div>
|
||||
</div>
|
||||
<div class="inputpositionwidget centered">
|
||||
<div
|
||||
v-if="!isInputtingPosition"
|
||||
@click="isInputtingPosition = true"
|
||||
@@ -59,17 +108,27 @@ function clearPositionInput() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="separator" />
|
||||
<div class="positionwidget centered is-narrow">
|
||||
</template>
|
||||
<template v-slot:right>
|
||||
<div class="separator" />
|
||||
<div class="positionwidget centered is-narrow" @pointerdown="onPointerDown">
|
||||
<b-icon-list />
|
||||
</div>
|
||||
<div @click="$emit('delete')" class="is-hidden-touch centered closebtn clickable">
|
||||
<b-icon-x />
|
||||
</div>
|
||||
</template>
|
||||
</BookListElement>
|
||||
</div>
|
||||
<div v-if="isDragover && isDragoverFromAbove" class="dragover" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.collectionitembox {
|
||||
transition: ease-in-out 0.04s;
|
||||
display: flex;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.collectionitembox:hover {
|
||||
@@ -102,10 +161,10 @@ function clearPositionInput() {
|
||||
color: var(--bulma-scheme-main);
|
||||
font-size: 48px;
|
||||
margin-left: 30px;
|
||||
margin-right: 30px;
|
||||
border-top-right-radius: var(--bulma-box-radius);
|
||||
border-bottom-right-radius: var(--bulma-box-radius);
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.positionwidget:active {
|
||||
@@ -116,4 +175,21 @@ function clearPositionInput() {
|
||||
border: 3px solid var(--bulma-primary);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.closebtn {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.positionwidget {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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,36 +17,62 @@ 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 setBookOpacity(index) {
|
||||
if (props.books.length > 7 && index > 3) {
|
||||
return 'opacity: ' + (100 - index * 13) + '%;'
|
||||
function setBookOpacityClass(index) {
|
||||
const length = props.books.length
|
||||
if (length < 5) {
|
||||
return ''
|
||||
} else if (length < 8) {
|
||||
if (index < 4) {
|
||||
return 'opacity-' + index
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
} else {
|
||||
return 'opacity-' + 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 class="bookpreview mx-1" v-for="(book, index) in props.books" :key="book.id">
|
||||
<div
|
||||
:class="index > 3 ? 'is-hidden-touch' : ''"
|
||||
class="bookpreview mx-1"
|
||||
v-for="(book, index) in props.books"
|
||||
:key="book.id"
|
||||
>
|
||||
<img
|
||||
:style="setBookOpacity(index)"
|
||||
:class="setBookOpacityClass(index)"
|
||||
v-bind:src="getImagePathOrDefault(book.coverPath)"
|
||||
v-bind:alt="book.title"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="closeDesktop" class="is-hidden-touch centered closebtn clickable">
|
||||
<b-icon-x />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -63,25 +95,88 @@ img {
|
||||
transition: ease-in-out 0.02s;
|
||||
}
|
||||
|
||||
.namecontainer {
|
||||
flex: 1;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.collectionheader {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.collectionpreviewbooks {
|
||||
flex: 6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.opacity-1 {
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
.opacity-2 {
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
.opacity-3 {
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
.opacity-4 {
|
||||
opacity: 70%;
|
||||
}
|
||||
|
||||
.opacity-5 {
|
||||
opacity: 40%;
|
||||
}
|
||||
|
||||
.opacity-6 {
|
||||
opacity: 20%;
|
||||
}
|
||||
|
||||
.opacity-7 {
|
||||
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: 50px;
|
||||
max-width: 50px;
|
||||
max-height: 75px;
|
||||
max-width: 75px;
|
||||
}
|
||||
.collectionpreviewbooks {
|
||||
flex: 2;
|
||||
flex: 1;
|
||||
margin-top: 20px;
|
||||
justify-content: center;
|
||||
}
|
||||
.collectioncontainer {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.opacity-1 {
|
||||
opacity: 70%;
|
||||
}
|
||||
|
||||
.opacity-2 {
|
||||
opacity: 40%;
|
||||
}
|
||||
|
||||
.opacity-3 {
|
||||
opacity: 10%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -146,6 +146,14 @@ export function postCollectionChangePosition(collectionId, itemId, position) {
|
||||
)
|
||||
}
|
||||
|
||||
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')
|
||||
}
|
||||
@@ -234,6 +242,22 @@ export function genericPayloadCall(apiRoute, object, method) {
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteApiCall(apiRoute) {
|
||||
const { user } = useAuthStore()
|
||||
|
||||
if (user != null) {
|
||||
return fetch(apiRoute, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer ' + user.token,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
export function extractFormErrorFromField(fieldName, errors) {
|
||||
if (errors == null || typeof errors == 'undefined' || !Array.isArray(errors)) {
|
||||
return ''
|
||||
|
||||
45
internal/apitest/delete_collection_element_test.go
Normal file
45
internal/apitest/delete_collection_element_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package apitest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.artlef.fr/bibliomane/internal/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDeleteCollectionBookHandler_DelOk(t *testing.T) {
|
||||
status := testDeleteCollectionBookHandler("23")
|
||||
assert.Equal(t, http.StatusOK, status)
|
||||
_, collection := testGetCollection(t, "6", "10", "0")
|
||||
assert.Equal(t, int64(5), collection.Count)
|
||||
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)
|
||||
assert.Equal(t, uint(5), collection.Items[4].Position)
|
||||
}
|
||||
|
||||
func TestDeleteCollectionBookHandler_NotFound(t *testing.T) {
|
||||
status := testDeleteCollectionBookHandler("425")
|
||||
assert.Equal(t, http.StatusNotFound, status)
|
||||
}
|
||||
|
||||
func TestDeleteCollectionBookHandler_Unauthorized(t *testing.T) {
|
||||
status := testDeleteCollectionBookHandler("11")
|
||||
assert.Equal(t, http.StatusUnauthorized, status)
|
||||
}
|
||||
|
||||
func testDeleteCollectionBookHandler(itemId string) int {
|
||||
router := testutils.TestSetup()
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
token := testutils.ConnectDemoUser(router)
|
||||
req, _ := http.NewRequest("DELETE", "/ws/collection/item/"+itemId, nil)
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
return w.Code
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -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(4), res.Count)
|
||||
assert.Equal(t, 4, len(res.Collections))
|
||||
assert.Equal(t, int64(5), res.Count)
|
||||
assert.Equal(t, 5, len(res.Collections))
|
||||
}
|
||||
|
||||
func testFetchCollections(t *testing.T, limit string, offset string) (int, dto.CollectionItemsGet) {
|
||||
|
||||
@@ -7,5 +7,4 @@ type Collection struct {
|
||||
Name string
|
||||
User User
|
||||
UserID uint
|
||||
Items []CollectionItem
|
||||
}
|
||||
|
||||
@@ -20,6 +20,16 @@ func FetchCollectionHeader(db *gorm.DB, collectionId uint) (CollectionHeader, er
|
||||
return collection, res.Error
|
||||
}
|
||||
|
||||
func FetchCollectionHeaderFromItem(db *gorm.DB, itemId uint) (CollectionHeader, error) {
|
||||
var collection CollectionHeader
|
||||
query := db.Model(&model.Collection{})
|
||||
query = query.Select("collections.name, collections.user_id")
|
||||
query = query.Joins("left join collection_items on (collection_items.collection_id = collections.id)")
|
||||
query = query.Where("collection_items.id = ?", itemId)
|
||||
res := query.Find(&collection)
|
||||
return collection, res.Error
|
||||
}
|
||||
|
||||
type CollectionsQueryResult struct {
|
||||
ID uint
|
||||
UserID uint
|
||||
|
||||
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)
|
||||
|
||||
62
internal/routes/collectionremovebookdelete.go
Normal file
62
internal/routes/collectionremovebookdelete.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/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 DeleteCollectionBookHandler(ac appcontext.AppContext) {
|
||||
collectionItemId, 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 collectionItem model.CollectionItem
|
||||
err = ac.Db.First(&collectionItem, collectionItemId).Error
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
collection, err := query.FetchCollectionHeaderFromItem(ac.Db, collectionItem.ID)
|
||||
|
||||
if collection.UserID != user.ID {
|
||||
err := myvalidator.HttpError{
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
Err: errors.New(i18nresource.GetTranslatedMessage(&ac, "Unauthorized")),
|
||||
}
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = ac.Db.Delete(&collectionItem).Error
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
|
||||
//update position on remaining items
|
||||
q := ac.Db.Model(&model.CollectionItem{})
|
||||
q = q.Where("collection_id = ? AND position > ?", collectionItem.CollectionID, collectionItem.Position)
|
||||
err = q.UpdateColumn("position", gorm.Expr("position - 1")).Error
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
ac.C.JSON(http.StatusOK, "Success")
|
||||
}
|
||||
@@ -102,7 +102,12 @@ 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})
|
||||
})
|
||||
r.Static("/static/bookcover", config.ImageFolderPath)
|
||||
|
||||
folders := []string{"assets", "css", "image"}
|
||||
|
||||
Reference in New Issue
Block a user