client: add bulk delete feature (#459)

This introduces a new privilege 'posts:bulk-edit:delete' which by default is given to power users.
This commit is contained in:
Neo 2023-01-19 18:44:31 +01:00 committed by GitHub
parent 8088ff3bbe
commit e3062b1c77
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 174 additions and 1 deletions

View file

@ -114,6 +114,29 @@
&[data-disabled] &[data-disabled]
background: rgba(200, 200, 200, 0.7) background: rgba(200, 200, 200, 0.7)
.delete-flipper
display: inline-block
padding: 0.5em
box-sizing: border-box
border: 0
&:after
display: inline-block
width: 1em
height: 1em
text-align: center
line-height: 1em
font-size: 2.2em
&.delete
background: rgba(255, 0, 0, 0.7)
&:after
color: white
font-family: FontAwesome;
content: "\f1f8"; // fa-trash
&:not(.delete)
background: rgba(200, 200, 200, 0.7)
&:after
color: white
content: '-'
.thumbnail .thumbnail
width: 100% width: 100%
@ -215,7 +238,19 @@
.append .append
@media (max-width: 1000px) @media (max-width: 1000px)
margin-left: 0 margin-left: 0
.bulk-edit-delete
&.opened
.start
@media (max-width: 1000px)
margin-left: 0
&:not(.opened)
.start
display: none
.append.open
@media (max-width: 1000px)
margin-left: 0
.start
margin-left: 1em
.safety .safety
margin-right: 0.25em margin-right: 0.25em
&.safety-safe &.safety-safe

View file

@ -28,4 +28,11 @@
%><a href class='mousetrap button append close'>Stop editing safety</a><% %><a href class='mousetrap button append close'>Stop editing safety</a><%
%></form><% %></form><%
%><% } %><% %><% } %><%
%><% if (ctx.canBulkDelete) { %><%
%><form class='horizontal bulk-edit bulk-edit-delete'><%
%><a href class='mousetrap button append open'>Mass delete</a><%
%><input class='mousetrap start' type='submit' value='Delete selected posts'/><%
%><a href class='mousetrap button append close'>Stop deleting</a><%
%></form><%
%><% } %><%
%></div> %></div>

View file

@ -50,6 +50,10 @@
<% } %> <% } %>
</span> </span>
<% } %> <% } %>
<% if (ctx.canBulkDelete && ctx.parameters && ctx.parameters.delete) { %>
<a href class='delete-flipper'>
</a>
<% } %>
</span> </span>
</li> </li>
<% } %> <% } %>

View file

@ -44,6 +44,7 @@ class PostListController {
enableSafety: api.safetyEnabled(), enableSafety: api.safetyEnabled(),
canBulkEditTags: api.hasPrivilege("posts:bulk-edit:tags"), canBulkEditTags: api.hasPrivilege("posts:bulk-edit:tags"),
canBulkEditSafety: api.hasPrivilege("posts:bulk-edit:safety"), canBulkEditSafety: api.hasPrivilege("posts:bulk-edit:safety"),
canBulkDelete: api.hasPrivilege("posts:bulk-edit:delete"),
bulkEdit: { bulkEdit: {
tags: this._bulkEditTags, tags: this._bulkEditTags,
}, },
@ -52,6 +53,14 @@ class PostListController {
this._evtNavigate(e) this._evtNavigate(e)
); );
this._headerView._bulkDeleteEditor.addEventListener(
"deleteSelectedPosts",
(e) => {
this._evtDeleteSelectedPosts(e);
}
);
this._postsMarkedForDeletion = [];
this._syncPageController(); this._syncPageController();
} }
@ -91,6 +100,38 @@ class PostListController {
e.detail.post.save().catch((error) => window.alert(error.message)); e.detail.post.save().catch((error) => window.alert(error.message));
} }
_evtMarkForDeletion(e) {
const postId = e.detail;
// Add or remove post from delete list
if (e.detail.delete) {
this._postsMarkedForDeletion.push(e.detail.post);
} else {
this._postsMarkedForDeletion = this._postsMarkedForDeletion.filter(
(x) => x.id != e.detail.post.id
);
}
}
_evtDeleteSelectedPosts(e) {
if (this._postsMarkedForDeletion.length == 0) return;
if (
confirm(
`Are you sure you want to delete ${this._postsMarkedForDeletion.length} posts?`
)
) {
Promise.all(
this._postsMarkedForDeletion.map((post) => post.delete())
)
.catch((error) => window.alert(error.message))
.then(() => {
this._postsMarkedForDeletion = [];
this._headerView._navigate();
});
}
}
_syncPageController() { _syncPageController() {
this._pageController.run({ this._pageController.run({
parameters: this._ctx.parameters, parameters: this._ctx.parameters,
@ -117,8 +158,10 @@ class PostListController {
canBulkEditSafety: api.hasPrivilege( canBulkEditSafety: api.hasPrivilege(
"posts:bulk-edit:safety" "posts:bulk-edit:safety"
), ),
canBulkDelete: api.hasPrivilege("posts:bulk-edit:delete"),
bulkEdit: { bulkEdit: {
tags: this._bulkEditTags, tags: this._bulkEditTags,
markedForDeletion: this._postsMarkedForDeletion,
}, },
postFlow: settings.get().postFlow, postFlow: settings.get().postFlow,
}); });
@ -128,6 +171,9 @@ class PostListController {
view.addEventListener("changeSafety", (e) => view.addEventListener("changeSafety", (e) =>
this._evtChangeSafety(e) this._evtChangeSafety(e)
); );
view.addEventListener("markForDeletion", (e) =>
this._evtMarkForDeletion(e)
);
return view; return view;
}, },
}); });

View file

@ -141,6 +141,34 @@ class BulkTagEditor extends BulkEditor {
} }
} }
class BulkDeleteEditor extends BulkEditor {
constructor(hostNode) {
super(hostNode);
this._hostNode.addEventListener("submit", (e) =>
this._evtFormSubmit(e)
);
}
_evtFormSubmit(e) {
e.preventDefault();
this.dispatchEvent(
new CustomEvent("deleteSelectedPosts", { detail: {} })
);
}
_evtOpenLinkClick(e) {
e.preventDefault();
this.toggleOpen(true);
this.dispatchEvent(new CustomEvent("open", { detail: {} }));
}
_evtCloseLinkClick(e) {
e.preventDefault();
this.toggleOpen(false);
this.dispatchEvent(new CustomEvent("close", { detail: {} }));
}
}
class PostsHeaderView extends events.EventTarget { class PostsHeaderView extends events.EventTarget {
constructor(ctx) { constructor(ctx) {
super(); super();
@ -186,6 +214,13 @@ class PostsHeaderView extends events.EventTarget {
this._bulkEditors.push(this._bulkSafetyEditor); this._bulkEditors.push(this._bulkSafetyEditor);
} }
if (this._bulkEditDeleteNode) {
this._bulkDeleteEditor = new BulkDeleteEditor(
this._bulkEditDeleteNode
);
this._bulkEditors.push(this._bulkDeleteEditor);
}
for (let editor of this._bulkEditors) { for (let editor of this._bulkEditors) {
editor.addEventListener("submit", (e) => { editor.addEventListener("submit", (e) => {
this._navigate(); this._navigate();
@ -204,6 +239,8 @@ class PostsHeaderView extends events.EventTarget {
this._openBulkEditor(this._bulkTagEditor); this._openBulkEditor(this._bulkTagEditor);
} else if (ctx.parameters.safety && this._bulkSafetyEditor) { } else if (ctx.parameters.safety && this._bulkSafetyEditor) {
this._openBulkEditor(this._bulkSafetyEditor); this._openBulkEditor(this._bulkSafetyEditor);
} else if (ctx.parameters.delete && this._bulkDeleteEditor) {
this._openBulkEditor(this._bulkDeleteEditor);
} }
} }
@ -227,6 +264,10 @@ class PostsHeaderView extends events.EventTarget {
return this._hostNode.querySelector(".bulk-edit-safety"); return this._hostNode.querySelector(".bulk-edit-safety");
} }
get _bulkEditDeleteNode() {
return this._hostNode.querySelector(".bulk-edit-delete");
}
_openBulkEditor(editor) { _openBulkEditor(editor) {
editor.toggleOpen(true); editor.toggleOpen(true);
this._hideBulkEditorsExcept(editor); this._hideBulkEditorsExcept(editor);
@ -293,6 +334,10 @@ class PostsHeaderView extends events.EventTarget {
this._bulkSafetyEditor && this._bulkSafetyEditor.opened this._bulkSafetyEditor && this._bulkSafetyEditor.opened
? "1" ? "1"
: null; : null;
parameters.delete =
this._bulkDeleteEditor && this._bulkDeleteEditor.opened
? "1"
: null;
this.dispatchEvent( this.dispatchEvent(
new CustomEvent("navigate", { detail: { parameters: parameters } }) new CustomEvent("navigate", { detail: { parameters: parameters } })
); );

View file

@ -39,6 +39,13 @@ class PostsPageView extends events.EventTarget {
); );
} }
} }
const deleteFlipperNode = this._getDeleteFlipperNode(listItemNode);
if (deleteFlipperNode) {
deleteFlipperNode.addEventListener("click", (e) =>
this._evtBulkToggleDeleteClick(e, post)
);
}
} }
this._syncBulkEditorsHighlights(); this._syncBulkEditorsHighlights();
@ -56,6 +63,10 @@ class PostsPageView extends events.EventTarget {
return listItemNode.querySelector(".safety-flipper"); return listItemNode.querySelector(".safety-flipper");
} }
_getDeleteFlipperNode(listItemNode) {
return listItemNode.querySelector(".delete-flipper");
}
_evtPostChange(e) { _evtPostChange(e) {
const listItemNode = this._postIdToListItemNode[e.detail.post.id]; const listItemNode = this._postIdToListItemNode[e.detail.post.id];
for (let node of listItemNode.querySelectorAll("[data-disabled]")) { for (let node of listItemNode.querySelectorAll("[data-disabled]")) {
@ -99,6 +110,20 @@ class PostsPageView extends events.EventTarget {
); );
} }
_evtBulkToggleDeleteClick(e, post) {
e.preventDefault();
const linkNode = e.target;
linkNode.classList.toggle("delete");
this.dispatchEvent(
new CustomEvent("markForDeletion", {
detail: {
post,
delete: linkNode.classList.contains("delete"),
},
})
);
}
_syncBulkEditorsHighlights() { _syncBulkEditorsHighlights() {
for (let listItemNode of this._listItemNodes) { for (let listItemNode of this._listItemNodes) {
const postId = listItemNode.getAttribute("data-post-id"); const postId = listItemNode.getAttribute("data-post-id");
@ -123,6 +148,16 @@ class PostsPageView extends events.EventTarget {
); );
} }
} }
const deleteFlipperNode = this._getDeleteFlipperNode(listItemNode);
if (deleteFlipperNode) {
deleteFlipperNode.classList.toggle(
"delete",
this._ctx.bulkEdit.markedForDeletion.some(
(x) => x.id == postId
)
);
}
} }
} }
} }

View file

@ -115,6 +115,7 @@ privileges:
'posts:favorite': regular 'posts:favorite': regular
'posts:bulk-edit:tags': power 'posts:bulk-edit:tags': power
'posts:bulk-edit:safety': power 'posts:bulk-edit:safety': power
'posts:bulk-edit:delete': power
'tags:create': regular 'tags:create': regular
'tags:edit:names': power 'tags:edit:names': power