FIX: various revision history modal quirks (#27058)

- FIX: properly scope category changes to what the current user can see
- UX: previous category is now highlighted in "red", new category is highlighted in "green"
- PERF: no need to serialize the categories
- FIX: properly track wiki
- FIX: properly track post_type (aka. Staff Color)
- FIX: properly track making a topic a PM
- FIX: never show the category changes when a topic is made a PM
- PERF: post_revision serializer is now more leaner (never includes title changes when post_number > 1, never includes user changes if there aren't any)
- UX: always sort the tags by name
This commit is contained in:
Régis Hanol 2024-05-22 10:09:20 +02:00 committed by GitHub
parent 52125d849f
commit 3d4d21693b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 162 additions and 66 deletions

View File

@ -7,7 +7,6 @@
<ConditionalLoadingSpinner @condition={{this.initialLoad}}> <ConditionalLoadingSpinner @condition={{this.initialLoad}}>
<Modal::History::Revision <Modal::History::Revision
@model={{this.postRevision}} @model={{this.postRevision}}
@wikiDisabled={{this.wikiDisabled}}
@previousCategory={{this.previousCategory}} @previousCategory={{this.previousCategory}}
@currentCategory={{this.currentCategory}} @currentCategory={{this.currentCategory}}
@displayInline={{this.displayInline}} @displayInline={{this.displayInline}}
@ -20,8 +19,6 @@
@hiddenClasses={{this.hiddenClasses}} @hiddenClasses={{this.hiddenClasses}}
@mobileView={{this.site.mobileView}} @mobileView={{this.site.mobileView}}
@userChanges={{this.user_changes}} @userChanges={{this.user_changes}}
@wikiDisabled={{this.wikiDisabled}}
@postTypeDisabled={{this.postTypeDisabled}}
@previousCategory={{this.previousCategory}} @previousCategory={{this.previousCategory}}
@currentCategory={{this.currentCategory}} @currentCategory={{this.currentCategory}}
@previousTagChanges={{this.previousTagChanges}} @previousTagChanges={{this.previousTagChanges}}

View File

@ -238,34 +238,29 @@ export default class History extends Component {
} }
get previousCategory() { get previousCategory() {
if (this.postRevision?.category_id_changes) { if (this.postRevision?.category_id_changes?.previous) {
let category = Category.findById( let category = Category.findById(
this.postRevision.category_id_changes.previous this.postRevision.category_id_changes.previous
); );
return categoryBadgeHTML(category, { allowUncategorized: true }); return categoryBadgeHTML(category, {
allowUncategorized: true,
extraClasses: "diff-del",
});
} }
} }
get currentCategory() { get currentCategory() {
if (this.postRevision?.category_id_changes) { if (this.postRevision?.category_id_changes?.current) {
let category = Category.findById( let category = Category.findById(
this.postRevision.category_id_changes.current this.postRevision.category_id_changes.current
); );
return categoryBadgeHTML(category, { allowUncategorized: true }); return categoryBadgeHTML(category, {
allowUncategorized: true,
extraClasses: "diff-ins",
});
} }
} }
get wikiDisabled() {
return !this.postRevision.wiki_changes?.current;
}
get postTypeDisabled() {
return (
this.postRevision?.post_type_changes?.current !==
this.site.post_types.moderator_action
);
}
@action @action
displayInline(event) { displayInline(event) {
event?.preventDefault(); event?.preventDefault();

View File

@ -35,17 +35,48 @@
{{/if}} {{/if}}
{{#if @model.wiki_changes}} {{#if @model.wiki_changes}}
<DisabledIcon @icon="far-edit" @disabled={{@wikiDisabled}} /> {{d-icon
"far-edit"
class=(if @model.wiki_changes.current "diff-ins" "diff-del")
}}
{{/if}} {{/if}}
{{#if @model.post_type_changes}} {{#if @model.post_type_changes}}
<DisabledIcon @icon="shield-alt" @disabled={{@postTypeDisabled}} /> {{d-icon
"shield-alt"
class=(if
(eq
@model.post_type_changes.current
@site.post_types.moderator_action
)
"diff-del"
"diff-ins"
)
}}
{{/if}} {{/if}}
{{#if @model.category_id_changes}} {{#if @model.archetype_changes}}
{{html-safe @previousCategory}} {{d-icon
(if
(eq @model.archetype_changes.current "private_message")
"envelope"
"comment"
)
}}
{{/if}}
{{#if (and @model.category_id_changes (not @model.archetype_changes))}}
{{#if @previousCategory}}
{{html-safe @previousCategory}}
{{else}}
{{d-icon "far-eye-slash" class="diff-del"}}
{{/if}}
&rarr; &rarr;
{{html-safe @currentCategory}} {{#if @currentCategory}}
{{html-safe @currentCategory}}
{{else}}
{{d-icon "far-eye-slash" class="diff-ins"}}
{{/if}}
{{/if}} {{/if}}
</span> </span>
{{/if}} {{/if}}

View File

@ -22,19 +22,36 @@
{{/if}} {{/if}}
{{#if @model.wiki_changes}} {{#if @model.wiki_changes}}
<div class="row"> <div class="row">
<DisabledIcon @icon="far-edit" @disabled={{@wikiDisabled}} /> {{d-icon
"far-edit"
class=(if @model.wiki_changes.current "diff-ins" "diff-del")
}}
</div> </div>
{{/if}} {{/if}}
{{#if @model.post_type_changes}} {{#if @model.archetype_changes}}
<div class="row"> <div class="row">
<DisabledIcon @icon="shield-alt" @disabled={{@postTypeDisabled}} /> {{d-icon
(if
(eq @model.archetype_changes.current "private_message")
"envelope"
"comment"
)
}}
</div> </div>
{{/if}} {{/if}}
{{#if @model.category_id_changes}} {{#if (and @model.category_id_changes (not @model.archetype_changes))}}
<div class="row"> <div class="row">
{{html-safe @previousCategory}} {{#if @previousCategory}}
{{html-safe @previousCategory}}
{{else}}
{{d-icon "far-eye-slash" class="diff-del"}}
{{/if}}
&rarr; &rarr;
{{html-safe @currentCategory}} {{#if @currentCategory}}
{{html-safe @currentCategory}}
{{else}}
{{d-icon "far-eye-slash" class="diff-ins"}}
{{/if}}
</div> </div>
{{/if}} {{/if}}
{{/if}} {{/if}}

View File

@ -25,11 +25,9 @@ class PostRevisionSerializer < ApplicationSerializer
:title_changes, :title_changes,
:user_changes, :user_changes,
:tags_changes, :tags_changes,
:wiki, :category_id_changes,
:can_edit :can_edit
has_many :categories, serializer: CategoryBadgeSerializer, embed: :objects
# Creates a field called field_name_changes with previous and # Creates a field called field_name_changes with previous and
# current members if a field has changed in this revision # current members if a field has changed in this revision
def self.add_compared_field(field) def self.add_compared_field(field)
@ -42,6 +40,7 @@ class PostRevisionSerializer < ApplicationSerializer
end end
add_compared_field :wiki add_compared_field :wiki
add_compared_field :post_type
def previous_hidden def previous_hidden
previous["hidden"] previous["hidden"]
@ -101,19 +100,16 @@ class PostRevisionSerializer < ApplicationSerializer
user.avatar_template user.avatar_template
end end
def wiki
object.post.wiki
end
def can_edit def can_edit
scope.can_edit?(object.post) scope.can_edit?(object.post)
end end
def edit_reason def edit_reason
# only show 'edit_reason' when revisions are consecutive current["edit_reason"]
if scope.can_view_hidden_post_revisions? || current["revision"] == previous["revision"] + 1 end
current["edit_reason"]
end def include_edit_reason?
scope.can_view_hidden_post_revisions? || current["revision"] == previous["revision"] + 1
end end
def body_changes def body_changes
@ -139,10 +135,13 @@ class PostRevisionSerializer < ApplicationSerializer
{ inline: diff.inline_html, side_by_side: diff.side_by_side_html } { inline: diff.inline_html, side_by_side: diff.side_by_side_html }
end end
def include_title_changes?
object.post.post_number == 1
end
def user_changes def user_changes
prev = previous["user_id"] prev = previous["user_id"]
cur = current["user_id"] cur = current["user_id"]
return if prev == cur
# if stuff is messed up, default to system # if stuff is messed up, default to system
previous = User.find_by(id: prev) || Discourse.system_user previous = User.find_by(id: prev) || Discourse.system_user
@ -162,16 +161,30 @@ class PostRevisionSerializer < ApplicationSerializer
} }
end end
def include_user_changes?
previous["user_id"] != current["user_id"]
end
def tags_changes def tags_changes
changes = { pre = filter_tags previous["tags"]
previous: filter_visible_tags(previous["tags"]), cur = filter_tags current["tags"]
current: filter_visible_tags(current["tags"]),
} pre == cur ? nil : { previous: pre, current: cur }
changes[:previous] == changes[:current] ? nil : changes
end end
def include_tags_changes? def include_tags_changes?
scope.can_see_tags?(topic) && previous["tags"] != current["tags"] previous["tags"] != current["tags"] && scope.can_see_tags?(topic)
end
def category_id_changes
pre = filter_category_id previous["category_id"]
cur = filter_category_id current["category_id"]
pre == cur ? nil : { previous: pre, current: cur }
end
def include_category_id_changes?
previous["category_id"] != current["category_id"]
end end
protected protected
@ -206,14 +219,15 @@ class PostRevisionSerializer < ApplicationSerializer
# Retrieve any `tracked_topic_fields` # Retrieve any `tracked_topic_fields`
PostRevisor.tracked_topic_fields.each_key do |field| PostRevisor.tracked_topic_fields.each_key do |field|
next if field == :tags # Special handling below next if field == :tags
latest_modifications[field.to_s] = [topic.public_send(field)] if topic.respond_to?(field) latest_modifications[field.to_s] = [topic.public_send(field)] if topic.respond_to?(field)
end end
latest_modifications["featured_link"] = [ latest_modifications["featured_link"] = [
post.topic.featured_link, topic.featured_link,
] if SiteSetting.topic_featured_link_enabled ] if SiteSetting.topic_featured_link_enabled
latest_modifications["tags"] = [topic.tags.pluck(:name)] if scope.can_see_tags?(topic)
latest_modifications["tags"] = [topic.tags.map(&:name).sort]
post_revisions << PostRevision.new( post_revisions << PostRevision.new(
number: post_revisions.last.number + 1, number: post_revisions.last.number + 1,
@ -229,7 +243,7 @@ class PostRevisionSerializer < ApplicationSerializer
revision[:revision] = pr.number revision[:revision] = pr.number
revision[:hidden] = pr.hidden revision[:hidden] = pr.hidden
pr.modifications.each_key { |field| revision[field] = pr.modifications[field][0] } pr.modifications.each { |field, (value, _)| revision[field] = value }
@all_revisions << revision @all_revisions << revision
end end
@ -260,12 +274,16 @@ class PostRevisionSerializer < ApplicationSerializer
object.user || Discourse.system_user object.user || Discourse.system_user
end end
def filter_visible_tags(tags) def hidden_tags
if tags.is_a?(Array) && tags.size > 0 @hidden_tags ||= DiscourseTagging.hidden_tag_names(scope)
@hidden_tag_names ||= DiscourseTagging.hidden_tag_names(scope) end
tags - @hidden_tag_names
else def filter_tags(tags)
tags tags - hidden_tags
end end
def filter_category_id(category_id)
return if category_id.blank?
Category.secured(scope).find_by(id: category_id)&.id
end end
end end

View File

@ -3,13 +3,51 @@
RSpec.describe PostRevisionSerializer do RSpec.describe PostRevisionSerializer do
fab!(:post) { Fabricate(:post, version: 2) } fab!(:post) { Fabricate(:post, version: 2) }
context "with secured categories" do
fab!(:group)
fab!(:private_category) { Fabricate(:private_category, group: group) }
fab!(:post_revision) do
Fabricate(
:post_revision,
post: post,
modifications: {
"category_id" => [private_category.id, post.topic.category_id],
},
)
end
it "returns category changes to staff" do
json =
PostRevisionSerializer.new(
post_revision,
scope: Guardian.new(Fabricate(:admin)),
root: false,
).as_json
expect(json[:category_id_changes][:previous]).to eq(private_category.id)
expect(json[:category_id_changes][:current]).to eq(post.topic.category_id)
end
it "does not return all category changes to non-staff" do
json =
PostRevisionSerializer.new(
post_revision,
scope: Guardian.new(Fabricate(:user)),
root: false,
).as_json
expect(json[:category_id_changes][:previous]).to eq(nil)
expect(json[:category_id_changes][:current]).to eq(post.topic.category_id)
end
end
context "with hidden tags" do context "with hidden tags" do
fab!(:public_tag) { Fabricate(:tag, name: "public") } fab!(:public_tag) { Fabricate(:tag, name: "public") }
fab!(:public_tag2) { Fabricate(:tag, name: "visible") } fab!(:public_tag2) { Fabricate(:tag, name: "visible") }
fab!(:hidden_tag) { Fabricate(:tag, name: "hidden") } fab!(:hidden_tag) { Fabricate(:tag, name: "hidden") }
fab!(:hidden_tag2) { Fabricate(:tag, name: "secret") } fab!(:hidden_tag2) { Fabricate(:tag, name: "secret") }
let(:staff_tag_group) do fab!(:staff_tag_group) do
Fabricate( Fabricate(
:tag_group, :tag_group,
permissions: { permissions: {
@ -41,7 +79,6 @@ RSpec.describe PostRevisionSerializer do
before do before do
SiteSetting.tagging_enabled = true SiteSetting.tagging_enabled = true
staff_tag_group
post.topic.tags = [public_tag2, hidden_tag] post.topic.tags = [public_tag2, hidden_tag]
end end
@ -52,10 +89,9 @@ RSpec.describe PostRevisionSerializer do
scope: Guardian.new(Fabricate(:admin)), scope: Guardian.new(Fabricate(:admin)),
root: false, root: false,
).as_json ).as_json
expect(json[:tags_changes][:previous]).to include(public_tag.name)
expect(json[:tags_changes][:previous]).to include(hidden_tag.name) expect(json[:tags_changes][:previous]).to contain_exactly(public_tag.name, hidden_tag.name)
expect(json[:tags_changes][:current]).to include(public_tag2.name) expect(json[:tags_changes][:current]).to contain_exactly(public_tag2.name, hidden_tag.name)
expect(json[:tags_changes][:current]).to include(hidden_tag.name)
end end
it "does not return hidden tags to non-staff" do it "does not return hidden tags to non-staff" do
@ -65,8 +101,9 @@ RSpec.describe PostRevisionSerializer do
scope: Guardian.new(Fabricate(:user)), scope: Guardian.new(Fabricate(:user)),
root: false, root: false,
).as_json ).as_json
expect(json[:tags_changes][:previous]).to eq([public_tag.name])
expect(json[:tags_changes][:current]).to eq([public_tag2.name]) expect(json[:tags_changes][:previous]).to contain_exactly(public_tag.name)
expect(json[:tags_changes][:current]).to contain_exactly(public_tag2.name)
end end
it "does not show tag modifications if changes are not visible to the user" do it "does not show tag modifications if changes are not visible to the user" do
@ -76,6 +113,7 @@ RSpec.describe PostRevisionSerializer do
scope: Guardian.new(Fabricate(:user)), scope: Guardian.new(Fabricate(:user)),
root: false, root: false,
).as_json ).as_json
expect(json[:tags_changes]).to_not be_present expect(json[:tags_changes]).to_not be_present
end end
end end