mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-02-16 19:47:34 +01:00
Post queue write api (#13473)
* move post queue from socket.io to rest api * move harmony post-queue to core add canEdit, allow users to edit their queued posts * fix: openapi spec * lint: whitespace
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- admin
|
- admin
|
||||||
summary: Get flag data
|
summary: Get post queue
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: ""
|
description: ""
|
||||||
@@ -42,6 +42,8 @@ get:
|
|||||||
description: A user identifier
|
description: A user identifier
|
||||||
type:
|
type:
|
||||||
type: string
|
type: string
|
||||||
|
canEdit:
|
||||||
|
type: boolean
|
||||||
data:
|
data:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@@ -202,6 +202,10 @@ paths:
|
|||||||
$ref: 'write/posts/pid/diffs/timestamp.yaml'
|
$ref: 'write/posts/pid/diffs/timestamp.yaml'
|
||||||
/posts/{pid}/replies:
|
/posts/{pid}/replies:
|
||||||
$ref: 'write/posts/pid/replies.yaml'
|
$ref: 'write/posts/pid/replies.yaml'
|
||||||
|
/posts/queue/{id}:
|
||||||
|
$ref: 'write/posts/queue/id.yaml'
|
||||||
|
/posts/queue/{id}/notify:
|
||||||
|
$ref: 'write/posts/queue/notify.yaml'
|
||||||
/chats/:
|
/chats/:
|
||||||
$ref: 'write/chats.yaml'
|
$ref: 'write/chats.yaml'
|
||||||
/chats/unread:
|
/chats/unread:
|
||||||
|
|||||||
92
public/openapi/write/posts/queue/id.yaml
Normal file
92
public/openapi/write/posts/queue/id.yaml
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
post:
|
||||||
|
summary: Accept a queued post
|
||||||
|
tags:
|
||||||
|
- QueuedPosts
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: a valid queued post id
|
||||||
|
example: 2
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: post successfully accepted
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
$ref: ../../../components/schemas/Status.yaml#/Status
|
||||||
|
response:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
pid:
|
||||||
|
type: number
|
||||||
|
tid:
|
||||||
|
type: number
|
||||||
|
'400':
|
||||||
|
description: Bad request, invalid post id
|
||||||
|
delete:
|
||||||
|
summary: Remove a queued post
|
||||||
|
tags:
|
||||||
|
- QueuedPosts
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: 'topic-12345'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Post removed successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
$ref: ../../../components/schemas/Status.yaml#/Status
|
||||||
|
response:
|
||||||
|
type: object
|
||||||
|
'400':
|
||||||
|
description: Bad request, invalid post id
|
||||||
|
put:
|
||||||
|
summary: Edit a queued post
|
||||||
|
tags:
|
||||||
|
- QueuedPosts
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: 'topic-12345'
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
content:
|
||||||
|
type: string
|
||||||
|
example: This is a test reply
|
||||||
|
cid:
|
||||||
|
type: number
|
||||||
|
description: Category ID to which the post belongs
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
description: Updated Post Title
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Post edited successfully
|
||||||
|
'400':
|
||||||
|
description: Bad request, invalid post id
|
||||||
|
|
||||||
|
|
||||||
36
public/openapi/write/posts/queue/notify.yaml
Normal file
36
public/openapi/write/posts/queue/notify.yaml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
post:
|
||||||
|
summary: Notify the owner of a queued post
|
||||||
|
tags:
|
||||||
|
- QueuedPosts
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: a valid queued post id
|
||||||
|
example: 2
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
example: body of the notification message
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: post successfully accepted
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
$ref: ../../../components/schemas/Status.yaml#/Status
|
||||||
|
response:
|
||||||
|
type: object
|
||||||
|
'400':
|
||||||
|
description: Bad request, invalid post id
|
||||||
@@ -69,14 +69,10 @@ define('forum/post-queue', [
|
|||||||
const id = textarea.parents('[data-id]').attr('data-id');
|
const id = textarea.parents('[data-id]').attr('data-id');
|
||||||
const titleEdit = triggerClass === '[data-action="editTitle"]';
|
const titleEdit = triggerClass === '[data-action="editTitle"]';
|
||||||
|
|
||||||
socket.emit('posts.editQueuedContent', {
|
api.put(`/posts/queue/${id}`, {
|
||||||
id: id,
|
|
||||||
title: titleEdit ? textarea.val() : undefined,
|
title: titleEdit ? textarea.val() : undefined,
|
||||||
content: titleEdit ? undefined : textarea.val(),
|
content: titleEdit ? undefined : textarea.val(),
|
||||||
}, function (err, data) {
|
}).then((data) => {
|
||||||
if (err) {
|
|
||||||
return alerts.error(err);
|
|
||||||
}
|
|
||||||
if (titleEdit) {
|
if (titleEdit) {
|
||||||
preview.find('.title-text').text(data.postData.title);
|
preview.find('.title-text').text(data.postData.title);
|
||||||
} else {
|
} else {
|
||||||
@@ -85,7 +81,7 @@ define('forum/post-queue', [
|
|||||||
|
|
||||||
textarea.parent().addClass('hidden');
|
textarea.parent().addClass('hidden');
|
||||||
preview.removeClass('hidden');
|
preview.removeClass('hidden');
|
||||||
});
|
}).catch(alerts.error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,8 +92,7 @@ define('forum/post-queue', [
|
|||||||
onSubmit: function (selectedCategory) {
|
onSubmit: function (selectedCategory) {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
api.get(`/categories/${selectedCategory.cid}`, {}),
|
api.get(`/categories/${selectedCategory.cid}`, {}),
|
||||||
socket.emit('posts.editQueuedContent', {
|
api.put(`/posts/queue/${id}`, {
|
||||||
id: id,
|
|
||||||
cid: selectedCategory.cid,
|
cid: selectedCategory.cid,
|
||||||
}),
|
}),
|
||||||
]).then(function (result) {
|
]).then(function (result) {
|
||||||
@@ -174,6 +169,35 @@ define('forum/post-queue', [
|
|||||||
|
|
||||||
async function handleQueueActions() {
|
async function handleQueueActions() {
|
||||||
// accept, reject, notify
|
// accept, reject, notify
|
||||||
|
|
||||||
|
const parent = $(this).parents('[data-id]');
|
||||||
|
const action = $(this).attr('data-action');
|
||||||
|
const id = parent.attr('data-id');
|
||||||
|
const listContainer = parent.get(0).parentNode;
|
||||||
|
|
||||||
|
if ((!['accept', 'reject', 'notify'].includes(action)) ||
|
||||||
|
(action === 'reject' && !await confirmReject(ajaxify.data.canAccept ? '[[post-queue:confirm-reject]]' : '[[post-queue:confirm-remove]]'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
doAction(action, id).then(function () {
|
||||||
|
if (action === 'accept' || action === 'reject') {
|
||||||
|
parent.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listContainer.childElementCount === 0) {
|
||||||
|
if (ajaxify.data.singlePost) {
|
||||||
|
ajaxify.go('/post-queue' + window.location.search);
|
||||||
|
} else {
|
||||||
|
ajaxify.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(alerts.error);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doAction(action, id) {
|
||||||
function getMessage() {
|
function getMessage() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const modal = bootbox.dialog({
|
const modal = bootbox.dialog({
|
||||||
@@ -194,36 +218,16 @@ define('forum/post-queue', [
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const parent = $(this).parents('[data-id]');
|
const actionsMap = {
|
||||||
const action = $(this).attr('data-action');
|
accept: () => api.post(`/posts/queue/${id}`, {}),
|
||||||
const id = parent.attr('data-id');
|
reject: () => api.del(`/posts/queue/${id}`, {}),
|
||||||
const listContainer = parent.get(0).parentNode;
|
notify: async () => api.post(`/posts/queue/${id}/notify`, { message: await getMessage() }),
|
||||||
|
};
|
||||||
if ((!['accept', 'reject', 'notify'].includes(action)) ||
|
if (actionsMap[action]) {
|
||||||
(action === 'reject' && !await confirmReject(ajaxify.data.canAccept ? '[[post-queue:confirm-reject]]' : '[[post-queue:confirm-remove]]'))) {
|
const result = actionsMap[action]();
|
||||||
return;
|
return (result instanceof Promise ? result : Promise.resolve(result));
|
||||||
}
|
}
|
||||||
|
throw new Error(`Unknown action: ${action}`);
|
||||||
socket.emit('posts.' + action, {
|
|
||||||
id: id,
|
|
||||||
message: action === 'notify' ? await getMessage() : undefined,
|
|
||||||
}, function (err) {
|
|
||||||
if (err) {
|
|
||||||
return alerts.error(err);
|
|
||||||
}
|
|
||||||
if (action === 'accept' || action === 'reject') {
|
|
||||||
parent.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (listContainer.childElementCount === 0) {
|
|
||||||
if (ajaxify.data.singlePost) {
|
|
||||||
ajaxify.go('/post-queue' + window.location.search);
|
|
||||||
} else {
|
|
||||||
ajaxify.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBulkActions() {
|
function handleBulkActions() {
|
||||||
@@ -244,7 +248,7 @@ define('forum/post-queue', [
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const action = bulkAction.split('-')[0];
|
const action = bulkAction.split('-')[0];
|
||||||
const promises = ids.map(id => socket.emit('posts.' + action, { id: id }));
|
const promises = ids.map(id => doAction(action, id));
|
||||||
|
|
||||||
Promise.allSettled(promises).then(function (results) {
|
Promise.allSettled(promises).then(function (results) {
|
||||||
const fulfilled = results.filter(res => res.status === 'fulfilled').length;
|
const fulfilled = results.filter(res => res.status === 'fulfilled').length;
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ const activitypub = require('../activitypub');
|
|||||||
const apiHelpers = require('./helpers');
|
const apiHelpers = require('./helpers');
|
||||||
const websockets = require('../socket.io');
|
const websockets = require('../socket.io');
|
||||||
const socketHelpers = require('../socket.io/helpers');
|
const socketHelpers = require('../socket.io/helpers');
|
||||||
|
const translator = require('../translator');
|
||||||
|
const notifications = require('../notifications');
|
||||||
|
|
||||||
const postsAPI = module.exports;
|
const postsAPI = module.exports;
|
||||||
|
|
||||||
@@ -574,3 +576,91 @@ postsAPI.getReplies = async (caller, { pid }) => {
|
|||||||
|
|
||||||
return postData;
|
return postData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
postsAPI.acceptQueuedPost = async (caller, data) => {
|
||||||
|
await canEditQueue(caller.uid, data, 'accept');
|
||||||
|
const result = await posts.submitFromQueue(data.id);
|
||||||
|
if (result && caller.uid !== parseInt(result.uid, 10)) {
|
||||||
|
await sendQueueNotification('post-queue-accepted', result.uid, `/post/${result.pid}`);
|
||||||
|
}
|
||||||
|
await logQueueEvent(caller, result, 'accept');
|
||||||
|
return { type: result.type, pid: result.pid, tid: result.tid };
|
||||||
|
};
|
||||||
|
|
||||||
|
postsAPI.removeQueuedPost = async (caller, data) => {
|
||||||
|
await canEditQueue(caller.uid, data, 'reject');
|
||||||
|
const result = await posts.removeFromQueue(data.id);
|
||||||
|
if (result && caller.uid !== parseInt(result.uid, 10)) {
|
||||||
|
await sendQueueNotification('post-queue-rejected', result.uid, '/');
|
||||||
|
}
|
||||||
|
await logQueueEvent(caller, result, 'reject');
|
||||||
|
};
|
||||||
|
|
||||||
|
postsAPI.editQueuedPost = async (caller, data) => {
|
||||||
|
if (!data || !data.id || (!data.content && !data.title && !data.cid)) {
|
||||||
|
throw new Error('[[error:invalid-data]]');
|
||||||
|
}
|
||||||
|
await posts.editQueuedContent(caller.uid, data);
|
||||||
|
if (data.content) {
|
||||||
|
return await plugins.hooks.fire('filter:parse.post', { postData: data });
|
||||||
|
}
|
||||||
|
return { postData: data };
|
||||||
|
};
|
||||||
|
|
||||||
|
postsAPI.notifyQueuedPostOwner = async (caller, data) => {
|
||||||
|
await canEditQueue(caller.uid, data, 'notify');
|
||||||
|
const result = await posts.getFromQueue(data.id);
|
||||||
|
if (result) {
|
||||||
|
await sendQueueNotification('post-queue-notify', result.uid, `/post-queue/${data.id}`, validator.escape(String(data.message)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function canEditQueue(uid, data, action) {
|
||||||
|
const [canEditQueue, queuedPost] = await Promise.all([
|
||||||
|
posts.canEditQueue(uid, data, action),
|
||||||
|
posts.getFromQueue(data.id),
|
||||||
|
]);
|
||||||
|
if (!queuedPost) {
|
||||||
|
throw new Error('[[error:no-post]]');
|
||||||
|
}
|
||||||
|
if (!canEditQueue) {
|
||||||
|
throw new Error('[[error:no-privileges]]');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logQueueEvent(caller, result, type) {
|
||||||
|
const eventData = {
|
||||||
|
type: `post-queue-${result.type}-${type}`,
|
||||||
|
uid: caller.uid,
|
||||||
|
ip: caller.ip,
|
||||||
|
content: result.data.content,
|
||||||
|
targetUid: result.uid,
|
||||||
|
};
|
||||||
|
if (result.type === 'topic') {
|
||||||
|
eventData.cid = result.data.cid;
|
||||||
|
eventData.title = result.data.title;
|
||||||
|
} else {
|
||||||
|
eventData.tid = result.data.tid;
|
||||||
|
}
|
||||||
|
if (result.pid) {
|
||||||
|
eventData.pid = result.pid;
|
||||||
|
}
|
||||||
|
await events.log(eventData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendQueueNotification(type, targetUid, path, notificationText) {
|
||||||
|
const bodyShort = notificationText ?
|
||||||
|
translator.compile(`notifications:${type}`, notificationText) :
|
||||||
|
translator.compile(`notifications:${type}`);
|
||||||
|
const notifData = {
|
||||||
|
type: type,
|
||||||
|
nid: `${type}-${targetUid}-${path}`,
|
||||||
|
bodyShort: bodyShort,
|
||||||
|
path: path,
|
||||||
|
};
|
||||||
|
if (parseInt(meta.config.postQueueNotificationUid, 10) > 0) {
|
||||||
|
notifData.from = meta.config.postQueueNotificationUid;
|
||||||
|
}
|
||||||
|
const notifObj = await notifications.create(notifData);
|
||||||
|
await notifications.push(notifObj, [targetUid]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -237,6 +237,7 @@ modsController.postQueue = async function (req, res, next) {
|
|||||||
.map((post) => {
|
.map((post) => {
|
||||||
const isSelf = post.user.uid === req.uid;
|
const isSelf = post.user.uid === req.uid;
|
||||||
post.canAccept = !isSelf && (isAdmin || isGlobalMod || !!moderatedCids.length);
|
post.canAccept = !isSelf && (isAdmin || isGlobalMod || !!moderatedCids.length);
|
||||||
|
post.canEdit = isSelf || isAdmin || isGlobalMod;
|
||||||
return post;
|
return post;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -189,3 +189,24 @@ Posts.getReplies = async (req, res) => {
|
|||||||
|
|
||||||
helpers.formatApiResponse(200, res, { replies });
|
helpers.formatApiResponse(200, res, { replies });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Posts.acceptQueuedPost = async (req, res) => {
|
||||||
|
const post = await api.posts.acceptQueuedPost(req, { id: req.params.id });
|
||||||
|
helpers.formatApiResponse(200, res, { post });
|
||||||
|
};
|
||||||
|
|
||||||
|
Posts.removeQueuedPost = async (req, res) => {
|
||||||
|
await api.posts.removeQueuedPost(req, { id: req.params.id });
|
||||||
|
helpers.formatApiResponse(200, res);
|
||||||
|
};
|
||||||
|
|
||||||
|
Posts.editQueuedPost = async (req, res) => {
|
||||||
|
const result = await api.posts.editQueuedPost(req, { id: req.params.id, ...req.body });
|
||||||
|
helpers.formatApiResponse(200, res, result);
|
||||||
|
};
|
||||||
|
|
||||||
|
Posts.notifyQueuedPostOwner = async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
await api.posts.notifyQueuedPostOwner(req, { id, message: req.body.message });
|
||||||
|
helpers.formatApiResponse(200, res);
|
||||||
|
};
|
||||||
@@ -307,9 +307,11 @@ module.exports = function (Posts) {
|
|||||||
if (data.type === 'topic') {
|
if (data.type === 'topic') {
|
||||||
const result = await createTopic(data.data);
|
const result = await createTopic(data.data);
|
||||||
data.pid = result.postData.pid;
|
data.pid = result.postData.pid;
|
||||||
|
data.tid = result.topicData.tid;
|
||||||
} else if (data.type === 'reply') {
|
} else if (data.type === 'reply') {
|
||||||
const result = await createReply(data.data);
|
const result = await createReply(data.data);
|
||||||
data.pid = result.pid;
|
data.pid = result.pid;
|
||||||
|
data.tid = result.tid;
|
||||||
}
|
}
|
||||||
await removeFromQueue(id);
|
await removeFromQueue(id);
|
||||||
plugins.hooks.fire('action:post-queue:submitFromQueue', { data: data });
|
plugins.hooks.fire('action:post-queue:submitFromQueue', { data: data });
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ module.exports = function () {
|
|||||||
|
|
||||||
setupApiRoute(router, 'get', '/:pid/replies', [middleware.assert.post], controllers.write.posts.getReplies);
|
setupApiRoute(router, 'get', '/:pid/replies', [middleware.assert.post], controllers.write.posts.getReplies);
|
||||||
|
|
||||||
|
setupApiRoute(router, 'post', '/queue/:id', controllers.write.posts.acceptQueuedPost);
|
||||||
|
setupApiRoute(router, 'delete', '/queue/:id', controllers.write.posts.removeQueuedPost);
|
||||||
|
setupApiRoute(router, 'put', '/queue/:id', controllers.write.posts.editQueuedPost);
|
||||||
|
setupApiRoute(router, 'post', '/queue/:id/notify', [middleware.checkRequired.bind(null, ['message'])], controllers.write.posts.notifyQueuedPostOwner);
|
||||||
|
|
||||||
|
|
||||||
// Shorthand route to access post routes by topic index
|
// Shorthand route to access post routes by topic index
|
||||||
router.all('/+byIndex/:index*?', [middleware.checkRequired.bind(null, ['tid'])], controllers.write.posts.redirectByIndex);
|
router.all('/+byIndex/:index*?', [middleware.checkRequired.bind(null, ['tid'])], controllers.write.posts.redirectByIndex);
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const validator = require('validator');
|
|
||||||
|
|
||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
const posts = require('../posts');
|
const posts = require('../posts');
|
||||||
const privileges = require('../privileges');
|
const privileges = require('../privileges');
|
||||||
const plugins = require('../plugins');
|
|
||||||
const meta = require('../meta');
|
|
||||||
const topics = require('../topics');
|
const topics = require('../topics');
|
||||||
const notifications = require('../notifications');
|
|
||||||
const utils = require('../utils');
|
const utils = require('../utils');
|
||||||
const events = require('../events');
|
|
||||||
const translator = require('../translator');
|
|
||||||
|
|
||||||
const api = require('../api');
|
const api = require('../api');
|
||||||
const sockets = require('.');
|
const sockets = require('.');
|
||||||
|
|
||||||
@@ -99,90 +91,23 @@ SocketPosts.getReplies = async function (socket, pid) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
SocketPosts.accept = async function (socket, data) {
|
SocketPosts.accept = async function (socket, data) {
|
||||||
await canEditQueue(socket, data, 'accept');
|
sockets.warnDeprecated(socket, 'POST /api/v3/posts/queue/:id');
|
||||||
const result = await posts.submitFromQueue(data.id);
|
await api.posts.acceptQueuedPost(socket, data);
|
||||||
if (result && socket.uid !== parseInt(result.uid, 10)) {
|
|
||||||
await sendQueueNotification('post-queue-accepted', result.uid, `/post/${result.pid}`);
|
|
||||||
}
|
|
||||||
await logQueueEvent(socket, result, 'accept');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
SocketPosts.reject = async function (socket, data) {
|
SocketPosts.reject = async function (socket, data) {
|
||||||
await canEditQueue(socket, data, 'reject');
|
sockets.warnDeprecated(socket, 'DELETE /api/v3/posts/queue/:id');
|
||||||
const result = await posts.removeFromQueue(data.id);
|
await api.posts.removeQueuedPost(socket, data);
|
||||||
if (result && socket.uid !== parseInt(result.uid, 10)) {
|
|
||||||
await sendQueueNotification('post-queue-rejected', result.uid, '/');
|
|
||||||
}
|
|
||||||
await logQueueEvent(socket, result, 'reject');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async function logQueueEvent(socket, result, type) {
|
|
||||||
const eventData = {
|
|
||||||
type: `post-queue-${result.type}-${type}`,
|
|
||||||
uid: socket.uid,
|
|
||||||
ip: socket.ip,
|
|
||||||
content: result.data.content,
|
|
||||||
targetUid: result.uid,
|
|
||||||
};
|
|
||||||
if (result.type === 'topic') {
|
|
||||||
eventData.cid = result.data.cid;
|
|
||||||
eventData.title = result.data.title;
|
|
||||||
} else {
|
|
||||||
eventData.tid = result.data.tid;
|
|
||||||
}
|
|
||||||
if (result.pid) {
|
|
||||||
eventData.pid = result.pid;
|
|
||||||
}
|
|
||||||
await events.log(eventData);
|
|
||||||
}
|
|
||||||
|
|
||||||
SocketPosts.notify = async function (socket, data) {
|
SocketPosts.notify = async function (socket, data) {
|
||||||
await canEditQueue(socket, data, 'notify');
|
sockets.warnDeprecated(socket, 'POST /api/v3/posts/queue/:id/notify');
|
||||||
const result = await posts.getFromQueue(data.id);
|
await api.posts.notifyQueuedPostOwner(socket, data);
|
||||||
if (result) {
|
|
||||||
await sendQueueNotification('post-queue-notify', result.uid, `/post-queue/${data.id}`, validator.escape(String(data.message)));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async function canEditQueue(socket, data, action) {
|
|
||||||
const [canEditQueue, queuedPost] = await Promise.all([
|
|
||||||
posts.canEditQueue(socket.uid, data, action),
|
|
||||||
posts.getFromQueue(data.id),
|
|
||||||
]);
|
|
||||||
if (!queuedPost) {
|
|
||||||
throw new Error('[[error:no-post]]');
|
|
||||||
}
|
|
||||||
if (!canEditQueue) {
|
|
||||||
throw new Error('[[error:no-privileges]]');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendQueueNotification(type, targetUid, path, notificationText) {
|
|
||||||
const bodyShort = notificationText ?
|
|
||||||
translator.compile(`notifications:${type}`, notificationText) :
|
|
||||||
translator.compile(`notifications:${type}`);
|
|
||||||
const notifData = {
|
|
||||||
type: type,
|
|
||||||
nid: `${type}-${targetUid}-${path}`,
|
|
||||||
bodyShort: bodyShort,
|
|
||||||
path: path,
|
|
||||||
};
|
|
||||||
if (parseInt(meta.config.postQueueNotificationUid, 10) > 0) {
|
|
||||||
notifData.from = meta.config.postQueueNotificationUid;
|
|
||||||
}
|
|
||||||
const notifObj = await notifications.create(notifData);
|
|
||||||
await notifications.push(notifObj, [targetUid]);
|
|
||||||
}
|
|
||||||
|
|
||||||
SocketPosts.editQueuedContent = async function (socket, data) {
|
SocketPosts.editQueuedContent = async function (socket, data) {
|
||||||
if (!data || !data.id || (!data.content && !data.title && !data.cid)) {
|
sockets.warnDeprecated(socket, 'PUT /api/v3/posts/queue/:id');
|
||||||
throw new Error('[[error:invalid-data]]');
|
return await api.posts.editQueuedPost(socket, data);
|
||||||
}
|
|
||||||
await posts.editQueuedContent(socket.uid, data);
|
|
||||||
if (data.content) {
|
|
||||||
return await plugins.hooks.fire('filter:parse.post', { postData: data });
|
|
||||||
}
|
|
||||||
return { postData: data };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
require('../promisify')(SocketPosts);
|
require('../promisify')(SocketPosts);
|
||||||
|
|||||||
@@ -1,92 +1,158 @@
|
|||||||
{{{ if isAdmin }}}
|
<div class="flex-fill">
|
||||||
{{{ if !enabled }}}
|
{{{ if isAdmin }}}
|
||||||
<div class="alert alert-info">
|
{{{ if !enabled }}}
|
||||||
[[post-queue:enabling-help, {config.relative_path}/admin/settings/post#post-queue]]
|
<div class="alert alert-info">
|
||||||
</div>
|
[[post-queue:enabling-help, {config.relative_path}/admin/settings/post#post-queue]]
|
||||||
{{{ end }}}
|
|
||||||
{{{ else }}}
|
|
||||||
<div>
|
|
||||||
<p class="lead">[[post-queue:public-intro]]</p>
|
|
||||||
<p>[[post-queue:public-description]]</p>
|
|
||||||
<hr />
|
|
||||||
</div>
|
|
||||||
{{{ end }}}
|
|
||||||
|
|
||||||
{{{ if (!singlePost && posts.length) }}}
|
|
||||||
<div class="btn-toolbar justify-content-end">
|
|
||||||
<div class="me-2">
|
|
||||||
<!-- IMPORT partials/category/filter-dropdown-right.tpl -->
|
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group bottom-sheet" component="post-queue/bulk-actions">
|
{{{ end }}}
|
||||||
<button type="button" class="btn btn-ghost btn-sm dropdown-toggle d-flex gap-2 align-items-center" data-bs-toggle="dropdown" autocomplete="off" aria-haspopup="true" aria-expanded="false">
|
{{{ else }}}
|
||||||
<i class="fa fa-clone text-primary"></i>
|
<div>
|
||||||
<span class="fw-semibold">[[post-queue:bulk-actions]]</span>
|
<p class="lead">[[post-queue:public-intro]]</p>
|
||||||
</button>
|
<p>[[post-queue:public-description]]</p>
|
||||||
<ul class="dropdown-menu dropdown-menu-end p-1" role="menu">
|
<hr />
|
||||||
{{{ if canAccept }}}
|
|
||||||
<li><a class="dropdown-item rounded-1" href="#" data-action="accept-all" role="menuitem">[[post-queue:accept-all]]</a></li>
|
|
||||||
<li><a class="dropdown-item rounded-1" href="#" data-action="accept-selected" role="menuitem">[[post-queue:accept-selected]]</a></li>
|
|
||||||
<li class="dropdown-divider"></li>
|
|
||||||
<li><a class="dropdown-item rounded-1" href="#" data-action="reject-all" role="menuitem">[[post-queue:reject-all]]</a></li>
|
|
||||||
<li><a class="dropdown-item rounded-1" href="#" data-action="reject-selected" role="menuitem">[[post-queue:reject-selected]]</a></li>
|
|
||||||
{{{ else }}}
|
|
||||||
<li><a class="dropdown-item rounded-1" href="#" data-action="reject-all">[[post-queue:remove-all]]</a></li>
|
|
||||||
<li><a class="dropdown-item rounded-1" href="#" data-action="reject-selected" role="menuitem">[[post-queue:remove-selected]]</a></li>
|
|
||||||
{{{ end }}}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{{{ end }}}
|
||||||
|
|
||||||
<hr/>
|
{{{ if (!singlePost && posts.length) }}}
|
||||||
{{{ end }}}
|
<div class="btn-toolbar justify-content-end mb-3">
|
||||||
|
<div class="me-2">
|
||||||
|
<!-- IMPORT partials/category/filter-dropdown-right.tpl -->
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="btn-group bottom-sheet" component="post-queue/bulk-actions">
|
||||||
<div class="col-12">
|
<button type="button" class="btn btn-ghost btn-sm ff-secondary dropdown-toggle d-flex align-items-center gap-2" data-bs-toggle="dropdown" autocomplete="off" aria-haspopup="true" aria-expanded="false">
|
||||||
<div class="post-queue preventSlideout posts-list">
|
<i class="fa fa-clone text-primary"></i><span class="fw-semibold"> [[post-queue:bulk-actions]]</span>
|
||||||
{{{ if !posts.length }}}
|
</button>
|
||||||
{{{ if !singlePost }}}
|
<ul class="dropdown-menu p-1 text-sm dropdown-menu-end" role="menu">
|
||||||
<div class="mx-auto">
|
{{{ if canAccept }}}
|
||||||
<div class="d-flex flex-column gap-3 justify-content-center text-center">
|
<li><a class="dropdown-item rounded-1" href="#" data-action="accept-all" role="menuitem">[[post-queue:accept-all]]</a></li>
|
||||||
<div class="mx-auto p-4 bg-light border rounded">
|
<li><a class="dropdown-item rounded-1" href="#" data-action="accept-selected" role="menuitem">[[post-queue:accept-selected]]</a></li>
|
||||||
<i class="text-secondary fa fa-fw fa-4x fa-seedling"></i>
|
<li class="dropdown-divider"></li>
|
||||||
</div>
|
<li><a class="dropdown-item rounded-1" href="#" data-action="reject-all" role="menuitem">[[post-queue:reject-all]]</a></li>
|
||||||
[[post-queue:no-queued-posts]]
|
<li><a class="dropdown-item rounded-1" href="#" data-action="reject-selected" role="menuitem">[[post-queue:reject-selected]]</a></li>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{{ else }}}
|
{{{ else }}}
|
||||||
<div class="alert alert-info d-flex align-items-md-center d-flex flex-column flex-md-row">
|
<li><a class="dropdown-item rounded-1" href="#" data-action="reject-all" role="menuitem">[[post-queue:remove-all]]</a></li>
|
||||||
<p class="mb-md-0">[[post-queue:no-single-post]]</p>
|
<li><a class="dropdown-item rounded-1" href="#" data-action="reject-selected" role="menuitem">[[post-queue:remove-selected]]</a></li>
|
||||||
<div class="d-grid ms-md-auto">
|
|
||||||
<a class="btn btn-sm btn-primary flex-shrink text-nowrap" href=".">[[post-queue:back-to-list]]</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{{ end }}}
|
{{{ end }}}
|
||||||
{{{ end }}}
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{{ end }}}
|
||||||
|
|
||||||
{{{ each posts }}}
|
<div class="post-queue posts-list">
|
||||||
<div class="card mb-3" data-id="{./id}"data-uid="{./user.uid}">
|
{{{ if !posts.length }}}
|
||||||
<div class="card-header">
|
{{{ if !singlePost }}}
|
||||||
{{{ if !singlePost }}}
|
<div class="mx-auto">
|
||||||
<input type="checkbox" class="form-check-input" autocomplete="off" />
|
<div class="d-flex flex-column gap-3 justify-content-center text-center">
|
||||||
{{{ end }}}
|
<div class="mx-auto p-4 bg-light border rounded">
|
||||||
<strong>{{{ if posts.data.tid }}}[[post-queue:reply]]{{{ else }}}[[post-queue:topic]]{{{ end }}}</strong>
|
<i class="text-secondary fa fa-fw fa-4x fa-seedling"></i>
|
||||||
<span class="timeago float-end" title={posts.data.timestampISO}></span>
|
</div>
|
||||||
|
[[post-queue:no-queued-posts]]
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
</div>
|
||||||
<div class="row">
|
{{{ else }}}
|
||||||
<div class="col-lg-2 col-12">
|
<div class="alert alert-info d-flex align-items-md-center d-flex flex-column flex-md-row">
|
||||||
<strong>[[post-queue:user]]</strong>
|
<p class="mb-md-0">[[post-queue:no-single-post]]</p>
|
||||||
<div>
|
<div class="d-grid ms-md-auto">
|
||||||
|
<a class="btn btn-sm btn-primary flex-shrink text-nowrap" href=".">[[post-queue:back-to-list]]</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{{ end }}}
|
||||||
|
{{{ end }}}
|
||||||
|
|
||||||
|
{{{ each posts }}}
|
||||||
|
<div class="card mb-4" data-id="{./id}" data-uid="{./user.uid}">
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-lg-3 bg-card-cap rounded-start">
|
||||||
|
<ul class="list-unstyled ps-0 mb-0 border-end h-100">
|
||||||
|
<li class="card-body border-bottom position-relative">
|
||||||
|
{{{ if !singlePost }}}
|
||||||
|
<input id="{./id}" type="checkbox" class="form-check-input" autocomplete="off" />
|
||||||
|
{{{ end }}}
|
||||||
|
<label for="{./id}" class="small stretched-link">
|
||||||
|
{{{ if posts.data.tid }}}[[post-queue:reply]]{{{ else }}}[[post-queue:topic]]{{{ end }}}
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li class="card-body d-flex flex-column gap-1 border-bottom">
|
||||||
|
<div class="d-flex text-xs fw-semibold align-items-center">
|
||||||
|
[[post-queue:user]]
|
||||||
|
{{{ if ((privileges.ban || privileges.mute) || privileges.admin:users) }}}
|
||||||
|
<div class="ms-auto btn-group bottom-sheet">
|
||||||
|
<button href="#" class="btn btn-ghost btn-sm ff-secondary border text-xs dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">[[global:actions]]</button>
|
||||||
|
<ul class="dropdown-menu p-1 text-sm" role="menu">
|
||||||
|
{{{ if privileges.view:users:info }}}
|
||||||
|
<li><a class="dropdown-item rounded-1" href="{config.relative_path}/user/{./user.userslug}/info" role="menuitem">[[user:account-info]]</a></li>
|
||||||
|
{{{ end }}}
|
||||||
|
{{{ if privileges.ban }}}
|
||||||
|
<li class="{{{ if target.user.banned }}}hidden{{{ end }}}"><a class="dropdown-item rounded-1" href="#" data-action="ban" role="menuitem">[[user:ban-account]]</a></li>
|
||||||
|
<li class="{{{ if !target.user.banned }}}hidden{{{ end }}}"><a class="dropdown-item rounded-1" href="#" data-action="unban" role="menuitem">[[user:unban-account]]</a></li>
|
||||||
|
{{{ end }}}
|
||||||
|
{{{ if privileges.mute}}}
|
||||||
|
<li class="{{{ if target.user.muted }}}hidden{{{ end }}}"><a class="dropdown-item rounded-1" href="#" data-action="mute" role="menuitem">[[user:mute-account]]</a></li>
|
||||||
|
<li class="{{{ if !target.user.muted }}}hidden{{{ end }}}"><a class="dropdown-item rounded-1" href="#" data-action="unmute" role="menuitem">[[user:unmute-account]]</a></li>
|
||||||
|
{{{ end }}}
|
||||||
|
{{{ if privileges.admin:users }}}
|
||||||
|
<li><a class="dropdown-item rounded-1" href="#" data-action="delete-account" role="menuitem">[[user:delete-account-as-admin]]</a></li>
|
||||||
|
<li><a class="dropdown-item rounded-1" href="#" data-action="delete-content" role="menuitem">[[user:delete-content]]</a></li>
|
||||||
|
<li><a class="dropdown-item rounded-1" href="#" data-action="delete-all" role="menuitem">[[user:delete-all]]</a></li>
|
||||||
|
{{{ end }}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{{ end }}}
|
||||||
|
</div>
|
||||||
|
<div class="small">
|
||||||
{{{ if posts.user.userslug}}}
|
{{{ if posts.user.userslug}}}
|
||||||
<a href="{config.relative_path}/uid/{posts.user.uid}">{buildAvatar(posts.user, "24px", true, "not-responsive")} {posts.user.username}</a>
|
<a class="text-decoration-none" href="{config.relative_path}/uid/{posts.user.uid}">{buildAvatar(posts.user, "24px", true, "not-responsive")} {posts.user.username}</a>
|
||||||
{{{ else }}}
|
{{{ else }}}
|
||||||
{posts.user.username}
|
{posts.user.username}
|
||||||
{{{ end }}}
|
{{{ end }}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div class="col-lg-3 col-12">
|
<span class="badge text-body border border-gray-300 stats text-xs">
|
||||||
<strong>[[post-queue:category]]{{{ if posts.data.cid}}} <i class="fa fa-fw fa-edit" data-bs-toggle="tooltip" title="[[post-queue:category-editable]]"></i>{{{ end }}}</strong>
|
<span title="{posts.user.postcount}" class="fw-bold">{humanReadableNumber(posts.user.postcount)}</span>
|
||||||
<div class="topic-category" {{{if posts.data.cid}}}data-editable="editable"{{{end}}}">
|
<span class="text-lowercase fw-normal">[[global:posts]]</span>
|
||||||
|
</span>
|
||||||
|
<span class="badge text-body border border-gray-300 stats text-xs">
|
||||||
|
<span title="{posts.user.reputation}" class="fw-bold">{humanReadableNumber(posts.user.reputation)}</span>
|
||||||
|
<span class="text-lowercase fw-normal">[[global:reputation]]</span>
|
||||||
|
</span>
|
||||||
|
<span class="badge text-body border border-gray-300 stats text-xs">
|
||||||
|
<span class="text-lowercase fw-normal">[[user:joined]]</span>
|
||||||
|
<span title="{posts.user.joindateISO}" class="timeago fw-bold"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="card-body border-bottom">
|
||||||
|
<div class="text-xs fw-semibold mb-1">[[post-queue:when]]</div>
|
||||||
|
<span class="small timeago" title={posts.data.timestampISO}></span>
|
||||||
|
</li>
|
||||||
|
<li class="card-body border-bottom">
|
||||||
|
<div class="text-xs fw-semibold mb-1">
|
||||||
|
{{{ if posts.data.tid }}}[[post-queue:topic]]{{{ else }}}[[post-queue:title]]{{{ end }}}
|
||||||
|
</div>
|
||||||
|
<span class="small topic-title text-break">
|
||||||
|
{{{ if posts.data.tid }}}
|
||||||
|
<div class="d-flex flex-column align-items-start gap-1">
|
||||||
|
<a href="{config.relative_path}/topic/{posts.data.tid}">{posts.topic.title}</a>
|
||||||
|
<span class="badge text-body border border-gray-300 stats text-xs">
|
||||||
|
<span class="text-lowercase fw-normal">[[global:lastpost]]</span>
|
||||||
|
<span title="{posts.topic.lastposttimeISO}" class="timeago fw-bold"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{{ end }}}
|
||||||
|
<span class="title-text">{posts.data.title}</span>
|
||||||
|
</span>
|
||||||
|
{{{if !posts.data.tid}}}
|
||||||
|
<div class="topic-title-editable hidden">
|
||||||
|
<input class="form-control" type="text" value="{posts.data.title}"/>
|
||||||
|
</div>
|
||||||
|
{{{end}}}
|
||||||
|
</li>
|
||||||
|
<li class="card-body border-bottom">
|
||||||
|
<div class="text-xs fw-semibold mb-1">
|
||||||
|
[[post-queue:category]]
|
||||||
|
</div>
|
||||||
|
<div class="topic-category small">
|
||||||
<a href="{config.relative_path}/category/{posts.category.slug}">
|
<a href="{config.relative_path}/category/{posts.category.slug}">
|
||||||
<div class="category-item d-inline-block">
|
<div class="category-item d-inline-block">
|
||||||
{buildCategoryIcon(./category, "24px", "rounded-circle")}
|
{buildCategoryIcon(./category, "24px", "rounded-circle")}
|
||||||
@@ -94,46 +160,59 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
<div class="col-lg-7 col-12">
|
<li class="card-body">
|
||||||
<strong>{{{ if posts.data.tid }}}[[post-queue:topic]]{{{ else }}}[[post-queue:title]] <i class="fa fa-fw fa-edit" data-bs-toggle="tooltip" title="[[post-queue:title-editable]]"></i>{{{ end }}}</strong>
|
<div class="row row-cols-2 g-1">
|
||||||
<div class="topic-title text-break">
|
{{{ if ./canAccept }}}
|
||||||
{{{ if posts.data.tid }}}
|
<div class="col d-grid">
|
||||||
<a href="{config.relative_path}/topic/{posts.data.tid}">{posts.topic.title}</a>
|
<button class="btn btn-success btn-sm" data-action="accept"><i class="fa fa-fw fa-check"></i> [[post-queue:accept]] </button>
|
||||||
|
</div>
|
||||||
|
<div class="col d-grid">
|
||||||
|
<button class="btn btn-danger btn-sm" data-action="reject"><i class="fa fa-fw fa-times"></i> [[post-queue:reject]]</button>
|
||||||
|
</div>
|
||||||
|
{{{ end }}}
|
||||||
|
{{{ if ./canEdit}}}
|
||||||
|
{{{ if !posts.data.tid }}}
|
||||||
|
<div class="col d-grid">
|
||||||
|
<button class="btn btn-light btn-sm" data-action="editTitle"><i class="fa fa-fw fa-edit"></i> [[post-queue:title]]</button>
|
||||||
|
</div>
|
||||||
|
{{{ end }}}
|
||||||
|
<div class="col d-grid">
|
||||||
|
<button class="btn btn-light btn-sm" data-action="editContent"><i class="fa fa-fw fa-edit"></i> [[post-queue:content]]</button>
|
||||||
|
</div>
|
||||||
|
{{{if posts.data.cid}}}
|
||||||
|
<div class="col d-grid">
|
||||||
|
<button class="btn btn-light btn-sm" data-action="editCategory"><i class="fa fa-fw fa-edit"></i> [[post-queue:category]]</button>
|
||||||
|
</div>
|
||||||
|
{{{ end }}}
|
||||||
|
{{{ end }}}
|
||||||
|
{{{ if ./canAccept }}}
|
||||||
|
<div class="col d-grid">
|
||||||
|
<button class="btn btn-light btn-sm" data-action="notify"><i class="fa fa-fw fa-bell-o"></i> [[post-queue:notify]]</button>
|
||||||
|
</div>
|
||||||
|
{{{ else }}}
|
||||||
|
<div class="col d-grid">
|
||||||
|
<button class="btn btn-danger btn-sm" data-action="reject"><i class="fa fa-fw fa-times"></i> [[post-queue:remove]]</button>
|
||||||
|
</div>
|
||||||
{{{ end }}}
|
{{{ end }}}
|
||||||
<span data-action="editTitle" class="title-text">{posts.data.title}</span>
|
|
||||||
</div>
|
</div>
|
||||||
{{{if !posts.data.tid}}}
|
</li>
|
||||||
<div class="topic-title-editable hidden">
|
</ul>
|
||||||
<input class="form-control" type="text" value="{posts.data.title}"/>
|
|
||||||
</div>
|
|
||||||
{{{end}}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr/>
|
|
||||||
<div>
|
|
||||||
<strong>[[post-queue:content]] <i class="fa fa-fw fa-edit" data-bs-toggle="tooltip" title="[[post-queue:content-editable]]"></i></strong>
|
|
||||||
<div data-action="editContent" class="post-content text-break">{posts.data.content}</div>
|
|
||||||
<div class="post-content-editable hidden">
|
|
||||||
<textarea class="form-control w-100" style="height:300px;">{posts.data.rawContent}</textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer text-end">
|
<div class="col-lg-9 d-flex flex-column">
|
||||||
<div>
|
<div class="post-content mb-auto text-break p-3 pb-0 h-100">{posts.data.content}</div>
|
||||||
{{{ if ./canAccept }}}
|
<div class="post-content-editable flex-grow-1 hidden">
|
||||||
<button class="btn btn-danger btn-sm" data-action="reject"><i class="fa fa-fw fa-times"></i> [[post-queue:reject]]</button>
|
<textarea class="form-control w-100 h-100 p-3">{posts.data.rawContent}</textarea>
|
||||||
<button class="btn btn-info btn-sm" data-action="notify"><i class="fa fa-fw fa-bell-o"></i> [[post-queue:notify]]</button>
|
</div>
|
||||||
<button class="btn btn-success btn-sm" data-action="accept"><i class="fa fa-fw fa-check"></i> [[post-queue:accept]] </button>
|
<div component="post-queue/link-container" class="hidden border-top mx-3 py-3">
|
||||||
{{{ else }}}
|
<label class="text-secondary form-text mb-2">[[post-queue:links-in-this-post]]</label>
|
||||||
<button class="btn btn-danger btn-sm" data-action="reject"><i class="fa fa-fw fa-times"></i> [[post-queue:remove]]</button>
|
<ul component="post-queue/link-container/list" class="text-sm"></ul>
|
||||||
{{{ end }}}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{{ end }}}
|
|
||||||
</div>
|
</div>
|
||||||
|
{{{ end }}}
|
||||||
<!-- IMPORT partials/paginator.tpl -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- IMPORT partials/paginator.tpl -->
|
||||||
</div>
|
</div>
|
||||||
@@ -985,15 +985,15 @@ describe('Post\'s', () => {
|
|||||||
assert.equal(posts[1].data.content, 'this is a queued reply');
|
assert.equal(posts[1].data.content, 'this is a queued reply');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should error if data is invalid', (done) => {
|
it('should error if data is invalid', async () => {
|
||||||
socketPosts.editQueuedContent({ uid: globalModUid }, null, (err) => {
|
await assert.rejects(
|
||||||
assert.equal(err.message, '[[error:invalid-data]]');
|
apiPosts.editQueuedPost({ uid: globalModUid }, null),
|
||||||
done();
|
{ message: '[[error:invalid-data]]' },
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should edit post in queue', async () => {
|
it('should edit post in queue', async () => {
|
||||||
await socketPosts.editQueuedContent({ uid: globalModUid }, { id: queueId, content: 'newContent' });
|
await apiPosts.editQueuedPost({ uid: globalModUid }, { id: queueId, content: 'newContent' });
|
||||||
const { body } = await request.get(`${nconf.get('url')}/api/post-queue`, { jar });
|
const { body } = await request.get(`${nconf.get('url')}/api/post-queue`, { jar });
|
||||||
const { posts } = body;
|
const { posts } = body;
|
||||||
assert.equal(posts[1].type, 'reply');
|
assert.equal(posts[1].type, 'reply');
|
||||||
@@ -1001,7 +1001,7 @@ describe('Post\'s', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should edit topic title in queue', async () => {
|
it('should edit topic title in queue', async () => {
|
||||||
await socketPosts.editQueuedContent({ uid: globalModUid }, { id: topicQueueId, title: 'new topic title' });
|
await apiPosts.editQueuedPost({ uid: globalModUid }, { id: topicQueueId, title: 'new topic title' });
|
||||||
const { body } = await request.get(`${nconf.get('url')}/api/post-queue`, { jar });
|
const { body } = await request.get(`${nconf.get('url')}/api/post-queue`, { jar });
|
||||||
const { posts } = body;
|
const { posts } = body;
|
||||||
assert.equal(posts[0].type, 'topic');
|
assert.equal(posts[0].type, 'topic');
|
||||||
@@ -1009,39 +1009,39 @@ describe('Post\'s', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should edit topic category in queue', async () => {
|
it('should edit topic category in queue', async () => {
|
||||||
await socketPosts.editQueuedContent({ uid: globalModUid }, { id: topicQueueId, cid: 2 });
|
await apiPosts.editQueuedPost({ uid: globalModUid }, { id: topicQueueId, cid: 2 });
|
||||||
const { body } = await request.get(`${nconf.get('url')}/api/post-queue`, { jar });
|
const { body } = await request.get(`${nconf.get('url')}/api/post-queue`, { jar });
|
||||||
const { posts } = body;
|
const { posts } = body;
|
||||||
assert.equal(posts[0].type, 'topic');
|
assert.equal(posts[0].type, 'topic');
|
||||||
assert.equal(posts[0].data.cid, 2);
|
assert.equal(posts[0].data.cid, 2);
|
||||||
await socketPosts.editQueuedContent({ uid: globalModUid }, { id: topicQueueId, cid: cid });
|
await apiPosts.editQueuedPost({ uid: globalModUid }, { id: topicQueueId, cid: cid });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should prevent regular users from approving posts', (done) => {
|
it('should prevent regular users from approving posts', async () => {
|
||||||
socketPosts.accept({ uid: uid }, { id: queueId }, (err) => {
|
await assert.rejects(
|
||||||
assert.equal(err.message, '[[error:no-privileges]]');
|
apiPosts.acceptQueuedPost({ uid: uid }, { id: queueId }),
|
||||||
done();
|
{ message: '[[error:no-privileges]]' },
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should prevent regular users from approving non existing posts', (done) => {
|
it('should prevent regular users from approving non existing posts', async () => {
|
||||||
socketPosts.accept({ uid: uid }, { id: 123123 }, (err) => {
|
await assert.rejects(
|
||||||
assert.equal(err.message, '[[error:no-post]]');
|
apiPosts.acceptQueuedPost({ uid: uid }, { id: 123123 }),
|
||||||
done();
|
{ message: '[[error:no-post]]' },
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should accept queued posts and submit', async () => {
|
it('should accept queued posts and submit', async () => {
|
||||||
const ids = await db.getSortedSetRange('post:queue', 0, -1);
|
const ids = await db.getSortedSetRange('post:queue', 0, -1);
|
||||||
await socketPosts.accept({ uid: globalModUid }, { id: ids[0] });
|
await apiPosts.acceptQueuedPost({ uid: globalModUid }, { id: ids[0] });
|
||||||
await socketPosts.accept({ uid: globalModUid }, { id: ids[1] });
|
await apiPosts.acceptQueuedPost({ uid: globalModUid }, { id: ids[1] });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not crash if id does not exist', (done) => {
|
it('should not crash if id does not exist', async () => {
|
||||||
socketPosts.reject({ uid: globalModUid }, { id: '123123123' }, (err) => {
|
await assert.rejects(
|
||||||
assert.equal(err.message, '[[error:no-post]]');
|
apiPosts.removeQueuedPost({ uid: globalModUid }, { id: '123123123' }),
|
||||||
done();
|
{ message: '[[error:no-post]]' },
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should bypass post queue if user is in exempt group', async () => {
|
it('should bypass post queue if user is in exempt group', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user