feat: closes #12902, allow adding users as post editors

This commit is contained in:
Barış Soner Uşaklı
2024-11-06 11:36:53 -05:00
parent 65f64ebaed
commit bc00df3cd9
8 changed files with 161 additions and 4 deletions

View File

@@ -107,10 +107,10 @@
"nodebb-plugin-ntfy": "1.7.7", "nodebb-plugin-ntfy": "1.7.7",
"nodebb-plugin-spam-be-gone": "2.2.2", "nodebb-plugin-spam-be-gone": "2.2.2",
"nodebb-rewards-essentials": "1.0.0", "nodebb-rewards-essentials": "1.0.0",
"nodebb-theme-harmony": "1.2.77", "nodebb-theme-harmony": "1.2.78",
"nodebb-theme-lavender": "7.1.10", "nodebb-theme-lavender": "7.1.10",
"nodebb-theme-peace": "2.2.8", "nodebb-theme-peace": "2.2.8",
"nodebb-theme-persona": "13.3.40", "nodebb-theme-persona": "13.3.41",
"nodebb-widget-essentials": "7.0.30", "nodebb-widget-essentials": "7.0.30",
"nodemailer": "6.9.16", "nodemailer": "6.9.16",
"nprogress": "0.2.0", "nprogress": "0.2.0",

View File

@@ -30,6 +30,7 @@
"restore": "Restore", "restore": "Restore",
"move": "Move", "move": "Move",
"change-owner": "Change Owner", "change-owner": "Change Owner",
"manage-editors": "Manage Editors",
"fork": "Fork", "fork": "Fork",
"link": "Link", "link": "Link",
"share": "Share", "share": "Share",
@@ -116,6 +117,7 @@
"thread-tools.move-posts": "Move Posts", "thread-tools.move-posts": "Move Posts",
"thread-tools.move-all": "Move All", "thread-tools.move-all": "Move All",
"thread-tools.change-owner": "Change Owner", "thread-tools.change-owner": "Change Owner",
"thread-tools.manage-editors": "Manage Editors",
"thread-tools.select-category": "Select Category", "thread-tools.select-category": "Select Category",
"thread-tools.fork": "Fork Topic", "thread-tools.fork": "Fork Topic",
"thread-tools.tag": "Tag Topic", "thread-tools.tag": "Tag Topic",
@@ -177,6 +179,7 @@
"move-posts-instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", "move-posts-instruction": "Click the posts you want to move then enter a topic ID or go to the target topic",
"move-topic-instruction": "Select the target category and then click move", "move-topic-instruction": "Select the target category and then click move",
"change-owner-instruction": "Click the posts you want to assign to another user", "change-owner-instruction": "Click the posts you want to assign to another user",
"manage-editors-instruction": "Manage the users who can edit this post below.",
"composer.title-placeholder": "Enter your topic title here...", "composer.title-placeholder": "Enter your topic title here...",
"composer.handle-placeholder": "Enter your name/handle here", "composer.handle-placeholder": "Enter your name/handle here",

View File

@@ -0,0 +1,77 @@
'use strict';
define('forum/topic/manage-editors', [
'autocomplete',
'alerts',
], function (autocomplete, alerts) {
const ManageEditors = {};
let modal;
ManageEditors.init = async function (postEl) {
if (modal) {
return;
}
const pid = postEl.attr('data-pid');
let editors = await socket.emit('posts.getEditors', { pid: pid });
app.parseAndTranslate('modals/manage-editors', {
editors: editors,
}, function (html) {
modal = html;
const commitEl = modal.find('#manage_editors_commit');
$('body').append(modal);
modal.find('#manage_editors_cancel').on('click', closeModal);
commitEl.on('click', function () {
saveEditors(pid);
});
autocomplete.user(modal.find('#username'), { filters: ['notbanned'] }, function (ev, ui) {
const isInEditors = editors.find(e => String(e.uid) === String(ui.item.user.uid));
if (!isInEditors) {
editors.push(ui.item.user);
app.parseAndTranslate('modals/manage-editors', 'editors', {
editors: editors,
}, function (html) {
modal.find('[component="topic/editors"]').html(html);
modal.find('#username').val('');
});
}
});
modal.on('click', 'button.remove-user-icon', function () {
const el = $(this).parents('[data-uid]');
const uid = el.attr('data-uid');
editors = editors.filter(e => String(e.uid) === String(uid));
el.remove();
});
});
};
function saveEditors(pid) {
const uids = modal.find('[component="topic/editors"]>[data-uid]')
.map((i, el) => $(el).attr('data-uid')).get();
socket.emit('posts.saveEditors', { pid: pid, uids: uids }, function (err) {
if (err) {
return alerts.error(err);
}
closeModal();
});
}
function closeModal() {
if (modal) {
modal.remove();
modal = null;
}
}
return ManageEditors;
});

View File

@@ -253,6 +253,13 @@ define('forum/topic/postTools', [
}); });
}); });
postContainer.on('click', '[component="post/manage-editors"]', function () {
const btn = $(this);
require(['forum/topic/manage-editors'], function (manageEditors) {
manageEditors.init(btn.parents('[data-pid]'));
});
});
postContainer.on('click', '[component="post/ban-ip"]', function () { postContainer.on('click', '[component="post/ban-ip"]', function () {
const ip = $(this).attr('data-ip'); const ip = $(this).attr('data-ip');
socket.emit('blacklist.addRule', ip, function (err) { socket.emit('blacklist.addRule', ip, function (err) {

View File

@@ -81,6 +81,7 @@ module.exports = function (Posts) {
deleteDiffs(pids), deleteDiffs(pids),
deleteFromUploads(pids), deleteFromUploads(pids),
db.sortedSetsRemove(['posts:pid', 'posts:votes', 'posts:flagged'], pids), db.sortedSetsRemove(['posts:pid', 'posts:votes', 'posts:flagged'], pids),
db.deleteAll(pids.map(pid => `pid:${pid}:editors`)),
]); ]);
await resolveFlags(postData, uid); await resolveFlags(postData, uid);

View File

@@ -3,6 +3,7 @@
const _ = require('lodash'); const _ = require('lodash');
const db = require('../database');
const meta = require('../meta'); const meta = require('../meta');
const posts = require('../posts'); const posts = require('../posts');
const topics = require('../topics'); const topics = require('../topics');
@@ -118,7 +119,8 @@ privsPosts.canEdit = async function (pid, uid) {
const results = await utils.promiseParallel({ const results = await utils.promiseParallel({
isAdmin: user.isAdministrator(uid), isAdmin: user.isAdministrator(uid),
isMod: posts.isModerator([pid], uid), isMod: posts.isModerator([pid], uid),
owner: posts.isOwner(pid, uid), isOwner: posts.isOwner(pid, uid),
isEditor: db.isSetMember(`pid:${pid}:editors`, uid),
edit: privsPosts.can('posts:edit', pid, uid), edit: privsPosts.can('posts:edit', pid, uid),
postData: posts.getPostFields(pid, ['tid', 'timestamp', 'deleted', 'deleterUid']), postData: posts.getPostFields(pid, ['tid', 'timestamp', 'deleted', 'deleterUid']),
userData: user.getUserFields(uid, ['reputation']), userData: user.getUserFields(uid, ['reputation']),
@@ -158,7 +160,10 @@ privsPosts.canEdit = async function (pid, uid) {
results.uid = uid; results.uid = uid;
const result = await plugins.hooks.fire('filter:privileges.posts.edit', results); const result = await plugins.hooks.fire('filter:privileges.posts.edit', results);
return { flag: result.edit && (result.owner || result.isMod), message: '[[error:no-privileges]]' }; return {
flag: result.edit && (result.isOwner || result.isEditor || result.isMod),
message: '[[error:no-privileges]]',
};
}; };
privsPosts.canDelete = async function (pid, uid) { privsPosts.canDelete = async function (pid, uid) {

View File

@@ -46,6 +46,7 @@ module.exports = function (SocketPosts) {
postData.display_moderator_tools = postData.display_edit_tools || postData.display_delete_tools; postData.display_moderator_tools = postData.display_edit_tools || postData.display_delete_tools;
postData.display_move_tools = results.isAdmin || results.isModerator; postData.display_move_tools = results.isAdmin || results.isModerator;
postData.display_change_owner_tools = results.isAdmin || results.isModerator; postData.display_change_owner_tools = results.isAdmin || results.isModerator;
postData.display_manage_editors_tools = results.isAdmin || results.isModerator || postData.selfPost;
postData.display_ip_ban = (results.isAdmin || results.isGlobalMod) && !postData.selfPost; postData.display_ip_ban = (results.isAdmin || results.isGlobalMod) && !postData.selfPost;
postData.display_history = results.history && results.canViewHistory; postData.display_history = results.history && results.canViewHistory;
postData.flags = { postData.flags = {
@@ -92,4 +93,35 @@ module.exports = function (SocketPosts) {
await Promise.all(logs); await Promise.all(logs);
}; };
SocketPosts.getEditors = async function (socket, data) {
if (!data || !data.pid) {
throw new Error('[[error:invalid-data]]');
}
await checkEditorPrivilege(socket.uid, data.pid);
const editorUids = await db.getSetMembers(`pid:${data.pid}:editors`);
const userData = await user.getUsersFields(editorUids, ['username', 'userslug', 'picture']);
return userData;
};
SocketPosts.saveEditors = async function (socket, data) {
if (!data || !data.pid || !Array.isArray(data.uids)) {
throw new Error('[[error:invalid-data]]');
}
await checkEditorPrivilege(socket.uid, data.pid);
await db.delete(`pid:${data.pid}:editors`);
await db.setAdd(`pid:${data.pid}:editors`, data.uids);
};
async function checkEditorPrivilege(uid, pid) {
const cid = await posts.getCidByPid(pid);
const [isAdminOrMod, owner] = await Promise.all([
privileges.categories.isAdminOrMod(cid, uid),
posts.getPostField(pid, 'uid'),
]);
const isSelfPost = String(uid) === String(owner);
if (!isAdminOrMod && !isSelfPost) {
throw new Error('[[error:no-privileges]]');
}
}
}; };

View File

@@ -0,0 +1,32 @@
<div class="card tool-modal shadow">
<h5 class="card-header">[[topic:thread-tools.manage-editors]]</h5>
<div class="card-body">
<p>
[[topic:manage-editors-instruction]]
</p>
<div class="mb-3">
<label class="form-label" for="username"><strong>[[user:username]]</strong></label>
<div class="input-group">
<input id="username" type="text" class="form-control" name="username">
<span class="input-group-text" type="button">
<i class="fa fa-search"></i>
</span>
</div>
</div>
<div class="d-flex flex-wrap" component="topic/editors">
{{{ each editors }}}
<div class="badge text-bg-light m-1 p-1 border d-inline-flex gap-1 align-items-center" data-uid="{./uid}">
{buildAvatar(@value, "24px", true)}
<a href="{config.relative_path}/user/{./userslug}">{./username}</a>
<button class="btn btn-ghost btn-sm p-0 remove-user-icon">
<i class="fa fa-fw fa-times"></i>
</button>
</div>
{{{ end }}}
</div>
</div>
<div class="card-footer text-end">
<button class="btn btn-link btn-sm" id="manage_editors_cancel">[[global:buttons.close]]</button>
<button class="btn btn-primary btn-sm" id="manage_editors_commit">[[global:save]]</button>
</div>
</div>