v1.18.x
psychobunny 5 years ago
commit a6ae69737b

@ -2,7 +2,7 @@
"name": "nodebb",
"license": "GPL-3.0",
"description": "NodeBB Forum",
"version": "1.14.3-beta.0",
"version": "1.14.3-beta.3",
"homepage": "http://www.nodebb.org",
"repository": {
"type": "git",
@ -80,17 +80,17 @@
"@nodebb/mubsub": "^1.6.0",
"@nodebb/socket.io-adapter-mongo": "3.0.1",
"nconf": "^0.10.0",
"nodebb-plugin-composer-default": "6.3.51",
"nodebb-plugin-dbsearch": "4.1.1",
"nodebb-plugin-composer-default": "6.3.52",
"nodebb-plugin-dbsearch": "4.1.2",
"nodebb-plugin-emoji": "^3.3.0",
"nodebb-plugin-emoji-android": "2.0.0",
"nodebb-plugin-markdown": "8.11.2",
"nodebb-plugin-mentions": "2.9.2",
"nodebb-plugin-mentions": "2.9.3",
"nodebb-plugin-soundpack-default": "1.0.0",
"nodebb-plugin-spam-be-gone": "0.7.2",
"nodebb-rewards-essentials": "0.1.3",
"nodebb-theme-lavender": "5.0.11",
"nodebb-theme-persona": "10.1.67",
"nodebb-theme-persona": "10.1.68",
"nodebb-theme-slick": "1.2.29",
"nodebb-theme-vanilla": "11.1.35",
"nodebb-widget-essentials": "4.1.1",

@ -1,3 +0,0 @@
/*
The following stylesheet is only included on pages that can execute javascript
*/

@ -3,6 +3,7 @@
"ip": "IP <strong>%1</strong>",
"nodes-responded": "%1 nodes responded within %2ms!",
"host": "host",
"primary": "primary / run jobs",
"pid": "pid",
"nodejs": "nodejs",
"online": "online",

@ -79,5 +79,6 @@
"alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!",
"collapse-all": "Collapse All",
"expand-all": "Expand All",
"disable-on-create": "Disable on create"
"disable-on-create": "Disable on create",
"no-matches": "No matches"
}

@ -1,6 +1,7 @@
{
"chat.chatting_with": "Chat with",
"chat.placeholder": "Type chat message here, press enter to send",
"chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.",
"chat.send": "Send",
"chat.no_active": "You have no active chats.",
"chat.user_typing": "%1 is typing ...",

@ -2195,11 +2195,24 @@ paths:
load:
type: string
description: CPU load
nodebb:
type: object
properties:
isPrimary:
type: boolean
isCluster:
type: boolean
runJobs:
type: boolean
jobsDisabled:
type: boolean
git:
type: object
properties:
hash:
type: string
hashShort:
type: string
branch:
type: string
stats:
@ -5043,6 +5056,35 @@ paths:
properties:
title:
type: string
allCategories:
type: array
items:
type: object
properties:
bgColor:
type: string
cid:
type: number
color:
type: string
disabled:
type: number
disabledClass:
type: boolean
icon:
type: string
imageClass:
type: string
level:
type: string
link:
type: string
name:
type: string
parentCid:
type: number
slug:
type: string
posts:
type: array
items:

@ -11,9 +11,9 @@ define('admin/manage/categories', [
var sortables;
Categories.init = function () {
socket.emit('admin.categories.getAll', function (error, payload) {
if (error) {
return app.alertError(error.message);
socket.emit('admin.categories.getAll', function (err, payload) {
if (err) {
return app.alertError(err.message);
}
Categories.render(payload);
@ -63,8 +63,55 @@ define('admin/manage/categories', [
el.find('i').toggleClass('fa-minus', expand).toggleClass('fa-plus', !expand);
el.closest('[data-cid]').find('> ul[data-cid]').toggleClass('hidden', !expand);
}
$('#category-search').on('keyup', function () {
searchCategory();
});
};
function searchCategory() {
var container = $('#content .categories');
function revealParents(cid) {
var parentCid = container.find('li[data-cid="' + cid + '"]').attr('data-parent-cid');
if (parentCid) {
container.find('li[data-cid="' + parentCid + '"]').removeClass('hidden');
revealParents(parentCid);
}
}
function revealChildren(cid) {
var els = container.find('li[data-parent-cid="' + cid + '"]');
els.each(function (index, el) {
var $el = $(el);
$el.removeClass('hidden');
revealChildren($el.attr('data-cid'));
});
}
var categoryEls = container.find('li[data-cid]');
var val = $('#category-search').val().toLowerCase();
var noMatch = true;
var cids = [];
categoryEls.each(function () {
var liEl = $(this);
var isMatch = liEl.attr('data-name').toLowerCase().indexOf(val) !== -1;
if (noMatch && isMatch) {
noMatch = false;
}
if (isMatch && val) {
cids.push(liEl.attr('data-cid'));
}
liEl.toggleClass('hidden', !isMatch);
});
cids.forEach(function (cid) {
revealParents(cid);
revealChildren(cid);
});
$('[component="category/no-matches"]').toggleClass('hidden', !noMatch);
}
Categories.throwCreateModal = function () {
socket.emit('categories.getSelectCategories', {}, function (err, categories) {
if (err) {

@ -1,7 +1,7 @@
'use strict';
define('admin/manage/post-queue', function () {
define('admin/manage/post-queue', ['categorySelector'], function (categorySelector) {
var PostQueue = {};
PostQueue.init = function () {
@ -22,31 +22,64 @@ define('admin/manage/post-queue', function () {
return false;
});
$('.posts-list').on('click', '.post-content', function () {
handleContentEdit('.post-content', '.post-content-editable', 'textarea');
handleContentEdit('.topic-title', '.topic-title-editable', 'input');
$('.posts-list').on('click', '.topic-category[data-editable]', function () {
var $this = $(this);
var id = $this.parents('[data-id]').attr('data-id');
categorySelector.modal(ajaxify.data.allCategories, function (cid) {
var category = ajaxify.data.allCategories.find(function (c) {
return parseInt(c.cid, 10) === parseInt(cid, 10);
});
socket.emit('posts.editQueuedContent', {
id: id,
cid: cid,
}, function (err) {
if (err) {
return app.alertError(err.message);
}
app.parseAndTranslate('admin/manage/post-queue', 'posts', {
posts: [{
category: category,
}],
}, function (html) {
$this.replaceWith(html.find('.topic-category'));
});
});
});
return false;
});
};
function handleContentEdit(displayClass, editableClass, inputSelector) {
$('.posts-list').on('click', displayClass, function () {
var el = $(this);
el.addClass('hidden');
var textareaParent = el.parent().find('.post-content-editable');
textareaParent.removeClass('hidden').find('textarea').focus();
var inputEl = el.parent().find(editableClass);
inputEl.removeClass('hidden').find(inputSelector).focus();
});
$('.posts-list').on('blur', '.post-content-editable textarea', function () {
$('.posts-list').on('blur', editableClass + ' ' + inputSelector, function () {
var textarea = $(this);
var preview = textarea.parent().parent().find('.post-content');
var preview = textarea.parent().parent().find(displayClass);
var id = textarea.parents('[data-id]').attr('data-id');
var titleEdit = displayClass === '.topic-title';
socket.emit('posts.editQueuedContent', {
id: id,
content: textarea.val(),
title: titleEdit ? textarea.val() : undefined,
content: titleEdit ? undefined : textarea.val(),
}, function (err, data) {
if (err) {
return app.alertError(err);
}
preview.html(data.postData.content);
preview.html(titleEdit ? data.postData.title : data.postData.content);
textarea.parent().addClass('hidden');
preview.removeClass('hidden');
});
});
};
}
return PostQueue;
});

@ -38,8 +38,6 @@ app.cacheBuster = null;
});
app.load = function () {
app.loadProgressiveStylesheet();
overrides.overrideTimeago();
var url = ajaxify.start(window.location.pathname.slice(1) + window.location.search + window.location.hash);
@ -751,14 +749,6 @@ app.cacheBuster = null;
});
};
app.loadProgressiveStylesheet = function () {
var linkEl = document.createElement('link');
linkEl.rel = 'stylesheet';
linkEl.href = config.relative_path + '/assets/js-enabled.css?' + app.cacheBuster;
document.head.appendChild(linkEl);
};
app.showCookieWarning = function () {
require(['translator', 'storage'], function (translator, storage) {
if (!config.cookies.enabled || !navigator.cookieEnabled) {

@ -56,6 +56,7 @@ define('forum/chats', [
Chats.addRenameHandler(ajaxify.data.roomId, components.get('chat/controls').find('[data-action="rename"]'));
Chats.addLeaveHandler(ajaxify.data.roomId, components.get('chat/controls').find('[data-action="leave"]'));
Chats.addScrollHandler(ajaxify.data.roomId, ajaxify.data.uid, $('.chat-content'));
Chats.addScrollBottomHandler($('.chat-content'));
Chats.addCharactersLeftHandler($('[component="chat/main-wrapper"]'));
Chats.addIPHandler($('[component="chat/main-wrapper"]'));
Chats.createAutoComplete($('[component="chat/input"]'));
@ -101,6 +102,7 @@ define('forum/chats', [
Chats.addScrollHandler = function (roomId, uid, el) {
var loading = false;
el.off('scroll').on('scroll', function () {
messages.toggleScrollUpAlert(el);
if (loading) {
return;
}
@ -144,6 +146,14 @@ define('forum/chats', [
});
};
Chats.addScrollBottomHandler = function (chatContent) {
chatContent.parent()
.find('[component="chat/messages/scroll-up-alert"]')
.off('click').on('click', function () {
messages.scrollToBottom(chatContent);
});
};
Chats.addCharactersLeftHandler = function (parent) {
var element = parent.find('[component="chat/input"]');
element.on('change keyup paste', function () {

@ -84,11 +84,13 @@ define('forum/chats/messages', ['components', 'sounds', 'translator', 'benchpres
function onMessagesParsed(chatContentEl, html) {
var newMessage = $(html);
var isAtBottom = messages.isAtBottom(chatContentEl);
newMessage.appendTo(chatContentEl);
newMessage.find('.timeago').timeago();
newMessage.find('img:not(.not-responsive)').addClass('img-responsive');
messages.scrollToBottom(chatContentEl);
if (isAtBottom) {
messages.scrollToBottom(chatContentEl);
}
$(window).trigger('action:chat.received', {
messageEl: newMessage,
@ -112,13 +114,31 @@ define('forum/chats/messages', ['components', 'sounds', 'translator', 'benchpres
}
};
messages.isAtBottom = function (containerEl, threshold) {
if (containerEl.length) {
var distanceToBottom = containerEl[0].scrollHeight - (
containerEl.outerHeight() + containerEl.scrollTop()
);
return distanceToBottom < (threshold || 100);
}
};
messages.scrollToBottom = function (containerEl) {
if (containerEl.length) {
if (containerEl && containerEl.length) {
containerEl.scrollTop(containerEl[0].scrollHeight - containerEl.height());
containerEl.parent()
.find('[component="chat/messages/scroll-up-alert"]')
.addClass('hidden');
}
};
messages.toggleScrollUpAlert = function (containerEl) {
var isAtBottom = messages.isAtBottom(containerEl, 300);
containerEl.parent()
.find('[component="chat/messages/scroll-up-alert"]')
.toggleClass('hidden', isAtBottom);
};
messages.prepEdit = function (inputEl, messageId, roomId) {
socket.emit('modules.chats.getRaw', { mid: messageId, roomId: roomId }, function (err, raw) {
if (err) {

@ -91,7 +91,9 @@ define('chat', [
if (modal.is(':visible')) {
taskbar.updateActive(modal.attr('data-uuid'));
ChatsMessages.scrollToBottom(modal.find('.chat-content'));
if (ChatsMessages.isAtBottom(modal.find('.chat-content'))) {
ChatsMessages.scrollToBottom(modal.find('.chat-content'));
}
} else if (!ajaxify.data.template.chats) {
module.toggleNew(modal.attr('data-uuid'), true, true);
}
@ -239,6 +241,7 @@ define('chat', [
Chats.createAutoComplete(chatModal.find('[component="chat/input"]'));
Chats.addScrollHandler(chatModal.attr('data-roomid'), data.uid, chatModal.find('.chat-content'));
Chats.addScrollBottomHandler(chatModal.find('.chat-content'));
Chats.addCharactersLeftHandler(chatModal);
Chats.addIPHandler(chatModal);

@ -278,7 +278,7 @@
}
if (namespace && !key) {
warn('Missing key in translation token "' + name + '"');
warn('Missing key in translation token "' + name + '" for language "' + self.lang + '"');
return Promise.resolve('[[' + namespace + ']]');
}
@ -286,7 +286,7 @@
return translation.then(function (translated) {
// check if the translation is missing first
if (!translated) {
warn('Missing translation "' + name + '"');
warn('Missing translation "' + name + '" for language "' + self.lang + '"');
return backup || key;
}

@ -79,6 +79,12 @@ async function getNodeInfo() {
release: os.release(),
load: os.loadavg().map(function (load) { return load.toFixed(2); }).join(', '),
},
nodebb: {
isCluster: nconf.get('isCluster'),
isPrimary: nconf.get('isPrimary'),
runJobs: nconf.get('runJobs'),
jobsDisabled: nconf.get('jobsDisabled'),
},
};
data.process.cpuUsage.user /= 1000000;
data.process.cpuUsage.user = data.process.cpuUsage.user.toFixed(2);
@ -109,5 +115,5 @@ async function getGitInfo() {
getAsync('git rev-parse HEAD'),
getAsync('git rev-parse --abbrev-ref HEAD'),
]);
return { hash: hash, branch: branch };
return { hash: hash, hashShort: hash.substr(0, 6), branch: branch };
}

@ -181,12 +181,17 @@ modsController.postQueue = async function (req, res, next) {
const page = parseInt(req.query.page, 10) || 1;
const postsPerPage = 20;
const [ids, isAdminOrGlobalMod, moderatedCids] = await Promise.all([
const [ids, isAdminOrGlobalMod, moderatedCids, allCategories] = await Promise.all([
db.getSortedSetRange('post:queue', 0, -1),
user.isAdminOrGlobalMod(req.uid),
user.getModeratedCids(req.uid),
categories.buildForSelect(req.uid, 'find', ['disabled', 'link', 'slug']),
]);
allCategories.forEach((c) => {
c.disabledClass = !isAdminOrGlobalMod && !moderatedCids.includes(String(c.cid));
});
let postData = await getQueuedPosts(ids);
postData = postData.filter(p => p && (isAdminOrGlobalMod || moderatedCids.includes(String(p.category.cid))));
@ -198,6 +203,7 @@ modsController.postQueue = async function (req, res, next) {
res.render('admin/manage/post-queue', {
title: '[[pages:post-queue]]',
posts: postData,
allCategories: allCategories,
pagination: pagination.create(page, pageCount),
breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[pages:post-queue]]' }]),
});

@ -49,7 +49,7 @@ unreadController.get = async function (req, res, next) {
req.query.page = Math.max(1, Math.min(data.pageCount, page));
return helpers.redirect(res, '/unread?' + querystring.stringify(req.query));
}
data.showSelect = isPrivileged;
data.showSelect = true;
data.showTopicTools = isPrivileged;
data.categories = watchedCategories.categories;
data.allCategoriesUrl = 'unread' + helpers.buildQueryString('', filter, '');

@ -50,10 +50,11 @@ module.exports = function (Messaging) {
db.sortedSetAdd('chat:room:' + roomId + ':uids', now, uid),
]);
await Promise.all([
Messaging.addSystemMessage('user-join', uid, roomId), // chat owner should also get the user-join system message
Messaging.addUsersToRoom(uid, toUids, roomId),
Messaging.addRoomToUsers(roomId, [uid].concat(toUids), now),
]);
// chat owner should also get the user-join system message
await Messaging.addSystemMessage('user-join', uid, roomId);
return roomId;
};

@ -56,10 +56,6 @@ Tags.parse = async (req, data, meta, link) => {
}, {
rel: 'manifest',
href: nconf.get('relative_path') + '/manifest.json',
}, {
rel: 'preload',
href: nconf.get('relative_path') + '/assets/js-enabled.css?' + (Meta.config['cache-buster'] ? '?' + Meta.config['cache-buster'] : ''),
as: 'style',
}];
if (plugins.hasListeners('filter:search.query')) {

@ -147,23 +147,31 @@ module.exports = function (Posts) {
socketHelpers.notifyNew(data.uid, 'newPost', result);
}
Posts.editQueuedContent = async function (uid, id, content) {
const canEditQueue = await Posts.canEditQueue(uid, id);
Posts.editQueuedContent = async function (uid, editData) {
const canEditQueue = await Posts.canEditQueue(uid, editData);
if (!canEditQueue) {
throw new Error('[[error:no-privileges]]');
}
const data = await getParsedObject(id);
const data = await getParsedObject(editData.id);
if (!data) {
return;
}
data.data.content = content;
await db.setObjectField('post:queue:' + id, 'data', JSON.stringify(data.data));
if (editData.content !== undefined) {
data.data.content = editData.content;
}
if (editData.title !== undefined) {
data.data.title = editData.title;
}
if (editData.cid !== undefined) {
data.data.cid = editData.cid;
}
await db.setObjectField('post:queue:' + editData.id, 'data', JSON.stringify(data.data));
};
Posts.canEditQueue = async function (uid, id) {
Posts.canEditQueue = async function (uid, editData) {
const [isAdminOrGlobalMod, data] = await Promise.all([
user.isAdminOrGlobalMod(uid),
getParsedObject(id),
getParsedObject(editData.id),
]);
if (!data) {
return false;
@ -179,6 +187,11 @@ module.exports = function (Posts) {
} else if (data.type === 'reply') {
cid = await topics.getTopicField(data.data.tid, 'cid');
}
return await user.isModerator(uid, cid);
const isModerator = await user.isModerator(uid, cid);
let isModeratorOfTargetCid = true;
if (editData.cid) {
isModeratorOfTargetCid = await user.isModerator(uid, editData.cid);
}
return isModerator && isModeratorOfTargetCid;
};
};

@ -67,7 +67,7 @@ Auth.reloadRoutes = async function (params) {
loginStrategies.forEach(function (strategy) {
if (strategy.url) {
router.get(strategy.url, Auth.middleware.applyCSRF, function (req, res, next) {
req.session.ssoState = req.csrfToken();
req.session.ssoState = req.csrfToken && req.csrfToken();
passport.authenticate(strategy.name, {
scope: strategy.scope,
prompt: strategy.prompt || undefined,

@ -150,6 +150,7 @@ async function onMessage(socket, payload) {
});
}
} catch (err) {
winston.error(err.stack ? err.stack : err.message);
callback({ message: err.message });
}
}

@ -166,7 +166,7 @@ SocketPosts.reject = async function (socket, data) {
};
async function acceptOrReject(method, socket, data) {
const canEditQueue = await posts.canEditQueue(socket.uid, data.id);
const canEditQueue = await posts.canEditQueue(socket.uid, data);
if (!canEditQueue) {
throw new Error('[[error:no-privileges]]');
}
@ -174,11 +174,14 @@ async function acceptOrReject(method, socket, data) {
}
SocketPosts.editQueuedContent = async function (socket, data) {
if (!data || !data.id || !data.content) {
if (!data || !data.id || (!data.content && !data.title && !data.cid)) {
throw new Error('[[error:invalid-data]]');
}
await posts.editQueuedContent(socket.uid, data.id, data.content);
return await plugins.fireHook('filter:parse.post', { postData: data });
await posts.editQueuedContent(socket.uid, data);
if (data.content) {
return await plugins.fireHook('filter:parse.post', { postData: data });
}
return { postData: data };
};
require('../promisify')(SocketPosts);

@ -29,13 +29,14 @@ Digest.execute = async function (payload) {
return;
}
try {
winston.info('[user/jobs] Digest (' + payload.interval + ') scheduling completed. Sending emails; this may take some time...');
await Digest.send({
interval: payload.interval,
subscribers: subscribers,
});
winston.info('[user/jobs] Digest (' + payload.interval + ') scheduling completed. Sending emails; this may take some time...');
winston.info('[user/jobs] Digest (' + payload.interval + ') complete.');
} catch (err) {
winston.error('[user/jobs] Could not send digests (' + payload.interval + ')', err.stack);
winston.error('[user/jobs] Could not send digests (' + payload.interval + ')\n' + err.stack);
throw err;
}
};
@ -81,7 +82,10 @@ Digest.getSubscribers = async function (interval) {
});
subUids = await user.bans.filterBanned(subUids);
subscribers = subscribers.concat(subUids);
}, { interval: 1000 });
}, {
interval: 1000,
batch: 500,
});
const results = await plugins.fireHook('filter:digest.subscribers', {
interval: interval,
@ -91,15 +95,13 @@ Digest.getSubscribers = async function (interval) {
};
Digest.send = async function (data) {
var emailsSent = 0;
let emailsSent = 0;
if (!data || !data.subscribers || !data.subscribers.length) {
return emailsSent;
}
const now = new Date();
const users = await user.getUsersFields(data.subscribers, ['uid', 'username', 'userslug', 'lastonline']);
async.eachLimit(users, 100, async function (userObj) {
await async.eachLimit(data.subscribers, 100, async function (uid) {
const userObj = await user.getUserFields(uid, ['uid', 'username', 'userslug', 'lastonline']);
let [notifications, topicsData] = await Promise.all([
user.notifications.getUnreadInterval(userObj.uid, data.interval),
getTermTopics(data.interval, userObj.uid, 0, 9),
@ -121,13 +123,14 @@ Digest.send = async function (data) {
// Fix relative paths in topic data
topicsData = topicsData.map(function (topicObj) {
const user = topicObj.hasOwnProperty('teaser') && topicObj.teaser !== undefined ? topicObj.teaser.user : topicObj.user;
const user = topicObj.hasOwnProperty('teaser') && topicObj.teaser && topicObj.teaser.user ? topicObj.teaser.user : topicObj.user;
if (user && user.picture && utils.isRelativeUrl(user.picture)) {
user.picture = nconf.get('base_url') + user.picture;
}
return topicObj;
});
emailsSent += 1;
const now = new Date();
try {
await emailer.send('digest', userObj.uid, {
subject: '[[email:digest.subject, ' + (now.getFullYear() + '/' + (now.getMonth() + 1) + '/' + now.getDate()) + ']]',
@ -139,15 +142,14 @@ Digest.send = async function (data) {
showUnsubscribe: true,
});
} catch (err) {
winston.error('[user/jobs] Could not send digest email', err.stack);
winston.error('[user/jobs] Could not send digest email\n' + err.stack);
}
if (data.interval !== 'alltime') {
await db.sortedSetAdd('digest:delivery', now.getTime(), userObj.uid);
}
}, function () {
winston.info('[user/jobs] Digest (' + data.interval + ') sending completed. ' + emailsSent + ' emails sent.');
});
winston.info('[user/jobs] Digest (' + data.interval + ') sending completed. ' + emailsSent + ' emails sent.');
};
Digest.getDeliveryTimes = async (start, stop) => {

@ -11,6 +11,7 @@
<thead>
<tr>
<td>[[admin/development/info:host]]</td>
<td class="text-center">[[admin/development/info:primary]]</td>
<td>[[admin/development/info:pid]]</td>
<td>[[admin/development/info:nodejs]]</td>
<td>[[admin/development/info:online]]</td>
@ -25,6 +26,10 @@
<!-- BEGIN info -->
<tr>
<td>{info.os.hostname}:{info.process.port}</td>
<td class="text-center">
{{{if info.nodebb.isPrimary}}}<i class="fa fa-check"></i>{{{else}}}<i class="fa fa-times"></i>{{{end}}} /
{{{if info.nodebb.runJobs}}}<i class="fa fa-check"></i>{{{else}}}<i class="fa fa-times"></i>{{{end}}}
</td>
<td>{info.process.pid}</td>
<td>{info.process.version}</td>
<td>
@ -32,7 +37,7 @@
<span title="[[admin/development/info:guests]]">{info.stats.onlineGuestCount}</span> /
<span title="[[admin/development/info:sockets]]">{info.stats.socketCount}</span>
</td>
<td>{info.git.branch}@<a href="https://github.com/NodeBB/NodeBB/commit/{info.git.hash}" target="_blank">{info.git.hash}</a></td>
<td>{info.git.branch}@<a href="https://github.com/NodeBB/NodeBB/commit/{info.git.hash}" target="_blank">{info.git.hashShort}</a></td>
<td>{info.process.cpuUsage.user} / {info.process.cpuUsage.system}</td>
<td>{info.process.memoryUsage.humanReadable} mb</td>
<td>{info.os.load}</td>

@ -1,5 +1,18 @@
<button id="collapse-all" class="btn btn-default">[[admin/manage/categories:collapse-all]]</button> <button id="expand-all" class="btn btn-default">[[admin/manage/categories:expand-all]]</button>
<div class="row">
<div class="col-lg-9">
<button id="collapse-all" class="btn btn-default">[[admin/manage/categories:collapse-all]]</button> <button id="expand-all" class="btn btn-default">[[admin/manage/categories:expand-all]]</button>
</div>
<div class="col-lg-3">
<div class="input-group">
<input type="text" class="form-control" placeholder="[[global:search]]" id="category-search">
<span class="input-group-addon search-button"><i class="fa fa-search"></i></span>
</div>
</div>
</div>
<hr/>
<div component="category/no-matches" class="hidden">[[admin/manage/categories:no-matches]]</div>
<div class="categories"></div>
<button data-action="create" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored">

@ -1,9 +1,13 @@
<div class="row">
<div class="col-lg-3 pull-right">
<div class="input-group">
<input type="text" class="form-control" placeholder="[[admin/manage/groups:search-placeholder]]" id="group-search">
<span class="input-group-addon search-button"><i class="fa fa-search"></i></span>
</div>
</div>
</div>
<div class="row groups">
<div class="col-xs-12">
<div>
<input id="group-search" type="text" class="form-control" placeholder="[[admin/manage/groups:search-placeholder]]" />
</div>
<table class="table table-striped groups-list">
<thead>
<tr>

@ -18,8 +18,8 @@
<thead>
<tr>
<th>[[admin/manage/post-queue:user]]</th>
<th>[[admin/manage/post-queue:category]]</th>
<th>[[admin/manage/post-queue:title]]</th>
<th>[[admin/manage/post-queue:category]] <i class="fa fa-info-circle" data-toggle="tooltip" title="[[admin/manage/post-queue:content-editable]]"></i></th>
<th>[[admin/manage/post-queue:title]] <i class="fa fa-info-circle" data-toggle="tooltip" title="[[admin/manage/post-queue:content-editable]]"></i></th>
<th>[[admin/manage/post-queue:content]] <i class="fa fa-info-circle" data-toggle="tooltip" title="[[admin/manage/post-queue:content-editable]]"></i></th>
<th>[[admin/manage/post-queue:posted]]</th>
<th></th>
@ -35,8 +35,8 @@
{posts.user.username}
<!-- ENDIF posts.user.userslug -->
</td>
<td class="col-md-2">
<a href="{config.relative_path}/category/{posts.category.slug}"><!-- IF posts.categiry.icon --><span class="fa-stack"><i style="color: {posts.category.bgColor};" class="fa fa-circle fa-stack-2x"></i><i style="color: {posts.category.color};" class="fa fa-stack-1x fa-fw {posts.category.icon}"></i></span><!-- ENDIF posts.category.icon --> {posts.category.name}</a>
<td class="col-md-2 topic-category" {{{if posts.data.cid}}}data-editable="editable"{{{end}}}">
<a href="{config.relative_path}/category/{posts.category.slug}"><!-- IF posts.category.icon --><span class="fa-stack"><i style="color: {posts.category.bgColor};" class="fa fa-circle fa-stack-2x"></i><i style="color: {posts.category.color};" class="fa fa-stack-1x fa-fw {posts.category.icon}"></i></span><!-- ENDIF posts.category.icon --> {posts.category.name}</a>
</td>
<td class="col-md-2 topic-title">
<!-- IF posts.data.tid -->
@ -44,9 +44,14 @@
<!-- ENDIF posts.data.tid -->
{posts.data.title}
</td>
{{{if !posts.data.tid}}}
<td class="col-md-2 topic-title-editable hidden">
<input class="form-control" type="text" value="{posts.data.title}"/>
</td>
{{{end}}}
<td class="col-md-5 post-content">{posts.data.content}</td>
<td class="col-md-5 post-content-editable hidden">
<textarea>{posts.data.rawContent}</textarea>
<textarea class="form-control">{posts.data.rawContent}</textarea>
</td>
<td class="col-md-1">
<span class="timeago" title={posts.data.timestampISO}></span>

@ -1,6 +1,6 @@
<ul data-cid="{cid}">
<!-- BEGIN categories -->
<li data-cid="{categories.cid}" <!-- IF categories.disabled -->class="disabled"<!-- ENDIF categories.disabled -->>
<li data-cid="{categories.cid}" data-parent-cid="{categories.parentCid}" data-name="{categories.name}" <!-- IF categories.disabled -->class="disabled"<!-- ENDIF categories.disabled -->>
<div class="row category-row">
<div class="col-md-9">
<div class="clearfix">

@ -12,7 +12,7 @@
<a role="menu-item">[[search:no-matches]]</a>
</li>
<!-- BEGIN categories -->
<li role="presentation" class="category" data-cid="{categories.cid}" data-name="{categories.name}">
<li role="presentation" class="category {{{if categories.disabledClass}}}disabled{{{end}}}" data-cid="{categories.cid}" data-name="{categories.name}">
<a role="menu-item">{categories.level}<span component="category-markup"><!-- IF categories.icon --><span class="fa-stack" style="{function.generateCategoryBackground}"><i style="color: {categories.color};" class="fa fa-stack-1x fa-fw {categories.icon}"></i></span><!-- ENDIF categories.icon --> {categories.name}</span></a>
</li>
<!-- END categories -->

@ -137,6 +137,8 @@ before(async function () {
nconf.set('bcrypt_rounds', 1);
nconf.set('socket.io:origins', '*:*');
nconf.set('version', packageInfo.version);
nconf.set('runJobs', false);
nconf.set('jobsDisabled', false);
await meta.dependencies.check();

@ -1012,6 +1012,7 @@ describe('Post\'s', function () {
describe('post queue', function () {
var uid;
var queueId;
var topicQueueId;
var jar;
before(function (done) {
meta.config.postQueue = 1;
@ -1033,6 +1034,7 @@ describe('Post\'s', function () {
assert.ifError(err);
assert.strictEqual(result.queued, true);
assert.equal(result.message, '[[success:post-queued]]');
topicQueueId = result.id;
done();
});
@ -1082,6 +1084,33 @@ describe('Post\'s', function () {
});
});
it('should edit topic title in queue', function (done) {
socketPosts.editQueuedContent({ uid: globalModUid }, { id: topicQueueId, title: 'new topic title' }, function (err) {
assert.ifError(err);
request(nconf.get('url') + '/api/post-queue', { jar: jar, json: true }, function (err, res, body) {
assert.ifError(err);
assert.equal(body.posts[0].type, 'topic');
assert.equal(body.posts[0].data.title, 'new topic title');
done();
});
});
});
it('should edit topic category in queue', function (done) {
socketPosts.editQueuedContent({ uid: globalModUid }, { id: topicQueueId, cid: 2 }, function (err) {
assert.ifError(err);
request(nconf.get('url') + '/api/post-queue', { jar: jar, json: true }, function (err, res, body) {
assert.ifError(err);
assert.equal(body.posts[0].type, 'topic');
assert.equal(body.posts[0].data.cid, 2);
socketPosts.editQueuedContent({ uid: globalModUid }, { id: topicQueueId, cid: cid }, function (err) {
assert.ifError(err);
done();
});
});
});
});
it('should prevent regular users from approving posts', function (done) {
socketPosts.accept({ uid: uid }, { id: queueId }, function (err) {
assert.equal(err.message, '[[error:no-privileges]]');

Loading…
Cancel
Save