DEV: Display flag reasons and count for reviewable in refreshed UI (#33408)

This commit adds a header with badges indicating why a reviewable was
created in the refreshed reviewable UI that is currently being hidden
behind the `reviewable_ui_refresh` site setting.

Co-authored-by: Gary <gary@pento.net>
This commit is contained in:
Alan Guo Xiang Tan
2025-07-02 13:34:31 +08:00
committed by GitHub
co-authored by Gary
parent 30450f0449
commit db49566d90
8 changed files with 387 additions and 5 deletions
@@ -0,0 +1,28 @@
import Component from "@glimmer/component";
import { gt } from "truth-helpers";
const SCORE_TYPE_TO_CSS_CLASS_MAP = {
illegal: "illegal",
inappropriate: "inappropriate",
needs_approval: "needs-approval",
off_topic: "off-topic",
spam: "spam",
};
export default class ReviewableFlagReason extends Component {
get scoreCSSClass() {
return `--${SCORE_TYPE_TO_CSS_CLASS_MAP[this.args.type] || "other"}`;
}
<template>
<span class="review-item__flag-reason {{this.scoreCSSClass}}">
{{#if (gt @count 0)}}
<span class="review-item__flag-count {{this.scoreCSSClass}}">
{{@count}}
</span>
{{/if}}
{{@title}}
</span>
</template>
}
@@ -1,13 +1,79 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
import { dasherize } from "@ember/string";
import ReviewableFlagReason from "discourse/components/reviewable-refresh/flag-reason";
import { i18n } from "discourse-i18n";
export default class ReviewableItem extends Component {
get reviewable() {
return this.args.reviewable;
@service siteSettings;
get customClasses() {
const type = this.args.reviewable.type;
const lastPerformingUsername =
this.args.reviewable.last_performing_username;
const blurEnabled = this.siteSettings.blur_tl0_flagged_posts_media;
const trustLevel = this.args.reviewable.target_created_by_trust_level;
let classes = dasherize(type);
if (lastPerformingUsername) {
classes = `${classes} reviewable-stale`;
}
if (blurEnabled && trustLevel === 0) {
classes = `${classes} blur-images`;
}
return classes;
}
get scoreSummary() {
const scores = this.args.reviewable.reviewable_scores || [];
const scoreData = scores.reduce((acc, score) => {
if (!acc[score.score_type.type]) {
acc[score.score_type.type] = {
title: score.score_type.title,
type: score.score_type.type,
count: 0,
};
}
acc[score.score_type.type].count += 1;
return acc;
}, {});
return Object.values(scoreData);
}
<template>
<div class="review-container">
{{this.reviewable.type}}
<div
class="review-item {{this.customClasses}}"
data-reviewable-id={{@reviewable.id}}
>
<div class="review-item__primary-content">
<div class="review-item__flag-summary">
<div class="review-item__header">
<div class="review-item__label-badges">
<span class="review-item__flag-label">{{i18n
"review.flagged_as"
}}</span>
<div class="review-item__flag-badges">
{{#each this.scoreSummary as |score|}}
<ReviewableFlagReason
@type={{score.type}}
@title={{score.title}}
@count={{score.count}}
/>
{{/each}}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
}
@@ -0,0 +1,51 @@
import { render } from "@ember/test-helpers";
import { module, test } from "qunit";
import ReviewableRefreshItem from "discourse/components/reviewable-refresh/item";
import Reviewable from "discourse/models/reviewable";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
module("Integration | Component | ReviewableRefresh | Item ", function (hooks) {
setupRenderingTest(hooks);
test("has right CSS class based on reviewable type", async function (assert) {
const reviewable = Reviewable.create({
type: "post",
});
this.siteSettings.blur_tl0_flagged_posts_media = true;
await render(
<template><ReviewableRefreshItem @reviewable={{reviewable}} /></template>
);
assert.dom(".review-item").hasClass("post");
});
test("has `reviewable-stale` class when last performing username is present for reviewable", async function (assert) {
const reviewable = Reviewable.create({
type: "post",
last_performing_username: "user123",
});
await render(
<template><ReviewableRefreshItem @reviewable={{reviewable}} /></template>
);
assert.dom(".review-item").hasClass("reviewable-stale");
});
test("has `blur-images` class when blur_tl0_flagged_posts_media is enabled and reviewable's `target_created_by_trust_level` is 0", async function (assert) {
const reviewable = Reviewable.create({
type: "post",
target_created_by_trust_level: 0,
});
this.siteSettings.blur_tl0_flagged_posts_media = true;
await render(
<template><ReviewableRefreshItem @reviewable={{reviewable}} /></template>
);
assert.dom(".review-item").hasClass("blur-images");
});
});
@@ -43,6 +43,7 @@
@import "redirection";
@import "reorder-categories";
@import "reviewables";
@import "review";
@import "revise-and-reject-post-reviewable";
@import "rtl";
@import "search";
@@ -0,0 +1,140 @@
:root {
--d-review-reason-spam-bg: #fee2e2;
--d-review-reason-spam-text: #b91c1c;
--d-review-reason-spam-count-bg: #b91c1c;
--d-review-reason-spam-count-text: #fee2e2;
--d-review-reason-off-topic-bg: #dbeafe;
--d-review-reason-off-topic-text: #1d4ed8;
--d-review-reason-off-topic-count-bg: #1d4ed8;
--d-review-reason-off-topic-count-text: #dbeafe;
--d-review-reason-illegal-bg: #d1fae5;
--d-review-reason-illegal-text: #065f46;
--d-review-reason-illegal-count-bg: #065f46;
--d-review-reason-illegal-count-text: #d1fae5;
--d-review-reason-inappropriate-bg: #ffedd5;
--d-review-reason-inappropriate-text: #c2410c;
--d-review-reason-inappropriate-count-bg: #c2410c;
--d-review-reason-inappropriate-count-text: #ffedd5;
--d-review-reason-other-bg: #dee7f1;
--d-review-reason-other-text: #334155;
--d-review-reason-other-count-bg: #334155;
--d-review-reason-other-count-text: #dee7f1;
}
.review-item {
display: flex;
flex-wrap: nowrap;
gap: var(--space-4);
align-items: flex-start;
&__primary-content {
position: relative;
flex: 0 1 70%;
}
&__flag-summary {
border-radius: var(--d-border-radius);
border: 1px solid var(--primary-low);
margin-bottom: var(--space-6);
}
&__header {
display: grid;
grid-template-columns: 1fr auto;
gap: var(--space-2);
align-items: start;
background-color: var(--primary-very-low);
padding: var(--space-5) var(--space-4);
}
&__label-badges {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--space-2);
align-items: start;
}
&__flag-label {
white-space: nowrap;
text-align: left;
}
&__flag-badges {
display: flex;
flex-wrap: wrap;
gap: var(--space-1);
}
}
.review-item__flag-reason {
display: inline-flex;
align-items: center;
gap: var(--space-1);
background: var(--primary-very-low);
border-radius: 999px;
padding: var(--space-1) var(--space-2);
font-size: var(--font-down-1);
line-height: 1;
&.--illegal {
background-color: var(--d-review-reason-illegal-bg);
color: var(--d-review-reason-illegal-text);
}
&.--inappropriate {
background-color: var(--d-review-reason-inappropriate-bg);
color: var(--d-review-reason-inappropriate-text);
}
&.--other,
&.--needs-approval {
background-color: var(--d-review-reason-other-bg);
color: var(--d-review-reason-other-text);
}
&.--spam {
background-color: var(--d-review-reason-spam-bg);
color: var(--d-review-reason-spam-text);
}
&.--off-topic {
background-color: var(--d-review-reason-off-topic-bg);
color: var(--d-review-reason-off-topic-text);
}
}
.review-item__flag-count {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
min-width: 1rem;
padding: var(--space-1);
font-size: var(--font-down-2);
&.--illegal {
background-color: var(--d-review-reason-illegal-count-bg);
color: var(--d-review-reason-illegal-count-text);
}
&.--inappropriate {
background-color: var(--d-review-reason-inappropriate-count-bg);
color: var(--d-review-reason-inappropriate-count-text);
}
&.--other,
&.--needs-approval {
background-color: var(--d-review-reason-other-count-bg);
color: var(--d-review-reason-other-count-text);
}
&.--spam {
background-color: var(--d-review-reason-spam-count-bg);
color: var(--d-review-reason-spam-count-text);
}
&.--off-topic {
background-color: var(--d-review-reason-off-topic-count-bg);
color: var(--d-review-reason-off-topic-count-text);
}
}
+1
View File
@@ -817,6 +817,7 @@ en:
reject_reason:
title: "Why are you rejecting this user?"
send_email: "Send rejection email"
flagged_as: "Flagged as"
relative_time_picker:
minutes:
@@ -0,0 +1,48 @@
# frozen_string_literal: true
module PageObjects
module Pages
class Reviewable < PageObjects::Pages::Base
def visit(reviewable)
page.visit("/review/#{reviewable.id}")
self
end
def has_spam_flag_reason?(reviewable, count: 1)
has_flag_reason?(reviewable, css_class: "spam", type: :spam, count:)
end
def has_off_topic_flag_reason?(reviewable, count: 1)
has_flag_reason?(reviewable, css_class: "off-topic", type: :off_topic, count:)
end
def has_illegal_flag_reason?(reviewable, count: 1)
has_flag_reason?(reviewable, css_class: "illegal", type: :illegal, count:)
end
def has_inappropriate_flag_reason?(reviewable, count: 1)
has_flag_reason?(reviewable, css_class: "inappropriate", type: :inappropriate, count:)
end
def has_needs_approval_flag_reason?(reviewable, count: 1)
has_flag_reason?(reviewable, css_class: "needs-approval", type: :needs_approval, count:)
end
private
def has_flag_reason?(reviewable, css_class:, type:, count: 1)
within_reviewable_item(reviewable) do
expect(find(".review-item__flag-reason.--#{css_class}").text.gsub(/\s+/, " ")).to eq(
"#{count} #{ReviewableScore.type_title(type)}",
)
expect(page).to have_css(".review-item__flag-count.--#{css_class}")
end
end
def within_reviewable_item(reviewable)
within(".review-item[data-reviewable-id='#{reviewable.id}']") { yield }
end
end
end
end
+49 -2
View File
@@ -5,13 +5,15 @@ describe "Viewing reviewable item", type: :system do
fab!(:group)
fab!(:reviewable_flagged_post)
let(:reviewable_page) { PageObjects::Pages::Reviewable.new }
before { sign_in(admin) }
context "when user is not part of the groups list of the `reviewable_ui_refresh` site setting" do
before { SiteSetting.reviewable_ui_refresh = "" }
it "shows the old reviewable UI" do
visit "/review/#{reviewable_flagged_post.id}"
reviewable_page.visit(reviewable_flagged_post)
expect(page).to have_selector(".reviewable-item ")
end
@@ -24,9 +26,54 @@ describe "Viewing reviewable item", type: :system do
end
it "shows the new reviewable UI" do
visit "/review/#{reviewable_flagged_post.id}"
reviewable_page.visit(reviewable_flagged_post)
expect(page).to have_selector(".review-container")
end
it "shows the reviewable item with badges stating the flag reasons" do
_spam_reviewable_score =
Fabricate(
:reviewable_score,
reviewable: reviewable_flagged_post,
reviewable_score_type: ReviewableScore.types[:spam],
)
_off_topic_reviewable_score =
Fabricate(
:reviewable_score,
reviewable: reviewable_flagged_post,
reviewable_score_type: ReviewableScore.types[:off_topic],
)
_illegal_reviewable_score =
Fabricate(
:reviewable_score,
reviewable: reviewable_flagged_post,
reviewable_score_type: ReviewableScore.types[:illegal],
)
_inappropriate_reviewable_score =
Fabricate(
:reviewable_score,
reviewable: reviewable_flagged_post,
reviewable_score_type: ReviewableScore.types[:inappropriate],
)
_needs_approval_reviewable_score =
Fabricate(
:reviewable_score,
reviewable: reviewable_flagged_post,
reviewable_score_type: ReviewableScore.types[:needs_approval],
)
reviewable_page.visit(reviewable_flagged_post)
expect(reviewable_page).to have_spam_flag_reason(reviewable_flagged_post, count: 1)
expect(reviewable_page).to have_off_topic_flag_reason(reviewable_flagged_post, count: 1)
expect(reviewable_page).to have_illegal_flag_reason(reviewable_flagged_post, count: 1)
expect(reviewable_page).to have_inappropriate_flag_reason(reviewable_flagged_post, count: 2)
expect(reviewable_page).to have_needs_approval_flag_reason(reviewable_flagged_post, count: 1)
end
end
end