FEATURE: Show stale reviewable to other clients (#13114)

The previous commits removed reviewables leading to a bad user
experience. This commit updates the status, replaces actions with a
message and greys out the reviewable.
This commit is contained in:
Dan Ungureanu 2021-05-26 02:47:35 +03:00 committed by GitHub
parent 8c83803109
commit 197e3f24ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 151 additions and 51 deletions

View File

@ -29,12 +29,17 @@ export default Component.extend({
@discourseComputed( @discourseComputed(
"reviewable.type", "reviewable.type",
"reviewable.stale",
"siteSettings.blur_tl0_flagged_posts_media", "siteSettings.blur_tl0_flagged_posts_media",
"reviewable.target_created_by_trust_level" "reviewable.target_created_by_trust_level"
) )
customClasses(type, blurEnabled, trustLevel) { customClasses(type, stale, blurEnabled, trustLevel) {
let classes = type.dasherize(); let classes = type.dasherize();
if (stale) {
classes = `${classes} reviewable-stale`;
}
if (blurEnabled && trustLevel === 0) { if (blurEnabled && trustLevel === 0) {
classes = `${classes} blur-images`; classes = `${classes} blur-images`;
} }

View File

@ -55,6 +55,18 @@ export default DiscourseRoute.extend({
}); });
} }
}); });
this.messageBus.subscribe("/reviewable_counts", (data) => {
if (data.updates) {
this.controller.reviewables.forEach((reviewable) => {
const updates = data.updates[reviewable.id];
if (updates) {
reviewable.setProperties(updates);
reviewable.set("stale", true);
}
});
}
});
}, },
deactivate() { deactivate() {

View File

@ -44,42 +44,46 @@
{{/if}} {{/if}}
</div> </div>
<div class="reviewable-actions"> <div class="reviewable-actions">
{{#if claimEnabled}} {{#if reviewable.stale}}
<div class="claimed-actions"> <div class="stale-help">{{i18n "review.stale_help"}}</div>
<span class="help">{{html-safe claimHelp}}</span> {{else}}
{{reviewable-claimed-topic topicId=topicId claimedBy=reviewable.claimed_by}} {{#if claimEnabled}}
</div> <div class="claimed-actions">
{{/if}} <span class="help">{{html-safe claimHelp}}</span>
{{reviewable-claimed-topic topicId=topicId claimedBy=reviewable.claimed_by}}
</div>
{{/if}}
{{#if canPerform}} {{#if canPerform}}
{{#if editing}} {{#if editing}}
{{d-button
class="btn-primary reviewable-action save-edit"
disabled=updating
icon="check"
action=(action "saveEdit")
label="review.save"}}
{{d-button
class="btn-danger reviewable-action cancel-edit"
disabled=updating
icon="times"
action=(action "cancelEdit")
label="review.cancel"}}
{{else}}
{{#each reviewable.bundled_actions as |bundle|}}
{{reviewable-bundled-action
bundle=bundle
performAction=(action "perform")
reviewableUpdating=updating}}
{{/each}}
{{#if reviewable.can_edit}}
{{d-button {{d-button
class="reviewable-action edit" class="btn-primary reviewable-action save-edit"
disabled=updating disabled=disabled
icon="pencil-alt" icon="check"
action=(action "edit") action=(action "saveEdit")
label="review.edit"}} label="review.save"}}
{{d-button
class="btn-danger reviewable-action cancel-edit"
disabled=disabled
icon="times"
action=(action "cancelEdit")
label="review.cancel"}}
{{else}}
{{#each reviewable.bundled_actions as |bundle|}}
{{reviewable-bundled-action
bundle=bundle
performAction=(action "perform")
reviewableUpdating=disabled}}
{{/each}}
{{#if reviewable.can_edit}}
{{d-button
class="reviewable-action edit"
disabled=disabled
icon="pencil-alt"
action=(action "edit")
label="review.edit"}}
{{/if}}
{{/if}} {{/if}}
{{/if}} {{/if}}
{{/if}} {{/if}}

View File

@ -1,4 +1,8 @@
import { acceptance, queryAll } from "discourse/tests/helpers/qunit-helpers"; import {
acceptance,
publishToMessageBus,
queryAll,
} from "discourse/tests/helpers/qunit-helpers";
import { click, fillIn, visit } from "@ember/test-helpers"; import { click, fillIn, visit } from "@ember/test-helpers";
import I18n from "I18n"; import I18n from "I18n";
import selectKit from "discourse/tests/helpers/select-kit-helper"; import selectKit from "discourse/tests/helpers/select-kit-helper";
@ -193,4 +197,26 @@ acceptance("Review", function (needs) {
); );
assert.equal(queryAll(`${topic} .category-name`).text().trim(), "support"); assert.equal(queryAll(`${topic} .category-name`).text().trim(), "support");
}); });
test("Reviewables can become stale", async function (assert) {
await visit("/review");
const reviewable = find("[data-reviewable-id=1234]")[0];
assert.notOk(reviewable.className.includes("reviewable-stale"));
assert.equal(find("[data-reviewable-id=1234] .status .pending").length, 1);
assert.equal(find(".stale-help").length, 0);
publishToMessageBus("/reviewable_counts", {
review_count: 1,
updates: {
1234: { status: 1 },
},
});
await visit("/review"); // wait for re-render
assert.ok(reviewable.className.includes("reviewable-stale"));
assert.equal(find("[data-reviewable-id=1234] .status .approved").length, 1);
assert.equal(find(".stale-help").length, 1);
});
}); });

View File

@ -276,6 +276,10 @@
padding-bottom: 1em; padding-bottom: 1em;
} }
.reviewable-stale {
opacity: 0.7;
}
.blur-images { .blur-images {
img:not(.avatar):not(.emoji) { img:not(.avatar):not(.emoji) {
filter: blur(10px); filter: blur(10px);

View File

@ -12,11 +12,11 @@ class ReviewablesController < ApplicationController
offset = params[:offset].to_i offset = params[:offset].to_i
if params[:type].present? if params[:type].present?
raise Discourse::InvalidParameter.new(:type) unless Reviewable.valid_type?(params[:type]) raise Discourse::InvalidParameters.new(:type) unless Reviewable.valid_type?(params[:type])
end end
status = (params[:status] || 'pending').to_sym status = (params[:status] || 'pending').to_sym
raise Discourse::InvalidParameter.new(:status) unless allowed_statuses.include?(status) raise Discourse::InvalidParameters.new(:status) unless allowed_statuses.include?(status)
topic_id = params[:topic_id] ? params[:topic_id].to_i : nil topic_id = params[:topic_id] ? params[:topic_id].to_i : nil
category_id = params[:category_id] ? params[:category_id].to_i : nil category_id = params[:category_id] ? params[:category_id].to_i : nil

View File

@ -15,28 +15,58 @@ class Jobs::NotifyReviewable < ::Jobs::Base
counts[r.reviewable_by_group_id] += 1 if r.reviewable_by_group_id counts[r.reviewable_by_group_id] += 1 if r.reviewable_by_group_id
end end
all_updates = Hash.new { |h, k| h[k] = {} }
if args[:updated_reviewable_ids].present?
Reviewable.where(id: args[:updated_reviewable_ids]).each do |r|
payload = { status: r.status }
all_updates[:admins][r.id] = payload
all_updates[:moderators][r.id] = payload if r.reviewable_by_moderator?
all_updates[r.reviewable_by_group_id][r.id] = payload if r.reviewable_by_group_id
end
end
# admins # admins
notify(counts[:admins], User.real.admins.pluck(:id)) notify(
User.real.admins.pluck(:id),
count: counts[:admins],
updates: all_updates[:admins],
)
# moderators # moderators
if reviewable.reviewable_by_moderator? if reviewable.reviewable_by_moderator?
notify(counts[:moderators], User.real.moderators.where("id NOT IN (?)", @contacted).pluck(:id)) notify(
User.real.moderators.where("id NOT IN (?)", @contacted).pluck(:id),
count: counts[:moderators],
updates: all_updates[:moderators],
)
end end
# category moderators # category moderators
if SiteSetting.enable_category_group_moderation? && (group = reviewable.reviewable_by_group) if SiteSetting.enable_category_group_moderation? && (group = reviewable.reviewable_by_group)
group.users.includes(:group_users).where("users.id NOT IN (?)", @contacted).find_each do |user| group.users.includes(:group_users).where("users.id NOT IN (?)", @contacted).find_each do |user|
count = user.group_users.map { |gu| counts[gu.group_id] }.sum count = 0
notify(count, [user.id]) updates = {}
user.group_users.each do |gu|
count += counts[gu.group_id] || 0
updates.merge!(all_updates[gu.group_id] || {})
end
notify([user.id], count: count, updates: updates)
end end
end end
end end
protected protected
def notify(count, user_ids) def notify(user_ids, count:, updates:)
return if user_ids.blank? return if user_ids.blank?
data = { reviewable_count: count } data = { reviewable_count: count }
data[:updates] = updates if updates.present?
MessageBus.publish("/reviewable_counts", data, user_ids: user_ids) MessageBus.publish("/reviewable_counts", data, user_ids: user_ids)
@contacted += user_ids @contacted += user_ids
end end

View File

@ -339,7 +339,6 @@ class Reviewable < ActiveRecord::Base
# the result of the operation and whether the status of the reviewable changed. # the result of the operation and whether the status of the reviewable changed.
def perform(performed_by, action_id, args = nil) def perform(performed_by, action_id, args = nil)
args ||= {} args ||= {}
# Support this action or any aliases # Support this action or any aliases
aliases = self.class.action_aliases aliases = self.class.action_aliases
valid = [ action_id, aliases.to_a.select { |k, v| v == action_id }.map(&:first) ].flatten valid = [ action_id, aliases.to_a.select { |k, v| v == action_id }.map(&:first) ].flatten
@ -367,7 +366,14 @@ class Reviewable < ActiveRecord::Base
if result && result.after_commit if result && result.after_commit
result.after_commit.call result.after_commit.call
end end
Jobs.enqueue(:notify_reviewable, reviewable_id: self.id) if update_count
if update_count || result.remove_reviewable_ids.present?
Jobs.enqueue(
:notify_reviewable,
reviewable_id: self.id,
updated_reviewable_ids: result.remove_reviewable_ids,
)
end
result result
end end

View File

@ -426,6 +426,7 @@ en:
type_bonus: type_bonus:
name: "type bonus" name: "type bonus"
title: "Certain reviewable types can be assigned a bonus by staff to make them a higher priority." title: "Certain reviewable types can be assigned a bonus by staff to make them a higher priority."
stale_help: "This reviewable has been resolved by someone else."
claim_help: claim_help:
optional: "You can claim this item to prevent others from reviewing it." optional: "You can claim this item to prevent others from reviewing it."
required: "You must claim items before you can review them." required: "You must claim items before you can review them."

View File

@ -300,6 +300,7 @@ RSpec.describe Reviewable, type: :model do
job = Jobs::NotifyReviewable.jobs.last job = Jobs::NotifyReviewable.jobs.last
expect(job["args"].first["reviewable_id"]).to eq(reviewable.id) expect(job["args"].first["reviewable_id"]).to eq(reviewable.id)
expect(job["args"].first["updated_reviewable_ids"]).to contain_exactly(reviewable.id)
end end
it "triggers a notification on pending -> reject" do it "triggers a notification on pending -> reject" do
@ -312,22 +313,33 @@ RSpec.describe Reviewable, type: :model do
job = Jobs::NotifyReviewable.jobs.last job = Jobs::NotifyReviewable.jobs.last
expect(job["args"].first["reviewable_id"]).to eq(reviewable.id) expect(job["args"].first["reviewable_id"]).to eq(reviewable.id)
expect(job["args"].first["updated_reviewable_ids"]).to contain_exactly(reviewable.id)
end end
it "doesn't trigger a notification on approve -> reject" do it "triggers a notification on approve -> reject to update status" do
reviewable = Fabricate(:reviewable_queued_post, status: Reviewable.statuses[:approved]) reviewable = Fabricate(:reviewable_queued_post, status: Reviewable.statuses[:approved])
expect do expect do
reviewable.perform(moderator, :reject_post) reviewable.perform(moderator, :reject_post)
end.to_not change { Jobs::NotifyReviewable.jobs.size } end.to change { Jobs::NotifyReviewable.jobs.size }.by(1)
job = Jobs::NotifyReviewable.jobs.last
expect(job["args"].first["reviewable_id"]).to eq(reviewable.id)
expect(job["args"].first["updated_reviewable_ids"]).to contain_exactly(reviewable.id)
end end
it "doesn't trigger a notification on reject -> approve" do it "triggers a notification on reject -> approve to update status" do
reviewable = Fabricate(:reviewable_queued_post, status: Reviewable.statuses[:rejected]) reviewable = Fabricate(:reviewable_queued_post, status: Reviewable.statuses[:rejected])
expect do expect do
reviewable.perform(moderator, :approve_post) reviewable.perform(moderator, :approve_post)
end.to_not change { Jobs::NotifyReviewable.jobs.size } end.to change { Jobs::NotifyReviewable.jobs.size }.by(1)
job = Jobs::NotifyReviewable.jobs.last
expect(job["args"].first["reviewable_id"]).to eq(reviewable.id)
expect(job["args"].first["updated_reviewable_ids"]).to contain_exactly(reviewable.id)
end end
end end

View File

@ -105,7 +105,7 @@ describe ReviewablesController do
it "raises an error with an invalid type" do it "raises an error with an invalid type" do
get "/review.json?type=ReviewableMadeUp" get "/review.json?type=ReviewableMadeUp"
expect(response.code).to eq("500") expect(response.code).to eq("400")
end end
it "supports filtering by status" do it "supports filtering by status" do
@ -135,7 +135,7 @@ describe ReviewablesController do
it "raises an error with an invalid status" do it "raises an error with an invalid status" do
get "/review.json?status=xyz" get "/review.json?status=xyz"
expect(response.code).to eq("500") expect(response.code).to eq("400")
end end
it "supports filtering by category_id" do it "supports filtering by category_id" do