Implement batch delete

This commit is contained in:
Radhi Fadlillah 2018-02-17 15:51:43 +07:00
parent 124c2845e9
commit 0f4ab6aa38
4 changed files with 168 additions and 44 deletions

View file

@ -29,7 +29,7 @@ var (
router.GET("/api/bookmarks", apiGetBookmarks)
router.POST("/api/bookmarks", apiInsertBookmarks)
router.PUT("/api/bookmarks", apiUpdateBookmarks)
router.DELETE("/api/bookmarks/:id", apiDeleteBookmarks)
router.DELETE("/api/bookmarks", apiDeleteBookmarks)
// Route for panic
router.PanicHandler = func(w http.ResponseWriter, r *http.Request, arg interface{}) {
@ -107,11 +107,13 @@ func apiUpdateBookmarks(w http.ResponseWriter, r *http.Request, ps httprouter.Pa
func apiDeleteBookmarks(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Decode request
id := ps.ByName("id")
// Delete bookmarks
_, _, err := DB.DeleteBookmarks(id)
request := []string{}
err := json.NewDecoder(r.Body).Decode(&request)
checkError(err)
fmt.Fprint(w, id)
// Delete bookmarks
_, _, err = DB.DeleteBookmarks(request...)
checkError(err)
fmt.Fprint(w, request)
}

File diff suppressed because one or more lines are too long

View file

@ -16,25 +16,41 @@
<body>
<div id="app">
<div id="header">
<a id="logo" href="/">
<span></span>shiori</a>
<div id="search-box">
<input type="text" name="keyword" id="input-search" placeholder="Search tags, title or content">
<a class="button">
<i class="fas fa-search fa-fw"></i>
</a>
</div>
<div id="header-menu" v-if="!loading">
<a href="#" @click="loadData">
<i class="fas fa-sync fa-fw"></i>
</a>
</div>
<template v-if="checkedBookmarks.length === 0">
<a id="logo" href="/">
<span></span>shiori</a>
<div id="search-box">
<input type="text" name="keyword" id="input-search" placeholder="Search tags, title or content">
<a class="button">
<i class="fas fa-search fa-fw"></i>
</a>
</div>
<div id="header-menu" v-if="!loading">
<a @click="loadData">
<i class="fas fa-cloud fa-fw"></i> Reload
</a>
</div>
</template>
<template v-else>
<p id="n-selected">{{checkedBookmarks.length}} selected</p>
<div id="header-menu">
<a @click="clearSelectedBookmarks">
<i class="fas fa-fw fa-ban"></i> Cancel
</a>
<a @click="selectAllBookmarks">
<i class="fas fa-fw fa-check-square"></i> Select all
</a>
<a @click="deleteBookmarks(checkedBookmarks)">
<i class="fas fa-fw fa-trash"></i> Delete
</a>
</div>
</template>
</div>
<div id="main">
<template v-if="!loading && error === ''">
<div id="input-bookmark" v-if="!loading">
<div id="input-bookmark">
<p v-if="inputBookmark.url !== ''">{{inputBookmark.id === -1 ? 'New bookmark' : 'Edit bookmark'}}</p>
<input type="text" ref="inputURL" v-model="inputBookmark.url" placeholder="URL for the bookmark">
<input type="text" ref="inputURL" v-model="inputBookmark.url" placeholder="URL for the bookmark" @focus="clearSelectedBookmarks">
<template v-if="inputBookmark.url !== ''">
<input type="text" v-model="inputBookmark.title" placeholder="Custom bookmark title (optional)">
<input type="text" v-model="inputBookmark.tags" placeholder="Space separated tags for this bookmark (optional)">
@ -52,30 +68,33 @@
</div>
</template>
</div>
<div id="grid" v-if="!loading">
<div id="grid">
<div v-for="column in gridColumns" class="column">
<div v-for="item in column" class="bookmark" :ref="'bookmark-'+item.index">
<a href="#" class="checkbox">
<div v-for="item in column" class="bookmark" :class="{checked: isBookmarkChecked(item.index)}" :ref="'bookmark-'+item.index">
<a class="checkbox" @click="toggleBookmarkCheck(item.index)">
<i class="fas fa-check"></i>
</a>
<a class="bookmark-metadata" :class="{'has-image':item.imageURL !== ''}" :style="bookmarkImage(item)" :href="item.url">
<a class="bookmark-metadata" target="_blank" :class="{'has-image':item.imageURL !== ''}" :style="bookmarkImage(item)" :href="item.url">
<p class="bookmark-time">{{bookmarkTime(item)}}</p>
<p class="bookmark-title">{{item.title}}</p>
<p class="bookmark-url">{{getDomainURL(item.url)}}</p>
</a>
<p v-if="item.excerpt !== ''" class="bookmark-excerpt">{{item.excerpt}}</p>
<div v-if="item.tags.length > 0" class="bookmark-tags">
<a v-for="tag in item.tags" href="#">{{tag.name}}</a>
<a v-for="tag in item.tags">{{tag.name}}</a>
</div>
<div class="bookmark-menu">
<a href="#" @click="editBookmark(item.index)">
<i class="fas fa-pencil-alt"></i>
<a @click="updateBookmark(item.index)">
<i class="fas fa-sync"></i> Update
</a>
<a href="#" @click="deleteBookmark(item.index)">
<i class="far fa-trash-alt"></i>
<a @click="editBookmark(item.index)">
<i class="fas fa-pencil-alt"></i> Edit
</a>
<a href="#">
<i class="fas fa-history"></i>
<a @click="deleteBookmarks([item.index])">
<i class="far fa-trash-alt"></i> Delete
</a>
<a>
<i class="fas fa-history"></i> Cache
</a>
</div>
</div>
@ -115,6 +134,7 @@
error: "",
loading: true,
bookmarks: [],
checkedBookmarks: [],
inputBookmark: {
index: -1,
id: -1,
@ -184,7 +204,10 @@
var idx = app.inputBookmark.index;
if (idx === -1) app.bookmarks.unshift(response.data);
else app.bookmarks.splice(idx, 1, response.data);
else {
app.bookmarks.splice(idx, 1, response.data);
app.bookmarks[idx].tags.splice(0, app.bookmarks[idx].tags.length, ...response.data.tags);
}
app.clearInputBookmark();
})
@ -210,31 +233,58 @@
this.inputBookmark.excerpt = bookmark.excerpt;
this.$nextTick(function () {
window.scrollTo(0, 0);
app.$refs.inputURL.focus();
});
},
deleteBookmark: function (idx) {
var bookmark = this.bookmarks[idx];
deleteBookmarks: function (indices) {
var title = "Delete Bookmarks",
content = "Delete the selected bookmark(s) ? This action is irreversible.",
smallestIndex = 1;
if (indices.length === 0) return;
else if (indices.length === 1) {
var bookmark = this.bookmarks[indices[0]];
smallestIndex = indices[0];
title = "Delete Bookmark";
content = "Delete <b>\"" + bookmark.title.trim() + "\"</b> from bookmarks ? This action is irreversible.";
} else {
indices.sort();
smallestIndex = indices[indices.length - 1];
}
this.dialog.visible = true;
this.dialog.isError = false;
this.dialog.loading = false;
this.dialog.title = "Delete Bookmark";
this.dialog.content = "Delete <b>\"" + bookmark.title.trim() + "\"</b> from bookmarks ? This action is irreversible.";
this.dialog.title = title;
this.dialog.content = content;
this.dialog.mainChoice = "Yes";
this.dialog.secondChoice = "No";
this.dialog.mainAction = function () {
app.dialog.loading = true;
instance.delete('/api/bookmarks/' + bookmark.id)
var listId = [];
for (var i = 0; i < indices.length; i++) {
listId.push('' + app.bookmarks[indices[i]].id);
}
instance.delete('/api/bookmarks/', {
data: listId
})
.then(function (response) {
app.dialog.loading = false;
app.dialog.visible = false;
app.bookmarks.splice(idx, 1);
var scrollIdx = idx === 1 ? 1 : idx - 1;
for (var i = indices.length - 1; i >= 0; i--) {
app.bookmarks.splice(indices[i], 1);
}
app.clearSelectedBookmarks();
var scrollIdx = smallestIndex === 1 ? 1 : smallestIndex - 1;
app.$nextTick(function () {
app.$refs['bookmark-' + scrollIdx][0].scrollIntoView();
var el = app.$refs['bookmark-' + scrollIdx];
if (el) el[0].scrollIntoView();
});
})
.catch(function (error) {
@ -245,11 +295,63 @@
this.dialog.secondAction = function () {
app.dialog.visible = false;
app.$nextTick(function () {
app.$refs['bookmark-' + idx][0].scrollIntoView();
app.$refs['bookmark-' + smallestIndex][0].scrollIntoView();
});
};
},
updateBookmark: function (idx) {
var bookmark = this.bookmarks[idx];
this.dialog.visible = true;
this.dialog.isError = false;
this.dialog.loading = false;
this.dialog.title = "Update Bookmark";
this.dialog.content = "Update data of <b>\"" + bookmark.title.trim() + "\"</b> ? This action is irreversible.";
this.dialog.mainChoice = "Yes";
this.dialog.secondChoice = "No";
this.dialog.mainAction = function () {
app.dialog.loading = true;
instance.put('/api/bookmarks', {
id: bookmark.id,
}, {
timeout: 15000,
})
.then(function (response) {
app.dialog.loading = false;
app.dialog.visible = false;
app.bookmarks.splice(idx, 1, response.data);
app.bookmarks[idx].tags.splice(0, app.bookmarks[idx].tags.length, ...response.data.tags);
})
.catch(function (error) {
var errorMsg = error.response ? error.response.data : error.message;
app.showDialogError("Error Updating Bookmark", errorMsg.trim());
});
};
this.dialog.secondAction = function () {
app.dialog.visible = false;
app.$nextTick(function () {
app.$refs['bookmark-' + idx][0].scrollIntoView();
});
};
},
toggleBookmarkCheck: function (idx) {
var checkedIdx = this.checkedBookmarks.indexOf(idx);
if (checkedIdx !== -1) this.checkedBookmarks.splice(checkedIdx, 1);
else this.checkedBookmarks.push(idx);
},
selectAllBookmarks: function () {
this.clearSelectedBookmarks();
for (var i = 0; i < this.bookmarks.length; i++) {
this.checkedBookmarks.push(i);
}
},
clearSelectedBookmarks: function () {
this.checkedBookmarks.splice(0, this.checkedBookmarks.length);
},
isBookmarkChecked: function (idx) {
return this.checkedBookmarks.indexOf(idx) !== -1;
},
clearInputBookmark: function () {
var idx = this.inputBookmark.index;

View file

@ -28,6 +28,14 @@
z-index: 99;
display: flex;
flex-flow: row nowrap;
#n-selected {
line-height: @headerHeight;
font-size: 1.3em;
color: @fontLightColor;
flex: 1 0;
border-right: 1px solid @border;
padding: 0 32px;
}
#logo {
border-left: 1px solid @border;
cursor: default;
@ -84,6 +92,12 @@
color: @linkColor;
font-size: 0.9em;
cursor: pointer;
&:not(:last-child) {
border-right: 1px solid @border;
}
i {
margin-right: 4px;
}
&:hover {
color: @main;
background-color: @appBg;
@ -148,6 +162,7 @@
line-height: 32px;
text-align: center;
display: block;
cursor: pointer;
font-size: 0.9em;
&:hover {
color: @accent !important;
@ -222,6 +237,7 @@
padding: 12px 12px 0;
margin-bottom: -4px;
a {
cursor: pointer;
font-size: 0.9em;
padding: 4px;
color: @main !important;
@ -240,12 +256,16 @@
visibility: hidden;
margin-top: 16px;
a {
cursor: pointer;
display: block;
flex: 1 0;
color: @linkColor !important;
padding: 8px;
font-size: 0.9em;
text-align: center;
i {
margin-right: 4px;
}
&:not(:last-child) {
border-right: 1px solid @border;
}
@ -263,7 +283,7 @@
}
}
&.checked {
border: 0;
border: 1px solid @borderDark;
outline: 6px solid @borderDark;
.checkbox {
opacity: 1;