Trusted users can view & use moderator response templates (#17867)

* Trusted users can use moderator response templates

* Iron out container reveal logic

* Explanatory comment on controller endpoint confusion
This commit is contained in:
Joshua Wehner 2022-06-16 14:05:49 +02:00 committed by GitHub
parent 551e42d80e
commit 07e1364f54
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 104 additions and 55 deletions

View file

@ -42,7 +42,7 @@ function buildCommentFormHTML(commentableId, commentableType, parentId) {
<div class="response-templates-container crayons-card crayons-card--secondary p-4 mb-4 fs-base comment-form__templates hidden">
<header>
<button type="button" class="personal-template-button active" data-target-type="personal" data-form-id="new_comment">Personal</button>
<button type="button" class="moderator-template-button hidden" data-target-type="moderator" data-form-id="new_comment">Moderator</button>
<button type="button" class="moderator-template-button hidden" data-target-type="moderator" data-form-id="new_comment">Trusted User</button>
</header>
<img class="loading-img hidden" src="<%= asset_path("loading-ellipsis.svg") %>" alt="loading">
<div class="personal-responses-container"></div>

View file

@ -6,18 +6,22 @@ class ResponseTemplatesController < ApplicationController
MOD_TYPES = %w[mod_comment tag_adjustment].freeze
ADMIN_TYPES = %w[email_reply abuse_report_email_reply].freeze
# This endpoint is called to provide a mix of personal & mod_comment templates
# when replying to a comment, it also might be called for Email and Abuse
# Report Email templates. To be compatible with both uses, it returns a Hash
# sometimes and an array at other times. When type_of is present, this returns
# an array of templates. When the type_of param is missing, this returns a
# hash, keys are type_ofs, values are an array of templates.
def index
raise ArgumentError, "Missing param type_of" if params[:type_of].blank?
user_id = params[:type_of] == "personal_comment" ? current_user.id : nil
@response_templates = ResponseTemplate.where(type_of: params[:type_of], user_id: user_id)
@response_templates = policy_scope(ResponseTemplate).group_by(&:type_of)
@response_templates = @response_templates.fetch(params[:type_of], []) if params[:type_of].present?
if MOD_TYPES.include?(params[:type_of])
authorize @response_templates, :moderator_index?
authorize :response_template, :moderator_index?
elsif ADMIN_TYPES.include?(params[:type_of])
authorize @response_templates, :admin_index?
authorize :response_template, :admin_index?
else
authorize @response_templates, :index?
authorize :response_template, :index?
end
respond_to do |format|

View file

@ -157,18 +157,15 @@ function addClickListeners(form) {
function fetchResponseTemplates(typeOf, formId) {
const form = document.getElementById(formId);
let dataContainer;
if (typeOf === 'personal_comment') {
dataContainer = form.getElementsByClassName(
'personal-responses-container',
)[0];
} else if (typeOf === 'mod_comment') {
dataContainer = form.getElementsByClassName(
'moderator-responses-container',
)[0];
}
const typesOf = [
['personal_comment', 'personal-responses-container'],
['mod_comment', 'moderator-responses-container'],
];
/* eslint-disable-next-line no-undef */
fetch(`/response_templates?type_of=${typeOf}`, {
fetch(`/response_templates`, {
method: 'GET',
headers: {
Accept: 'application/json',
@ -179,9 +176,31 @@ function fetchResponseTemplates(typeOf, formId) {
.then((response) => response.json())
.then((response) => {
form.querySelector('img.loading-img').classList.toggle('hidden');
dataContainer.innerHTML = buildHTML(response, typeOf);
let revealed;
const topLevelData = document.getElementById('response-templates-data');
topLevelData.innerHTML = dataContainer.parentElement.innerHTML;
for (const typesOfContainers of typesOf) {
const [typeOf, containedIn] = typesOfContainers;
if (typeof response[typeOf] != 'undefined') {
const dataContainer = form.getElementsByClassName(containedIn)[0];
dataContainer.innerHTML = buildHTML(response[typeOf], typeOf);
if (revealed) {
topLevelData.classList.add(typeOf);
dataContainer.classList.add('hidden');
prepareHeaderButtons(form);
} else {
revealed = dataContainer;
dataContainer.classList.remove('hidden');
topLevelData.classList.add(typeOf);
}
topLevelData.innerHTML = dataContainer.parentElement.innerHTML;
}
}
addClickListeners(form);
});
}
@ -197,28 +216,12 @@ function prepareHeaderButtons(form) {
personalTemplateButton.addEventListener('click', (e) => {
toggleTemplateTypeButton(form, e);
});
personalTemplateButton.classList.remove('hidden');
modTemplateButton.addEventListener('click', (e) => {
toggleTemplateTypeButton(form, e);
});
modTemplateButton.classList.remove('hidden');
modTemplateButton.addEventListener(
'click',
() => {
const topLevelData = document.getElementById('response-templates-data');
const modDataNotFetched =
topLevelData.innerHTML !== ''
? topLevelData.getElementsByClassName(
'moderator-responses-container',
)[0].childElementCount === 0
: false;
if (modDataNotFetched) {
form.querySelector('img.loading-img').classList.toggle('hidden');
fetchResponseTemplates('mod_comment', form.id);
}
},
{ once: true },
);
}
function copyData(responsesContainer) {
@ -236,8 +239,8 @@ function openButtonCallback(form) {
const responsesContainer = form.getElementsByClassName(
'response-templates-container',
)[0];
const dataFetched =
document.getElementById('response-templates-data').innerHTML !== '';
const topLevelData = document.getElementById('response-templates-data');
const dataFetched = topLevelData.innerHTML !== '';
responsesContainer.classList.toggle('hidden');
@ -249,10 +252,24 @@ function openButtonCallback(form) {
} else if (!dataFetched && !containerHidden) {
loadData(form);
}
/* eslint-disable-next-line no-undef */
if (userData().moderator_for_tags.length > 0) {
const hasBothTemplates =
topLevelData.classList.contains('personal_comment') &&
topLevelData.classList.contains('mod_comment');
if (hasBothTemplates) {
form
.getElementsByClassName('moderator-template-button')[0]
.classList.remove('hidden');
form
.getElementsByClassName('personal-template-button')[0]
.classList.remove('hidden');
prepareHeaderButtons(form);
} else {
form
.getElementsByClassName('moderator-template-button')[0]
.classList.add('hidden');
form
.getElementsByClassName('personal-template-button')[0]
.classList.add('hidden');

View file

@ -1,6 +1,16 @@
class ResponseTemplatePolicy < ApplicationPolicy
PERMITTED_ATTRIBUTES = %i[content_type content title].freeze
class Scope < Scope
def resolve
if user.has_trusted_role? || user.any_admin? || user.moderator? || user.tag_moderator?
scope.where(user: user, type_of: "personal_comment") + scope.where.not(type_of: "personal_comment")
else
scope.where(user: user, type_of: "personal_comment")
end
end
end
def index?
true
end

View file

@ -1,7 +1,7 @@
<%= form_with(model: [:admin, response_template], local: true, html: { class: "crayons-card p-6" }) do |f| %>
<div class="form-group">
<%= f.label :type_of, "Type of Response Template" %>
<% type_of_options = [["Mod Comment", "mod_comment"], ["Personal Comment", "personal_comment"], %w[Email email_reply], ["Abuse Report Email", "abuse_report_email_reply"]] %>
<% type_of_options = [["Trusted User Comment", "mod_comment"], ["Personal Comment", "personal_comment"], %w[Email email_reply], ["Abuse Report Email", "abuse_report_email_reply"]] %>
<%= f.select :type_of, options_for_select(type_of_options, response_template.type_of), html: { required: true }, class: "form-control" %>
</div>
<div class="form-group">

View file

@ -1,10 +1,11 @@
json.array!(@response_templates) do |response_template|
json.extract!(
response_template,
:id,
:type_of,
:user_id,
:title,
:content,
)
attrs = %i[id type_of user_id title content]
if @response_templates.is_a?(Array)
json.array!(@response_templates, *attrs)
else
@response_templates.each_pair do |type_of, templates|
json.set!(type_of) do
json.array!(templates, *attrs)
end
end
end

View file

@ -93,7 +93,7 @@ en:
create:
subtitle: Create template
desc: Templates let you quickly answer FAQs or store snippets for re-use.
moderator: Moderator
moderator: Trusted User
personal: Personal
submit: Submit
preview: Preview
@ -144,4 +144,3 @@ en:
one: "%{count}%{start}\_comment%{end}"
other: "%{count}%{start}\_comments%{end}"
title: Comments

View file

@ -35,6 +35,15 @@ RSpec.describe "ResponseTemplate", type: :request do
expect(response.parsed_body.length).to eq total_response_templates
end
it "returns a hash with personal response templates if type_of unspecified" do
create_list(:response_template, 2, user: nil, type_of: "mod_comment")
create_list(:response_template, 2, user: user, type_of: "personal_comment")
get response_templates_path, params: { type_of: nil }, headers: { HTTP_ACCEPT: "application/json" }
json = JSON.parse(response.body)
expect(json.keys).to contain_exactly("personal_comment")
expect(json.values.flatten.count).to eq(2)
end
it "returns only the users' response templates" do
create(:response_template, user: nil, type_of: "mod_comment")
create_list(:response_template, 2, user: user, type_of: "personal_comment")
@ -77,6 +86,15 @@ RSpec.describe "ResponseTemplate", type: :request do
expect(JSON.parse(response.body).length).to eq 2
end
it "returns both personal and moderator response templates if type_of unspecified" do
create_list(:response_template, 2, user: nil, type_of: "mod_comment")
create_list(:response_template, 2, user: moderator, type_of: "personal_comment")
get response_templates_path, params: { type_of: nil }, headers: { HTTP_ACCEPT: "application/json" }
json = JSON.parse(response.body)
expect(json.keys).to contain_exactly("mod_comment", "personal_comment")
expect(json.values.flatten.count).to eq(4)
end
it "raises unauthorized error if trying to view admin response templates" do
create_list(:response_template, 2, user: nil, type_of: "email_reply", content_type: "html")
expect do