mirror of
https://github.com/discourse/discourse.git
synced 2025-03-09 14:34:35 +00:00
FEATURE: allow category group moderators to pin/unpin topics (#12325)
* FEATURE: allow category group moderators to pin/unpin topics Category group moderators should be able to pin/unpin any topics within a category where they have appropraite category group moderator permissions.
This commit is contained in:
parent
4430bc153d
commit
ac7773a30d
@ -47,6 +47,11 @@ export default Controller.extend(ModalFunctionality, {
|
|||||||
return I18n.t(name, { categoryLink, until });
|
return I18n.t(name, { categoryLink, until });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@discourseComputed("model.details.can_pin_unpin_topic")
|
||||||
|
canPinGlobally(canPinUnpinTopic) {
|
||||||
|
return this.currentUser.isElder && canPinUnpinTopic;
|
||||||
|
},
|
||||||
|
|
||||||
@discourseComputed("categoryLink")
|
@discourseComputed("categoryLink")
|
||||||
pinMessage(categoryLink) {
|
pinMessage(categoryLink) {
|
||||||
return I18n.t("topic.feature_topic.pin", { categoryLink });
|
return I18n.t("topic.feature_topic.pin", { categoryLink });
|
||||||
|
@ -71,39 +71,26 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature-section">
|
{{#if canPinGlobally}}
|
||||||
<div class="desc">
|
<div class="feature-section">
|
||||||
<p>
|
<div class="desc">
|
||||||
{{#conditional-loading-spinner size="small" condition=loading}}
|
|
||||||
{{#if pinnedGloballyCount}}
|
|
||||||
{{html-safe (i18n "topic.feature_topic.already_pinned_globally" count=pinnedGloballyCount)}}
|
|
||||||
{{else}}
|
|
||||||
{{html-safe (i18n "topic.feature_topic.not_pinned_globally")}}
|
|
||||||
{{/if}}
|
|
||||||
{{/conditional-loading-spinner}}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{{i18n "topic.feature_topic.global_pin_note"}}
|
|
||||||
</p>
|
|
||||||
{{#if site.isMobileDevice}}
|
|
||||||
<p>
|
<p>
|
||||||
{{i18n "topic.feature_topic.pin_globally"}}
|
{{#conditional-loading-spinner size="small" condition=loading}}
|
||||||
|
{{#if pinnedGloballyCount}}
|
||||||
|
{{html-safe (i18n "topic.feature_topic.already_pinned_globally" count=pinnedGloballyCount)}}
|
||||||
|
{{else}}
|
||||||
|
{{html-safe (i18n "topic.feature_topic.not_pinned_globally")}}
|
||||||
|
{{/if}}
|
||||||
|
{{/conditional-loading-spinner}}
|
||||||
</p>
|
</p>
|
||||||
<p class="with-validation">
|
<p>
|
||||||
{{future-date-input
|
{{i18n "topic.feature_topic.global_pin_note"}}
|
||||||
class="pin-until"
|
|
||||||
includeFarFuture=true
|
|
||||||
clearable=true
|
|
||||||
input=model.pinnedGloballyUntil
|
|
||||||
onChangeInput=(action (mut model.pinnedGloballyUntil))
|
|
||||||
}}
|
|
||||||
{{popup-input-tip validation=pinGloballyValidation shownAt=pinGloballyTipShownAt}}
|
|
||||||
</p>
|
</p>
|
||||||
{{else}}
|
{{#if site.isMobileDevice}}
|
||||||
<p class="with-validation">
|
<p>
|
||||||
{{i18n "topic.feature_topic.pin_globally"}}
|
{{i18n "topic.feature_topic.pin_globally"}}
|
||||||
<span>
|
</p>
|
||||||
{{d-icon "far-clock"}}
|
<p class="with-validation">
|
||||||
{{future-date-input
|
{{future-date-input
|
||||||
class="pin-until"
|
class="pin-until"
|
||||||
includeFarFuture=true
|
includeFarFuture=true
|
||||||
@ -112,14 +99,29 @@
|
|||||||
onChangeInput=(action (mut model.pinnedGloballyUntil))
|
onChangeInput=(action (mut model.pinnedGloballyUntil))
|
||||||
}}
|
}}
|
||||||
{{popup-input-tip validation=pinGloballyValidation shownAt=pinGloballyTipShownAt}}
|
{{popup-input-tip validation=pinGloballyValidation shownAt=pinGloballyTipShownAt}}
|
||||||
</span>
|
</p>
|
||||||
|
{{else}}
|
||||||
|
<p class="with-validation">
|
||||||
|
{{i18n "topic.feature_topic.pin_globally"}}
|
||||||
|
<span>
|
||||||
|
{{d-icon "far-clock"}}
|
||||||
|
{{future-date-input
|
||||||
|
class="pin-until"
|
||||||
|
includeFarFuture=true
|
||||||
|
clearable=true
|
||||||
|
input=model.pinnedGloballyUntil
|
||||||
|
onChangeInput=(action (mut model.pinnedGloballyUntil))
|
||||||
|
}}
|
||||||
|
{{popup-input-tip validation=pinGloballyValidation shownAt=pinGloballyTipShownAt}}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
{{/if}}
|
||||||
|
<p>
|
||||||
|
{{d-button action=(action "pinGlobally") icon="thumbtack" label="topic.feature.pin_globally" class="btn-primary"}}
|
||||||
</p>
|
</p>
|
||||||
{{/if}}
|
</div>
|
||||||
<p>
|
|
||||||
{{d-button action=(action "pinGlobally") icon="thumbtack" label="topic.feature.pin_globally" class="btn-primary"}}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if currentUser.staff}}
|
{{#if currentUser.staff}}
|
||||||
<div class="feature-section">
|
<div class="feature-section">
|
||||||
|
@ -206,17 +206,23 @@ export default createWidget("topic-admin-menu", {
|
|||||||
icon: "far-clock",
|
icon: "far-clock",
|
||||||
label: "actions.timed_update",
|
label: "actions.timed_update",
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!isPrivateMessage && (topic.get("visible") || featured)) {
|
if (
|
||||||
this.addActionButton({
|
details.get("can_pin_unpin_topic") &&
|
||||||
className: "topic-admin-pin",
|
!isPrivateMessage &&
|
||||||
buttonClass: "popup-menu-btn",
|
(topic.get("visible") || featured)
|
||||||
action: "showFeatureTopic",
|
) {
|
||||||
icon: "thumbtack",
|
this.addActionButton({
|
||||||
label: featured ? "actions.unpin" : "actions.pin",
|
className: "topic-admin-pin",
|
||||||
});
|
buttonClass: "popup-menu-btn",
|
||||||
}
|
action: "showFeatureTopic",
|
||||||
|
icon: "thumbtack",
|
||||||
|
label: featured ? "actions.unpin" : "actions.pin",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.get("currentUser.canManageTopic")) {
|
||||||
if (this.currentUser.get("staff")) {
|
if (this.currentUser.get("staff")) {
|
||||||
this.addActionButton({
|
this.addActionButton({
|
||||||
className: "topic-admin-change-timestamp",
|
className: "topic-admin-change-timestamp",
|
||||||
|
@ -459,3 +459,45 @@ acceptance("Topic with title decorated", function (needs) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
acceptance("Topic pinning/unpinning as an admin", function (needs) {
|
||||||
|
needs.user({ admin: true });
|
||||||
|
|
||||||
|
test("Admin pinning topic", async function (assert) {
|
||||||
|
await visit("/t/topic-for-group-moderators/2480");
|
||||||
|
|
||||||
|
await click(".toggle-admin-menu");
|
||||||
|
await click(".topic-admin-pin .btn");
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
exists(".feature-topic .btn-primary"),
|
||||||
|
"it should show the 'Pin Topic' button"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
exists(".make-banner"),
|
||||||
|
"it should show the 'Banner Topic' button"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
acceptance("Topic pinning/unpinning as a group moderator", function (needs) {
|
||||||
|
needs.user({ moderator: false, admin: false, trust_level: 1 });
|
||||||
|
|
||||||
|
test("Group category moderator pinning topic", async function (assert) {
|
||||||
|
await visit("/t/topic-for-group-moderators/2480");
|
||||||
|
|
||||||
|
await click(".toggle-admin-menu");
|
||||||
|
await click(".topic-admin-pin .btn");
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
exists(".feature-topic .btn-primary"),
|
||||||
|
"it should show the 'Pin Topic' button"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
!exists(".make-banner"),
|
||||||
|
"it should not show the 'Banner Topic' button"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -2206,6 +2206,7 @@ export default {
|
|||||||
can_publish_page: true,
|
can_publish_page: true,
|
||||||
can_invite_via_email: true,
|
can_invite_via_email: true,
|
||||||
can_toggle_topic_visibility: true,
|
can_toggle_topic_visibility: true,
|
||||||
|
can_pin_unpin_topic: true,
|
||||||
auto_close_at: null,
|
auto_close_at: null,
|
||||||
auto_close_hours: null,
|
auto_close_hours: null,
|
||||||
auto_close_based_on_last_post: false,
|
auto_close_based_on_last_post: false,
|
||||||
@ -5597,6 +5598,7 @@ export default {
|
|||||||
can_toggle_topic_visibility: true,
|
can_toggle_topic_visibility: true,
|
||||||
can_split_merge_topic: true,
|
can_split_merge_topic: true,
|
||||||
can_edit_staff_notes: true,
|
can_edit_staff_notes: true,
|
||||||
|
can_pin_unpin_topic: true,
|
||||||
can_moderate_category: true,
|
can_moderate_category: true,
|
||||||
participants: [
|
participants: [
|
||||||
{
|
{
|
||||||
|
@ -429,6 +429,8 @@ class TopicsController < ApplicationController
|
|||||||
guardian.ensure_can_archive_topic!(@topic)
|
guardian.ensure_can_archive_topic!(@topic)
|
||||||
when 'visible'
|
when 'visible'
|
||||||
guardian.ensure_can_toggle_topic_visibility!(@topic)
|
guardian.ensure_can_toggle_topic_visibility!(@topic)
|
||||||
|
when 'pinned'
|
||||||
|
guardian.ensure_can_pin_unpin_topic!(@topic)
|
||||||
else
|
else
|
||||||
guardian.ensure_can_moderate!(@topic)
|
guardian.ensure_can_moderate!(@topic)
|
||||||
end
|
end
|
||||||
|
@ -21,6 +21,7 @@ class TopicViewDetailsSerializer < ApplicationSerializer
|
|||||||
:can_split_merge_topic,
|
:can_split_merge_topic,
|
||||||
:can_edit_staff_notes,
|
:can_edit_staff_notes,
|
||||||
:can_toggle_topic_visibility,
|
:can_toggle_topic_visibility,
|
||||||
|
:can_pin_unpin_topic,
|
||||||
:can_moderate_category]
|
:can_moderate_category]
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -149,6 +150,10 @@ class TopicViewDetailsSerializer < ApplicationSerializer
|
|||||||
scope.can_toggle_topic_visibility?(object.topic)
|
scope.can_toggle_topic_visibility?(object.topic)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def include_can_pin_unpin_topic?
|
||||||
|
scope.can_pin_unpin_topic?(object.topic)
|
||||||
|
end
|
||||||
|
|
||||||
def can_perform_action_available_to_group_moderators?
|
def can_perform_action_available_to_group_moderators?
|
||||||
@can_perform_action_available_to_group_moderators ||= scope.can_perform_action_available_to_group_moderators?(object.topic)
|
@can_perform_action_available_to_group_moderators ||= scope.can_perform_action_available_to_group_moderators?(object.topic)
|
||||||
end
|
end
|
||||||
|
@ -239,6 +239,7 @@ module TopicGuardian
|
|||||||
alias :can_open_topic? :can_perform_action_available_to_group_moderators?
|
alias :can_open_topic? :can_perform_action_available_to_group_moderators?
|
||||||
alias :can_split_merge_topic? :can_perform_action_available_to_group_moderators?
|
alias :can_split_merge_topic? :can_perform_action_available_to_group_moderators?
|
||||||
alias :can_edit_staff_notes? :can_perform_action_available_to_group_moderators?
|
alias :can_edit_staff_notes? :can_perform_action_available_to_group_moderators?
|
||||||
|
alias :can_pin_unpin_topic? :can_perform_action_available_to_group_moderators?
|
||||||
|
|
||||||
def can_move_posts?(topic)
|
def can_move_posts?(topic)
|
||||||
return false if is_silenced?
|
return false if is_silenced?
|
||||||
|
@ -940,12 +940,21 @@ RSpec.describe TopicsController do
|
|||||||
expect(topic.posts.last.action_code).to eq('archived.disabled')
|
expect(topic.posts.last.action_code).to eq('archived.disabled')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'should not allow a group moderator to pin a topic' do
|
it 'should allow a group moderator to pin a topic' do
|
||||||
put "/t/#{topic.id}/status.json", params: {
|
put "/t/#{topic.id}/status.json", params: {
|
||||||
status: 'pinned', enabled: 'true'
|
status: 'pinned', enabled: 'true', until: 2.weeks.from_now
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(response.status).to eq(403)
|
expect(response.status).to eq(200)
|
||||||
|
expect(topic.reload.pinned_at).to_not eq(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should allow a group moderator to unpin a topic' do
|
||||||
|
put "/t/#{topic.id}/status.json", params: {
|
||||||
|
status: 'pinned', enabled: 'false'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response.status).to eq(200)
|
||||||
expect(topic.reload.pinned_at).to eq(nil)
|
expect(topic.reload.pinned_at).to eq(nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user