mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 08:36:12 +01:00
feat: closes #12902, allow adding users as post editors
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
77
public/src/client/topic/manage-editors.js
Normal file
77
public/src/client/topic/manage-editors.js
Normal 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;
|
||||||
|
});
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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]]');
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
32
src/views/modals/manage-editors.tpl
Normal file
32
src/views/modals/manage-editors.tpl
Normal 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>
|
||||||
Reference in New Issue
Block a user