mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-01-01 21:30:30 +01:00
feat: allow tag editing from topic tools
closes #7536 closes #7465 closes #11538
This commit is contained in:
@@ -100,10 +100,10 @@
|
||||
"nodebb-plugin-ntfy": "1.0.15",
|
||||
"nodebb-plugin-spam-be-gone": "2.0.6",
|
||||
"nodebb-rewards-essentials": "0.2.3",
|
||||
"nodebb-theme-harmony": "1.0.6",
|
||||
"nodebb-theme-harmony": "1.0.7",
|
||||
"nodebb-theme-lavender": "7.0.9",
|
||||
"nodebb-theme-peace": "2.0.21",
|
||||
"nodebb-theme-persona": "13.0.58",
|
||||
"nodebb-theme-persona": "13.0.59",
|
||||
"nodebb-widget-essentials": "7.0.11",
|
||||
"nodemailer": "6.9.1",
|
||||
"nprogress": "0.2.0",
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
"already-posting": "You are already posting",
|
||||
"tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)",
|
||||
"tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 character(s)",
|
||||
"tag-not-allowed": "Tag not allowed",
|
||||
"not-enough-tags": "Not enough tags. Topics must have at least %1 tag(s)",
|
||||
"too-many-tags": "Too many tags. Topics can't have more than %1 tag(s)",
|
||||
"cant-use-system-tag": "You can not use this system tag.",
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
"enter_tags_here": "Enter tags here, between %1 and %2 characters each.",
|
||||
"enter_tags_here_short": "Enter tags...",
|
||||
"no_tags": "There are no tags yet.",
|
||||
"select_tags": "Select Tags"
|
||||
"select_tags": "Select Tags",
|
||||
"tag-whitelist": "Tag Whitelist"
|
||||
}
|
||||
@@ -115,6 +115,7 @@
|
||||
"thread_tools.change_owner": "Change Owner",
|
||||
"thread_tools.select_category": "Select Category",
|
||||
"thread_tools.fork": "Fork Topic",
|
||||
"thread_tools.tag": "Tag Topic",
|
||||
"thread_tools.delete": "Delete Topic",
|
||||
"thread_tools.delete-posts": "Delete Posts",
|
||||
"thread_tools.delete_confirm": "Are you sure you want to delete this topic?",
|
||||
|
||||
@@ -1,4 +1,46 @@
|
||||
put:
|
||||
tags:
|
||||
- topics
|
||||
summary: update the tags of a topic
|
||||
description: This operation updates the tags of the topic to the array of tags sent in the request
|
||||
parameters:
|
||||
- in: path
|
||||
name: tid
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
description: a valid topic id
|
||||
example: 1
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
tags:
|
||||
type: array
|
||||
description: 'An array of tags'
|
||||
items:
|
||||
type: string
|
||||
example: [test, foobar]
|
||||
responses:
|
||||
'200':
|
||||
description: Topic tags successfully updated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
$ref: ../../../components/schemas/Status.yaml#/Status
|
||||
response:
|
||||
type: array
|
||||
description: 'The current tags of the topic'
|
||||
items:
|
||||
type: object
|
||||
example: [{}, {}]
|
||||
patch:
|
||||
tags:
|
||||
- topics
|
||||
summary: adds tags to a topic
|
||||
@@ -35,8 +77,11 @@ put:
|
||||
status:
|
||||
$ref: ../../../components/schemas/Status.yaml#/Status
|
||||
response:
|
||||
type: object
|
||||
properties: {}
|
||||
type: array
|
||||
description: 'The current tags of the topic'
|
||||
items:
|
||||
type: object
|
||||
example: [{}, {}]
|
||||
delete:
|
||||
tags:
|
||||
- topics
|
||||
|
||||
@@ -6,6 +6,16 @@
|
||||
[component="category-selector-selected"] span {
|
||||
display: inline-flex!important;
|
||||
}
|
||||
.bootstrap-tagsinput {
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
.ui-autocomplete {
|
||||
max-height: 350px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
@@ -17,6 +27,6 @@
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
.tool-modal {
|
||||
max-width: 400px;
|
||||
max-width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +117,14 @@ define('forum/category/tools', [
|
||||
});
|
||||
});
|
||||
|
||||
components.get('topic/tag').on('click', async function () {
|
||||
const tids = topicSelect.getSelectedTids();
|
||||
const topics = await Promise.all(tids.map(tid => api.get(`/topics/${tid}`)));
|
||||
require(['forum/topic/tag'], function (tag) {
|
||||
tag.init(topics, ajaxify.data.tagWhitelist, onCommandComplete);
|
||||
});
|
||||
});
|
||||
|
||||
CategoryTools.removeListeners();
|
||||
socket.on('event:topic_deleted', setDeleteState);
|
||||
socket.on('event:topic_restored', setDeleteState);
|
||||
|
||||
@@ -159,12 +159,8 @@ define('forum/topic/events', [
|
||||
}
|
||||
|
||||
if (data.topic.tags && data.topic.tagsupdated) {
|
||||
Benchpress.render('partials/topic/tags', { tags: data.topic.tags }).then(function (html) {
|
||||
const tags = $('[data-pid="' + data.post.pid + '"] .tags');
|
||||
tags.fadeOut(250, function () {
|
||||
tags.toggleClass('hidden', data.topic.tags.length === 0);
|
||||
tags.html(html).fadeIn(250);
|
||||
});
|
||||
require(['forum/topic/tag'], function (tag) {
|
||||
tag.updateTopicTags([data.topic]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
120
public/src/client/topic/tag.js
Normal file
120
public/src/client/topic/tag.js
Normal file
@@ -0,0 +1,120 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
define('forum/topic/tag', [
|
||||
'alerts', 'autocomplete', 'api', 'benchpress',
|
||||
], function (alerts, autocomplete, api, Benchpress) {
|
||||
const Tag = {};
|
||||
let tagModal;
|
||||
let tagCommit;
|
||||
let topics;
|
||||
let tagWhitelist;
|
||||
Tag.init = function (_topics, _tagWhitelist, onComplete) {
|
||||
if (tagModal) {
|
||||
return;
|
||||
}
|
||||
topics = _topics;
|
||||
tagWhitelist = _tagWhitelist || [];
|
||||
|
||||
app.parseAndTranslate('modals/tag-topic', {
|
||||
topics: topics,
|
||||
tagWhitelist: tagWhitelist,
|
||||
}, function (html) {
|
||||
tagModal = html;
|
||||
|
||||
tagCommit = tagModal.find('#tag-topic-commit');
|
||||
|
||||
$('body').append(tagModal);
|
||||
|
||||
tagModal.find('#tag-topic-cancel').on('click', closeTagModal);
|
||||
|
||||
tagCommit.on('click', async () => {
|
||||
await tagTopics();
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
});
|
||||
|
||||
tagModal.find('.tags').each((index, el) => {
|
||||
const tagEl = $(el);
|
||||
const tagsinputEl = tagEl.tagsinput({
|
||||
tagClass: 'badge bg-info',
|
||||
confirmKeys: [13, 44],
|
||||
trimValue: true,
|
||||
});
|
||||
const input = tagsinputEl[0].$input;
|
||||
|
||||
const topic = topics[index];
|
||||
topic.tags.forEach(tag => tagEl.tagsinput('add', tag.value));
|
||||
|
||||
tagEl.on('itemAdded', function (event) {
|
||||
if (tagWhitelist.length && !tagWhitelist.includes(event.item)) {
|
||||
tagEl.tagsinput('remove', event.item);
|
||||
alerts.error('[[error:tag-not-allowed]]');
|
||||
}
|
||||
if (input.length) {
|
||||
input.autocomplete('close');
|
||||
}
|
||||
});
|
||||
|
||||
initAutocomplete({
|
||||
input,
|
||||
container: tagsinputEl[0].$container,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function initAutocomplete(params) {
|
||||
autocomplete.init({
|
||||
input: params.input,
|
||||
position: { my: 'left bottom', at: 'left top', collision: 'flip' },
|
||||
appendTo: params.container,
|
||||
source: async (request, response) => {
|
||||
socket.emit('topics.autocompleteTags', {
|
||||
query: request.term,
|
||||
}, function (err, tags) {
|
||||
if (err) {
|
||||
return alerts.error(err);
|
||||
}
|
||||
if (tags) {
|
||||
response(tags);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function tagTopics() {
|
||||
await Promise.all(tagModal.find('.tags').map(async (index, el) => {
|
||||
const topic = topics[index];
|
||||
const tagEl = $(el);
|
||||
topic.tags = await api.put(`/topics/${topic.tid}/tags`, { tags: tagEl.tagsinput('items') });
|
||||
Tag.updateTopicTags([topic]);
|
||||
}));
|
||||
closeTagModal();
|
||||
}
|
||||
|
||||
Tag.updateTopicTags = function (topics) {
|
||||
topics.forEach((topic) => {
|
||||
// render "partials/category/tags" or "partials/topic/tags"
|
||||
const tpl = ajaxify.data.template.topic ? 'partials/topic/tags' : 'partials/category/tags';
|
||||
Benchpress.render(tpl, { tags: topic.tags }).then(function (html) {
|
||||
const tags = $(`[data-tid="${topic.tid}"][component="topic/tags"]`);
|
||||
tags.fadeOut(250, function () {
|
||||
tags.toggleClass('hidden', topic.tags.length === 0);
|
||||
tags.html(html).fadeIn(250);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function closeTagModal() {
|
||||
if (tagModal) {
|
||||
tagModal.remove();
|
||||
tagModal = null;
|
||||
}
|
||||
}
|
||||
|
||||
return Tag;
|
||||
});
|
||||
@@ -133,6 +133,12 @@ define('forum/topic/threadTools', [
|
||||
});
|
||||
});
|
||||
|
||||
topicContainer.on('click', '[component="topic/tag"]', function () {
|
||||
require(['forum/topic/tag'], function (tag) {
|
||||
tag.init([ajaxify.data], ajaxify.data.tagWhitelist);
|
||||
});
|
||||
});
|
||||
|
||||
topicContainer.on('click', '[component="topic/move-posts"]', function () {
|
||||
require(['forum/topic/move-post'], function (movePosts) {
|
||||
movePosts.init();
|
||||
|
||||
@@ -4,21 +4,21 @@ define('autocomplete', ['api', 'alerts'], function (api, alerts) {
|
||||
const module = {};
|
||||
const _default = {
|
||||
delay: 200,
|
||||
appendTo: null,
|
||||
};
|
||||
|
||||
module.init = (params) => {
|
||||
const { input, source, onSelect, delay } = { ..._default, ...params };
|
||||
|
||||
const acParams = { ..._default, ...params };
|
||||
const { input, onSelect } = acParams;
|
||||
app.loadJQueryUI(function () {
|
||||
input.autocomplete({
|
||||
delay,
|
||||
...acParams,
|
||||
open: function () {
|
||||
$(this).autocomplete('widget').css('z-index', 100005);
|
||||
},
|
||||
select: function (event, ui) {
|
||||
handleOnSelect(input, onSelect, event, ui);
|
||||
},
|
||||
source,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -173,6 +173,17 @@ topicsAPI.unfollow = async function (caller, data) {
|
||||
await topics.unfollow(data.tid, caller.uid);
|
||||
};
|
||||
|
||||
topicsAPI.updateTags = async (caller, { tid, tags }) => {
|
||||
if (!await privileges.topics.canEdit(tid, caller.uid)) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
|
||||
const cid = await topics.getTopicField(tid, 'cid');
|
||||
await topics.validateTags(tags, cid, caller.uid, tid);
|
||||
await topics.updateTopicTags(tid, tags);
|
||||
return await topics.getTopicTagsObjects(tid);
|
||||
};
|
||||
|
||||
topicsAPI.addTags = async (caller, { tid, tags }) => {
|
||||
if (!await privileges.topics.canEdit(tid, caller.uid)) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
@@ -180,9 +191,10 @@ topicsAPI.addTags = async (caller, { tid, tags }) => {
|
||||
|
||||
const cid = await topics.getTopicField(tid, 'cid');
|
||||
await topics.validateTags(tags, cid, caller.uid, tid);
|
||||
tags = await topics.filterTags(tags);
|
||||
tags = await topics.filterTags(tags, cid);
|
||||
|
||||
await topics.addTags(tags, [tid]);
|
||||
return await topics.getTopicTagsObjects(tid);
|
||||
};
|
||||
|
||||
topicsAPI.deleteTags = async (caller, { tid }) => {
|
||||
|
||||
@@ -100,13 +100,21 @@ Topics.unfollow = async (req, res) => {
|
||||
helpers.formatApiResponse(200, res);
|
||||
};
|
||||
|
||||
Topics.updateTags = async (req, res) => {
|
||||
const payload = await api.topics.updateTags(req, {
|
||||
tid: req.params.tid,
|
||||
tags: req.body.tags,
|
||||
});
|
||||
helpers.formatApiResponse(200, res, payload);
|
||||
};
|
||||
|
||||
Topics.addTags = async (req, res) => {
|
||||
await api.topics.addTags(req, {
|
||||
const payload = await api.topics.addTags(req, {
|
||||
tid: req.params.tid,
|
||||
tags: req.body.tags,
|
||||
});
|
||||
|
||||
helpers.formatApiResponse(200, res);
|
||||
helpers.formatApiResponse(200, res, payload);
|
||||
};
|
||||
|
||||
Topics.deleteTags = async (req, res) => {
|
||||
|
||||
@@ -32,7 +32,8 @@ module.exports = function () {
|
||||
setupApiRoute(router, 'put', '/:tid/ignore', [...middlewares, middleware.assert.topic], controllers.write.topics.ignore);
|
||||
setupApiRoute(router, 'delete', '/:tid/ignore', [...middlewares, middleware.assert.topic], controllers.write.topics.unfollow); // intentional, unignore == unfollow
|
||||
|
||||
setupApiRoute(router, 'put', '/:tid/tags', [...middlewares, middleware.checkRequired.bind(null, ['tags']), middleware.assert.topic], controllers.write.topics.addTags);
|
||||
setupApiRoute(router, 'put', '/:tid/tags', [...middlewares, middleware.checkRequired.bind(null, ['tags']), middleware.assert.topic], controllers.write.topics.updateTags);
|
||||
setupApiRoute(router, 'patch', '/:tid/tags', [...middlewares, middleware.checkRequired.bind(null, ['tags']), middleware.assert.topic], controllers.write.topics.addTags);
|
||||
setupApiRoute(router, 'delete', '/:tid/tags', [...middlewares, middleware.assert.topic], controllers.write.topics.deleteTags);
|
||||
|
||||
setupApiRoute(router, 'get', '/:tid/thumbs', [], controllers.write.topics.getThumbs);
|
||||
|
||||
27
src/views/modals/tag-topic.tpl
Normal file
27
src/views/modals/tag-topic.tpl
Normal file
@@ -0,0 +1,27 @@
|
||||
<div class="card tool-modal shadow">
|
||||
<h5 class="card-header">
|
||||
[[topic:thread_tools.tag]]
|
||||
</h5>
|
||||
<div class="card-body d-flex flex-column gap-2">
|
||||
<div class="d-flex flex-column gap-1">
|
||||
{{{ if tagWhitelist }}}
|
||||
<span>[[tags:tag-whitelist]]</span>
|
||||
<div>
|
||||
{{{ each tagWhitelist }}}
|
||||
<span class="badge bg-info">{@value}</span>
|
||||
{{{ end }}}
|
||||
</div>
|
||||
{{{ end }}}
|
||||
</div>
|
||||
{{{ each topics }}}
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="fork-title"><strong>{./title}</strong></label>
|
||||
<input class="tags" type="text" placeholder="[[tags:enter_tags_here, {config.minimumTagLength}, {config.maximumTagLength}]]" />
|
||||
</div>
|
||||
{{{ end }}}
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<button class="btn btn-link btn-sm" id="tag-topic-cancel">[[global:buttons.close]]</button>
|
||||
<button class="btn btn-primary btn-sm" id="tag-topic-commit">[[global:save]]</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,6 +72,7 @@ describe('API', async () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
patch: {},
|
||||
delete: {
|
||||
'/users/{uid}/tokens/{token}': [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user