mirror of
https://github.com/discourse/discourse.git
synced 2025-04-01 21:56:53 +08:00
DEV: Convert history modal to component-based API (#22522)
This PR converts the `history` modal to make use of the new component-based API
This commit is contained in:
parent
e214fc38ed
commit
80a1709965
@ -0,0 +1,63 @@
|
||||
<DModal
|
||||
@title={{i18n this.modalTitleKey}}
|
||||
@closeModal={{@closeModal}}
|
||||
class="history-modal"
|
||||
>
|
||||
<:body>
|
||||
<Modal::History::Revision
|
||||
@model={{this.postRevision}}
|
||||
@mobileView={{this.site.mobileView}}
|
||||
@wikiDisabled={{this.wikiDisabled}}
|
||||
@previousCategory={{this.previousCategory}}
|
||||
@currentCategory={{this.currentCategory}}
|
||||
@displayInline={{this.displayInline}}
|
||||
@displaySideBySide={{this.displaySideBySide}}
|
||||
@displaySideBySideMarkdown={{this.displaySideBySideMarkdown}}
|
||||
@viewMode={{this.viewMode}}
|
||||
/>
|
||||
<Modal::History::Revisions
|
||||
@model={{this.postRevision}}
|
||||
@hiddenClasses={{this.hiddenClasses}}
|
||||
@mobileView={{this.site.mobileView}}
|
||||
@userChanges={{this.user_changes}}
|
||||
@wikiDisabled={{this.wikiDisabled}}
|
||||
@postTypeDisabled={{this.postTypeDisabled}}
|
||||
@previousCategory={{this.previousCategory}}
|
||||
@currentCategory={{this.currentCategory}}
|
||||
@previousTagChanges={{this.previousTagChanges}}
|
||||
@currentTagChanges={{this.currentTagChanges}}
|
||||
@bodyDiffHTML={{this.bodyDiffHTML}}
|
||||
@bodyDiff={{this.bodyDiff}}
|
||||
@calculateBodyDiff={{this.calculateBodyDiff}}
|
||||
@titleDiff={{this.titleDiff}}
|
||||
/>
|
||||
</:body>
|
||||
<:footer>
|
||||
{{#if @model.editPost}}
|
||||
<Modal::History::TopicFooter
|
||||
@model={{this.postRevision}}
|
||||
@loadFirstVersion={{this.loadFirstVersion}}
|
||||
@loadPreviousVersion={{this.loadPreviousVersion}}
|
||||
@loadNextVersion={{this.loadNextVersion}}
|
||||
@loadLastVersion={{this.loadLastVersion}}
|
||||
@displayEdit={{this.displayEdit}}
|
||||
@editPost={{this.editPost}}
|
||||
@editButtonLabel={{this.editButtonLabel}}
|
||||
@revertToVersion={{this.revertToVersion}}
|
||||
@hideVersion={{this.hideVersion}}
|
||||
@showVersion={{this.showVersion}}
|
||||
@permanentlyDeleteVersions={{this.permanentlyDeleteVersions}}
|
||||
@loading={{this.loading}}
|
||||
@canPermanentlyDelete={{this.siteSettings.can_permanently_delete}}
|
||||
@loadFirstDisabled={{this.loadFirstDisabled}}
|
||||
@loadPreviousDisabled={{this.loadPreviousDisabled}}
|
||||
@displayRevisions={{this.displayRevisions}}
|
||||
@revisionsText={{this.revisionsText}}
|
||||
@loadNextDisabled={{this.loadNextDisabled}}
|
||||
@loadLastDisabled={{this.loadLastDisabled}}
|
||||
@revertToRevisionText={{this.revertToRevisionText}}
|
||||
@isStaff={{this.currentUser.staff}}
|
||||
/>
|
||||
{{/if}}
|
||||
</:footer>
|
||||
</DModal>
|
339
app/assets/javascripts/discourse/app/components/modal/history.js
Normal file
339
app/assets/javascripts/discourse/app/components/modal/history.js
Normal file
@ -0,0 +1,339 @@
|
||||
import { action } from "@ember/object";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import Category from "discourse/models/category";
|
||||
import Component from "@glimmer/component";
|
||||
import I18n from "I18n";
|
||||
import Post from "discourse/models/post";
|
||||
import { categoryBadgeHTML } from "discourse/helpers/category-link";
|
||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||
import { sanitizeAsync } from "discourse/lib/text";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
function customTagArray(val) {
|
||||
if (!val) {
|
||||
return [];
|
||||
}
|
||||
if (!Array.isArray(val)) {
|
||||
val = [val];
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
export default class History extends Component {
|
||||
@service dialog;
|
||||
@service site;
|
||||
@service currentUser;
|
||||
@service siteSettings;
|
||||
|
||||
@tracked loading;
|
||||
@tracked postRevision;
|
||||
@tracked viewMode = this.site?.mobileView ? "inline" : "side_by_side";
|
||||
@tracked bodyDiff;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.refresh(this.args.model.postId, this.args.model.postVersion);
|
||||
}
|
||||
|
||||
get loadFirstDisabled() {
|
||||
return (
|
||||
this.loading ||
|
||||
this.postRevision?.current_revision <= this.postRevision?.first_revision
|
||||
);
|
||||
}
|
||||
|
||||
get loadPreviousDisabled() {
|
||||
return (
|
||||
this.loading ||
|
||||
(!this.postRevision.previous_revision &&
|
||||
this.postRevision.current_revision <=
|
||||
this.postRevision.previous_revision)
|
||||
);
|
||||
}
|
||||
|
||||
get loadNextDisabled() {
|
||||
return (
|
||||
this.loading ||
|
||||
this.postRevision?.current_revision >= this.postRevision?.next_revision
|
||||
);
|
||||
}
|
||||
|
||||
get loadLastDisabled() {
|
||||
return (
|
||||
this.loading ||
|
||||
this.postRevision?.current_revision >= this.postRevision?.next_revision
|
||||
);
|
||||
}
|
||||
|
||||
get displayRevisions() {
|
||||
return this.postRevision?.version_count > 2;
|
||||
}
|
||||
|
||||
get modalTitleKey() {
|
||||
return this.args.model.post.version > 100
|
||||
? "history_capped_revisions"
|
||||
: "history";
|
||||
}
|
||||
|
||||
get previousVersion() {
|
||||
return this.postRevision?.current_version
|
||||
? this.postRevision.current_version - 1
|
||||
: null;
|
||||
}
|
||||
|
||||
get revisionsText() {
|
||||
return I18n.t(
|
||||
"post.revisions.controls.comparing_previous_to_current_out_of_total",
|
||||
{
|
||||
previous: this.previousVersion,
|
||||
icon: iconHTML("arrows-alt-h"),
|
||||
current: this.postRevision?.current_version,
|
||||
total: this.postRevision?.version_count,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
get titleDiff() {
|
||||
let mode = this.viewMode;
|
||||
if (mode === "side_by_side_markdown") {
|
||||
mode = "side_by_side";
|
||||
}
|
||||
return this.postRevision?.title_changes?.[mode];
|
||||
}
|
||||
|
||||
get bodyDiffHTML() {
|
||||
return this.postRevision?.body_changes?.[this.viewMode];
|
||||
}
|
||||
|
||||
@action
|
||||
async calculateBodyDiff(_, bodyDiff) {
|
||||
let html = bodyDiff;
|
||||
if (this.viewMode !== "side_by_side_markdown") {
|
||||
const opts = {
|
||||
features: { editHistory: true, historyOneboxes: true },
|
||||
allowListed: {
|
||||
editHistory: { custom: (tag, attr) => attr === "class" },
|
||||
historyOneboxes: ["header", "article", "div[style]"],
|
||||
},
|
||||
};
|
||||
html = await sanitizeAsync(html, opts);
|
||||
}
|
||||
this.bodyDiff = html;
|
||||
}
|
||||
|
||||
get previousTagChanges() {
|
||||
const previousArray = customTagArray(
|
||||
this.postRevision.tags_changes.previous
|
||||
);
|
||||
const currentSet = new Set(
|
||||
customTagArray(this.postRevision.tags_changes.current)
|
||||
);
|
||||
|
||||
return previousArray.map((name) => ({
|
||||
name,
|
||||
deleted: !currentSet.has(name),
|
||||
}));
|
||||
}
|
||||
|
||||
get currentTagChanges() {
|
||||
const previousSet = new Set(
|
||||
customTagArray(this.postRevision.tags_changes.previous)
|
||||
);
|
||||
const currentArray = customTagArray(this.postRevision.tags_changes.current);
|
||||
|
||||
return currentArray.map((name) => ({
|
||||
name,
|
||||
inserted: !previousSet.has(name),
|
||||
}));
|
||||
}
|
||||
|
||||
get createdAtDate() {
|
||||
return moment(this.postRevision.created_at).format("LLLL");
|
||||
}
|
||||
|
||||
get displayEdit() {
|
||||
return !!(
|
||||
this.postRevision?.can_edit &&
|
||||
this.args.model.editPost &&
|
||||
this.postRevision?.last_revision === this.postRevision?.current_revision
|
||||
);
|
||||
}
|
||||
|
||||
get revertToRevisionText() {
|
||||
return I18n.t("post.revisions.controls.revert", {
|
||||
revision: this.previousVersion,
|
||||
});
|
||||
}
|
||||
|
||||
refresh(postId, postVersion) {
|
||||
this.loading = true;
|
||||
Post.loadRevision(postId, postVersion).then((result) => {
|
||||
this.loading = false;
|
||||
this.postRevision = result;
|
||||
});
|
||||
}
|
||||
|
||||
hide(postId, postVersion) {
|
||||
Post.hideRevision(postId, postVersion).then(() =>
|
||||
this.refresh(postId, postVersion)
|
||||
);
|
||||
}
|
||||
|
||||
show(postId, postVersion) {
|
||||
Post.showRevision(postId, postVersion).then(() =>
|
||||
this.refresh(postId, postVersion)
|
||||
);
|
||||
}
|
||||
|
||||
revert(post, postVersion) {
|
||||
post
|
||||
.revertToRevision(postVersion)
|
||||
.then((result) => {
|
||||
this.refresh(post.id, postVersion);
|
||||
if (result.topic) {
|
||||
post.set("topic.slug", result.topic.slug);
|
||||
post.set("topic.title", result.topic.title);
|
||||
post.set("topic.fancy_title", result.topic.fancy_title);
|
||||
}
|
||||
if (result.category_id) {
|
||||
post.set("topic.category", Category.findById(result.category_id));
|
||||
}
|
||||
this.args.closeModal();
|
||||
})
|
||||
.catch(function (e) {
|
||||
if (e.jqXHR.responseJSON?.errors?.[0]) {
|
||||
this.dialog.alert(e.jqXHR.responseJSON.errors[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get editButtonLabel() {
|
||||
return `post.revisions.controls.${
|
||||
this.postRevision.wiki ? "edit_wiki" : "edit_post"
|
||||
}`;
|
||||
}
|
||||
|
||||
get hiddenClasses() {
|
||||
if (this.viewMode === "inline") {
|
||||
return this.postRevision?.previous_hidden ||
|
||||
this.postRevision?.current_hidden
|
||||
? "hidden-revision-either"
|
||||
: null;
|
||||
} else {
|
||||
let result = [];
|
||||
if (this.postRevision?.previous_hidden) {
|
||||
result.push("hidden-revision-previous");
|
||||
}
|
||||
if (this.postRevision?.current_hidden) {
|
||||
result.push("hidden-revision-current");
|
||||
}
|
||||
return result.join(" ");
|
||||
}
|
||||
}
|
||||
|
||||
get previousCategory() {
|
||||
if (this.postRevision?.category_id_changes) {
|
||||
let category = Category.findById(
|
||||
this.postRevision.category_id_changes.previous
|
||||
);
|
||||
return categoryBadgeHTML(category, { allowUncategorized: true });
|
||||
}
|
||||
}
|
||||
|
||||
get currentCategory() {
|
||||
if (this.postRevision?.category_id_changes) {
|
||||
let category = Category.findById(
|
||||
this.postRevision.category_id_changes.current
|
||||
);
|
||||
return categoryBadgeHTML(category, { allowUncategorized: true });
|
||||
}
|
||||
}
|
||||
|
||||
get wikiDisabled() {
|
||||
return !this.postRevision.wiki_changes?.current;
|
||||
}
|
||||
|
||||
get postTypeDisabled() {
|
||||
return (
|
||||
this.postRevision?.post_type_changes?.current !==
|
||||
this.site.post_types.moderator_action
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
displayInline(event) {
|
||||
event?.preventDefault();
|
||||
this.viewMode = "inline";
|
||||
}
|
||||
|
||||
@action
|
||||
displaySideBySide(event) {
|
||||
event?.preventDefault();
|
||||
this.viewMode = "side_by_side";
|
||||
}
|
||||
|
||||
@action
|
||||
displaySideBySideMarkdown(event) {
|
||||
event?.preventDefault();
|
||||
this.viewMode = "side_by_side_markdown";
|
||||
}
|
||||
|
||||
@action
|
||||
loadFirstVersion() {
|
||||
this.refresh(this.postRevision.post_id, this.postRevision.first_revision);
|
||||
}
|
||||
|
||||
@action
|
||||
loadPreviousVersion() {
|
||||
this.refresh(
|
||||
this.postRevision.post_id,
|
||||
this.postRevision.previous_revision
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
loadNextVersion() {
|
||||
this.refresh(this.postRevision.post_id, this.postRevision.next_revision);
|
||||
}
|
||||
|
||||
@action
|
||||
loadLastVersion() {
|
||||
return this.refresh(
|
||||
this.postRevision.post_id,
|
||||
this.postRevision.last_revision
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
hideVersion() {
|
||||
this.hide(this.postRevision.post_id, this.postRevision.current_revision);
|
||||
}
|
||||
|
||||
@action
|
||||
permanentlyDeleteVersions() {
|
||||
this.dialog.yesNoConfirm({
|
||||
message: I18n.t("post.revisions.controls.destroy_confirm"),
|
||||
didConfirm: () => {
|
||||
Post.permanentlyDeleteRevisions(this.postRevision.post_id).then(() => {
|
||||
this.args.closeModal();
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
showVersion() {
|
||||
this.show(this.postRevision.post_id, this.postRevision.current_revision);
|
||||
}
|
||||
|
||||
@action
|
||||
editPost() {
|
||||
this.args.model.editPost(this.args.model.post);
|
||||
this.args.closeModal();
|
||||
}
|
||||
|
||||
@action
|
||||
revertToVersion() {
|
||||
this.revert(this.args.model.post, this.postRevision.current_revision);
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
<div id="revision">
|
||||
<div id="revision-details">
|
||||
{{d-icon "pencil-alt"}}
|
||||
<LinkTo @route="user" @model={{@model.username}}>
|
||||
{{bound-avatar-template @model.avatar_template "small"}}
|
||||
{{@model.username}}
|
||||
</LinkTo>
|
||||
<PluginOutlet
|
||||
@name="revision-user-details-after"
|
||||
@outletArgs={{hash model=@model}}
|
||||
/>
|
||||
<span class="date">{{bound-date @model.created_at}}</span>
|
||||
{{#if @model.edit_reason}}
|
||||
—
|
||||
<span class="edit-reason">{{@model.edit_reason}}</span>
|
||||
{{/if}}
|
||||
{{#unless @mobileView}}
|
||||
{{#if @model.user_changes}}
|
||||
—
|
||||
{{bound-avatar-template
|
||||
@model.user_changes.previous.avatar_template
|
||||
"small"
|
||||
}}
|
||||
{{@model.user_changes.previous.username}}
|
||||
→
|
||||
{{bound-avatar-template
|
||||
@model.user_changes.current.avatar_template
|
||||
"small"
|
||||
}}
|
||||
{{@model.user_changes.current.username}}
|
||||
{{/if}}
|
||||
{{#if @model.wiki_changes}}
|
||||
—
|
||||
<DisabledIcon @icon="far-edit" @disabled={{@wikiDisabled}} />
|
||||
{{/if}}
|
||||
{{#if @model.post_type_changes}}
|
||||
—
|
||||
<DisabledIcon @icon="shield-alt" @disabled={{@postTypeDisabled}} />
|
||||
{{/if}}
|
||||
{{#if @model.category_id_changes}}
|
||||
—
|
||||
{{html-safe @previousCategory}}
|
||||
→
|
||||
{{html-safe @currentCategory}}
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
</div>
|
||||
{{#unless @mobileView}}
|
||||
<div id="display-modes">
|
||||
<ul class="nav nav-pills">
|
||||
<li>
|
||||
<a
|
||||
href
|
||||
class={{concat-class
|
||||
"inline-mode"
|
||||
(if (eq @viewMode "inline") "active")
|
||||
}}
|
||||
{{on "click" @displayInline}}
|
||||
title={{i18n "post.revisions.displays.inline.title"}}
|
||||
aria-label={{i18n "post.revisions.displays.inline.title"}}
|
||||
>
|
||||
{{d-icon "far-square"}}
|
||||
{{i18n "post.revisions.displays.inline.button"}}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href
|
||||
class={{concat-class
|
||||
"side-by-side-mode"
|
||||
(if (eq @viewMode "side_by_side") "active")
|
||||
}}
|
||||
{{on "click" @displaySideBySide}}
|
||||
title={{i18n "post.revisions.displays.side_by_side.title"}}
|
||||
aria-label={{i18n "post.revisions.displays.side_by_side.title"}}
|
||||
>
|
||||
{{d-icon "columns"}}
|
||||
{{i18n "post.revisions.displays.side_by_side.button"}}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href
|
||||
class={{concat-class
|
||||
"side-by-side-markdown-mode"
|
||||
(if (eq @viewMode "side_by_side_markdown") "active")
|
||||
}}
|
||||
{{on "click" @displaySideBySideMarkdown}}
|
||||
title={{i18n "post.revisions.displays.side_by_side_markdown.title"}}
|
||||
aria-label={{i18n
|
||||
"post.revisions.displays.side_by_side_markdown.title"
|
||||
}}
|
||||
>
|
||||
{{d-icon "columns"}}
|
||||
{{i18n "post.revisions.displays.side_by_side_markdown.button"}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{/unless}}
|
||||
</div>
|
@ -0,0 +1,75 @@
|
||||
<div id="revisions" data-post-id={{@model.post_id}} class={{@hiddenClasses}}>
|
||||
{{#if @model.title_changes}}
|
||||
<div class="row">
|
||||
<h2>{{html-safe @titleDiff}}</h2>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if @mobileView}}
|
||||
{{#if @userChanges}}
|
||||
<div class="row">
|
||||
{{bound-avatar-template
|
||||
@model.user_changes.previous.avatar_template
|
||||
"small"
|
||||
}}
|
||||
{{@model.user_changes.previous.username}}
|
||||
→
|
||||
{{bound-avatar-template
|
||||
@model.user_changes.current.avatar_template
|
||||
"small"
|
||||
}}
|
||||
{{@model.user_changes.current.username}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if @model.wiki_changes}}
|
||||
<div class="row">
|
||||
<DisabledIcon @icon="far-edit" @disabled={{@wikiDisabled}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if @model.post_type_changes}}
|
||||
<div class="row">
|
||||
<DisabledIcon @icon="shield-alt" @disabled={{@postTypeDisabled}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if @model.category_id_changes}}
|
||||
<div class="row">
|
||||
{{html-safe @previousCategory}}
|
||||
→
|
||||
{{html-safe @currentCategory}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if @model.tags_changes}}
|
||||
<div class="row">
|
||||
{{i18n "tagging.changed"}}
|
||||
{{#each @previousTagChanges as |t|}}
|
||||
{{discourse-tag t.name style=(if t.deleted "diff-del")}}
|
||||
{{/each}}
|
||||
→
|
||||
{{#each @currentTagChanges as |t|}}
|
||||
{{discourse-tag t.name style=(if t.inserted "diff-ins")}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if @model.featured_link_changes}}
|
||||
<div class="row">
|
||||
{{@model.featured_link_changes.previous}}
|
||||
→
|
||||
{{@model.featured_link_changes.current}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<span>
|
||||
<PluginOutlet
|
||||
@name="post-revisions"
|
||||
@connectorTagName="div"
|
||||
@outletArgs={{hash model=@model}}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<LinksRedirect
|
||||
@class="row body-diff"
|
||||
{{did-update @calculateBodyDiff @bodyDiffHTML}}
|
||||
>
|
||||
{{html-safe @bodyDiff}}
|
||||
</LinksRedirect>
|
||||
</div>
|
@ -0,0 +1,84 @@
|
||||
<div id="revision-controls">
|
||||
<DButton
|
||||
class="btn-default first-revision"
|
||||
@action={{@loadFirstVersion}}
|
||||
@icon="fast-backward"
|
||||
@title="post.revisions.controls.first"
|
||||
@disabled={{@loadFirstDisabled}}
|
||||
/>
|
||||
<DButton
|
||||
class="btn-default previous-revision"
|
||||
@action={{@loadPreviousVersion}}
|
||||
@icon="backward"
|
||||
@title="post.revisions.controls.previous"
|
||||
@disabled={{@loadPreviousDisabled}}
|
||||
/>
|
||||
<div id="revision-numbers" class={{unless @displayRevisions "invisible"}}>
|
||||
<ConditionalLoadingSpinner @condition={{@loading}} @size="small">
|
||||
{{html-safe @revisionsText}}
|
||||
</ConditionalLoadingSpinner>
|
||||
</div>
|
||||
<DButton
|
||||
class="btn-default next-revision"
|
||||
@action={{@loadNextVersion}}
|
||||
@icon="forward"
|
||||
@title="post.revisions.controls.next"
|
||||
@disabled={{@loadNextDisabled}}
|
||||
/>
|
||||
<DButton
|
||||
class="btn-default last-revision"
|
||||
@action={{@loadLastVersion}}
|
||||
@icon="fast-forward"
|
||||
@title="post.revisions.controls.last"
|
||||
@disabled={{@loadLastDisabled}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="revision-footer-buttons">
|
||||
{{#if @displayEdit}}
|
||||
<DButton
|
||||
@action={{@editPost}}
|
||||
@icon="pencil-alt"
|
||||
class="btn-default edit-post"
|
||||
@label={{@editButtonLabel}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if @isStaff}}
|
||||
<DButton
|
||||
@action={{@revertToVersion}}
|
||||
@icon="undo"
|
||||
@translatedLabel={{@revertToRevisionText}}
|
||||
class="btn-danger revert-to-version"
|
||||
@disabled={{@loading}}
|
||||
/>
|
||||
|
||||
{{#if (not @model.previous_hidden)}}
|
||||
<DButton
|
||||
@action={{@hideVersion}}
|
||||
@icon="far-eye-slash"
|
||||
@label="post.revisions.controls.hide"
|
||||
class="btn-danger hide-revision"
|
||||
@disabled={{@loading}}
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
@action={{@showVersion}}
|
||||
@icon="far-eye"
|
||||
@label="post.revisions.controls.show"
|
||||
class="btn-default show-revision"
|
||||
@disabled={{@loading}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if (and @canPermanentlyDelete @model.previous_hidden)}}
|
||||
<DButton
|
||||
@action={{@permanentlyDeleteVersions}}
|
||||
@icon="far-trash-alt"
|
||||
@label="post.revisions.controls.destroy"
|
||||
class="btn-danger destroy-revision"
|
||||
@disabled={{@loading}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
@ -4,9 +4,12 @@ import { action } from "@ember/object";
|
||||
import { gt } from "@ember/object/computed";
|
||||
import { historyHeat } from "discourse/widgets/post-edits-indicator";
|
||||
import { longDate } from "discourse/lib/formatter";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import { inject as service } from "@ember/service";
|
||||
import HistoryModal from "discourse/components/modal/history";
|
||||
|
||||
export default Component.extend({
|
||||
modal: service(),
|
||||
|
||||
hasEdits: gt("reviewable.post_version", 1),
|
||||
|
||||
@discourseComputed("reviewable.post_updated_at")
|
||||
@ -24,13 +27,14 @@ export default Component.extend({
|
||||
event?.preventDefault();
|
||||
let postId = this.get("reviewable.post_id");
|
||||
this.store.find("post", postId).then((post) => {
|
||||
let historyController = showModal("history", {
|
||||
model: post,
|
||||
modalClass: "history-modal",
|
||||
this.modal.show(HistoryModal, {
|
||||
model: {
|
||||
post,
|
||||
postId,
|
||||
postVersion: "latest",
|
||||
topicController: null,
|
||||
},
|
||||
});
|
||||
historyController.refresh(postId, "latest");
|
||||
historyController.set("post", post);
|
||||
historyController.set("topicController", null);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -1,392 +0,0 @@
|
||||
import { action } from "@ember/object";
|
||||
import { alias, equal, gt, not, or } from "@ember/object/computed";
|
||||
import discourseComputed, {
|
||||
observes,
|
||||
on,
|
||||
} from "discourse-common/utils/decorators";
|
||||
import { propertyGreaterThan, propertyLessThan } from "discourse/lib/computed";
|
||||
import Category from "discourse/models/category";
|
||||
import Controller from "@ember/controller";
|
||||
import I18n from "I18n";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import Post from "discourse/models/post";
|
||||
import { categoryBadgeHTML } from "discourse/helpers/category-link";
|
||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||
import { sanitizeAsync } from "discourse/lib/text";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
function customTagArray(val) {
|
||||
if (!val) {
|
||||
return [];
|
||||
}
|
||||
if (!Array.isArray(val)) {
|
||||
val = [val];
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
// This controller handles displaying of history
|
||||
export default Controller.extend(ModalFunctionality, {
|
||||
dialog: service(),
|
||||
loading: true,
|
||||
viewMode: "side_by_side",
|
||||
|
||||
@on("init")
|
||||
_changeViewModeOnMobile() {
|
||||
if (this.site && this.site.mobileView) {
|
||||
this.set("viewMode", "inline");
|
||||
}
|
||||
},
|
||||
|
||||
previousFeaturedLink: alias("model.featured_link_changes.previous"),
|
||||
currentFeaturedLink: alias("model.featured_link_changes.current"),
|
||||
|
||||
@discourseComputed(
|
||||
"model.tags_changes.previous",
|
||||
"model.tags_changes.current"
|
||||
)
|
||||
previousTagChanges(previous, current) {
|
||||
const previousArray = customTagArray(previous);
|
||||
const currentSet = new Set(customTagArray(current));
|
||||
|
||||
return previousArray.map((name) => ({
|
||||
name,
|
||||
deleted: !currentSet.has(name),
|
||||
}));
|
||||
},
|
||||
|
||||
@discourseComputed(
|
||||
"model.tags_changes.previous",
|
||||
"model.tags_changes.current"
|
||||
)
|
||||
currentTagChanges(previous, current) {
|
||||
const previousSet = new Set(customTagArray(previous));
|
||||
const currentArray = customTagArray(current);
|
||||
|
||||
return currentArray.map((name) => ({
|
||||
name,
|
||||
inserted: !previousSet.has(name),
|
||||
}));
|
||||
},
|
||||
|
||||
@discourseComputed("post.version")
|
||||
modalTitleKey(version) {
|
||||
if (version > 100) {
|
||||
return "history_capped_revisions";
|
||||
} else {
|
||||
return "history";
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed(
|
||||
"previousVersion",
|
||||
"model.current_version",
|
||||
"model.version_count"
|
||||
)
|
||||
revisionsText(previous, current, total) {
|
||||
return I18n.t(
|
||||
"post.revisions.controls.comparing_previous_to_current_out_of_total",
|
||||
{
|
||||
previous,
|
||||
icon: iconHTML("arrows-alt-h"),
|
||||
current,
|
||||
total,
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
@discourseComputed("previousVersion")
|
||||
revertToRevisionText(revision) {
|
||||
return I18n.t("post.revisions.controls.revert", { revision });
|
||||
},
|
||||
|
||||
refresh(postId, postVersion) {
|
||||
this.set("loading", true);
|
||||
|
||||
Post.loadRevision(postId, postVersion).then((result) => {
|
||||
this.setProperties({ loading: false, model: result });
|
||||
});
|
||||
},
|
||||
|
||||
hide(postId, postVersion) {
|
||||
Post.hideRevision(postId, postVersion).then(() =>
|
||||
this.refresh(postId, postVersion)
|
||||
);
|
||||
},
|
||||
|
||||
permanentlyDeleteRevisions(postId) {
|
||||
this.dialog.yesNoConfirm({
|
||||
message: I18n.t("post.revisions.controls.destroy_confirm"),
|
||||
didConfirm: () => {
|
||||
Post.permanentlyDeleteRevisions(postId).then(() => {
|
||||
this.send("closeModal");
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
show(postId, postVersion) {
|
||||
Post.showRevision(postId, postVersion).then(() =>
|
||||
this.refresh(postId, postVersion)
|
||||
);
|
||||
},
|
||||
|
||||
revert(post, postVersion) {
|
||||
post
|
||||
.revertToRevision(postVersion)
|
||||
.then((result) => {
|
||||
this.refresh(post.get("id"), postVersion);
|
||||
if (result.topic) {
|
||||
post.set("topic.slug", result.topic.slug);
|
||||
post.set("topic.title", result.topic.title);
|
||||
post.set("topic.fancy_title", result.topic.fancy_title);
|
||||
}
|
||||
if (result.category_id) {
|
||||
post.set("topic.category", Category.findById(result.category_id));
|
||||
}
|
||||
this.send("closeModal");
|
||||
})
|
||||
.catch(function (e) {
|
||||
if (
|
||||
e.jqXHR.responseJSON &&
|
||||
e.jqXHR.responseJSON.errors &&
|
||||
e.jqXHR.responseJSON.errors[0]
|
||||
) {
|
||||
this.dialog.alert(e.jqXHR.responseJSON.errors[0]);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@discourseComputed("model.created_at")
|
||||
createdAtDate(createdAt) {
|
||||
return moment(createdAt).format("LLLL");
|
||||
},
|
||||
|
||||
@discourseComputed("model.current_version")
|
||||
previousVersion(current) {
|
||||
return current - 1;
|
||||
},
|
||||
|
||||
@discourseComputed("model.current_revision", "model.previous_revision")
|
||||
displayGoToPrevious(current, prev) {
|
||||
return prev && current > prev;
|
||||
},
|
||||
|
||||
displayRevisions: gt("model.version_count", 2),
|
||||
|
||||
displayGoToFirst: propertyGreaterThan(
|
||||
"model.current_revision",
|
||||
"model.first_revision"
|
||||
),
|
||||
displayGoToNext: propertyLessThan(
|
||||
"model.current_revision",
|
||||
"model.next_revision"
|
||||
),
|
||||
displayGoToLast: propertyLessThan(
|
||||
"model.current_revision",
|
||||
"model.next_revision"
|
||||
),
|
||||
|
||||
hideGoToFirst: not("displayGoToFirst"),
|
||||
hideGoToPrevious: not("displayGoToPrevious"),
|
||||
hideGoToNext: not("displayGoToNext"),
|
||||
hideGoToLast: not("displayGoToLast"),
|
||||
|
||||
loadFirstDisabled: or("loading", "hideGoToFirst"),
|
||||
loadPreviousDisabled: or("loading", "hideGoToPrevious"),
|
||||
loadNextDisabled: or("loading", "hideGoToNext"),
|
||||
loadLastDisabled: or("loading", "hideGoToLast"),
|
||||
|
||||
@discourseComputed("model.previous_hidden")
|
||||
displayShow(prevHidden) {
|
||||
return prevHidden && this.currentUser && this.currentUser.get("staff");
|
||||
},
|
||||
|
||||
@discourseComputed("model.previous_hidden")
|
||||
displayHide(prevHidden) {
|
||||
return !prevHidden && this.currentUser && this.currentUser.get("staff");
|
||||
},
|
||||
|
||||
@discourseComputed(
|
||||
"model.last_revision",
|
||||
"model.current_revision",
|
||||
"model.can_edit",
|
||||
"topicController"
|
||||
)
|
||||
displayEdit(lastRevision, currentRevision, canEdit, topicController) {
|
||||
return !!(canEdit && topicController && lastRevision === currentRevision);
|
||||
},
|
||||
|
||||
@discourseComputed("model.wiki")
|
||||
editButtonLabel(wiki) {
|
||||
return `post.revisions.controls.${wiki ? "edit_wiki" : "edit_post"}`;
|
||||
},
|
||||
|
||||
@discourseComputed()
|
||||
displayRevert() {
|
||||
return this.currentUser && this.currentUser.get("staff");
|
||||
},
|
||||
|
||||
@discourseComputed("model.previous_hidden")
|
||||
displayPermanentlyDeleteButton(previousHidden) {
|
||||
return (
|
||||
this.siteSettings.can_permanently_delete &&
|
||||
this.currentUser?.staff &&
|
||||
previousHidden
|
||||
);
|
||||
},
|
||||
|
||||
isEitherRevisionHidden: or("model.previous_hidden", "model.current_hidden"),
|
||||
|
||||
@discourseComputed(
|
||||
"model.previous_hidden",
|
||||
"model.current_hidden",
|
||||
"displayingInline"
|
||||
)
|
||||
hiddenClasses(prevHidden, currentHidden, displayingInline) {
|
||||
if (displayingInline) {
|
||||
return this.isEitherRevisionHidden ? "hidden-revision-either" : null;
|
||||
} else {
|
||||
let result = [];
|
||||
if (prevHidden) {
|
||||
result.push("hidden-revision-previous");
|
||||
}
|
||||
if (currentHidden) {
|
||||
result.push("hidden-revision-current");
|
||||
}
|
||||
return result.join(" ");
|
||||
}
|
||||
},
|
||||
|
||||
displayingInline: equal("viewMode", "inline"),
|
||||
displayingSideBySide: equal("viewMode", "side_by_side"),
|
||||
displayingSideBySideMarkdown: equal("viewMode", "side_by_side_markdown"),
|
||||
|
||||
@discourseComputed("displayingInline")
|
||||
inlineClass(displayingInline) {
|
||||
return displayingInline ? "active" : "";
|
||||
},
|
||||
|
||||
@discourseComputed("displayingSideBySide")
|
||||
sideBySideClass(displayingSideBySide) {
|
||||
return displayingSideBySide ? "active" : "";
|
||||
},
|
||||
|
||||
@discourseComputed("displayingSideBySideMarkdown")
|
||||
sideBySideMarkdownClass(displayingSideBySideMarkdown) {
|
||||
return displayingSideBySideMarkdown ? "active" : "";
|
||||
},
|
||||
|
||||
@discourseComputed("model.category_id_changes")
|
||||
previousCategory(changes) {
|
||||
if (changes) {
|
||||
let category = Category.findById(changes["previous"]);
|
||||
return categoryBadgeHTML(category, { allowUncategorized: true });
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed("model.category_id_changes")
|
||||
currentCategory(changes) {
|
||||
if (changes) {
|
||||
let category = Category.findById(changes["current"]);
|
||||
return categoryBadgeHTML(category, { allowUncategorized: true });
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed("model.wiki_changes")
|
||||
wikiDisabled(changes) {
|
||||
return changes && !changes["current"];
|
||||
},
|
||||
|
||||
@discourseComputed("model.post_type_changes")
|
||||
postTypeDisabled(changes) {
|
||||
return (
|
||||
changes &&
|
||||
changes["current"] !== this.site.get("post_types.moderator_action")
|
||||
);
|
||||
},
|
||||
|
||||
@discourseComputed("viewMode", "model.title_changes")
|
||||
titleDiff(viewMode) {
|
||||
if (viewMode === "side_by_side_markdown") {
|
||||
viewMode = "side_by_side";
|
||||
}
|
||||
return this.get("model.title_changes." + viewMode);
|
||||
},
|
||||
|
||||
@observes("viewMode", "model.body_changes")
|
||||
bodyDiffChanged() {
|
||||
const viewMode = this.viewMode;
|
||||
const html = this.get(`model.body_changes.${viewMode}`);
|
||||
if (viewMode === "side_by_side_markdown") {
|
||||
this.set("bodyDiff", html);
|
||||
} else {
|
||||
const opts = {
|
||||
features: { editHistory: true, historyOneboxes: true },
|
||||
allowListed: {
|
||||
editHistory: { custom: (tag, attr) => attr === "class" },
|
||||
historyOneboxes: ["header", "article", "div[style]"],
|
||||
},
|
||||
};
|
||||
|
||||
return sanitizeAsync(html, opts).then((result) =>
|
||||
this.set("bodyDiff", result)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@action
|
||||
displayInline(event) {
|
||||
event?.preventDefault();
|
||||
this.set("viewMode", "inline");
|
||||
},
|
||||
|
||||
@action
|
||||
displaySideBySide(event) {
|
||||
event?.preventDefault();
|
||||
this.set("viewMode", "side_by_side");
|
||||
},
|
||||
|
||||
@action
|
||||
displaySideBySideMarkdown(event) {
|
||||
event?.preventDefault();
|
||||
this.set("viewMode", "side_by_side_markdown");
|
||||
},
|
||||
|
||||
actions: {
|
||||
loadFirstVersion() {
|
||||
this.refresh(this.get("model.post_id"), this.get("model.first_revision"));
|
||||
},
|
||||
loadPreviousVersion() {
|
||||
this.refresh(
|
||||
this.get("model.post_id"),
|
||||
this.get("model.previous_revision")
|
||||
);
|
||||
},
|
||||
loadNextVersion() {
|
||||
this.refresh(this.get("model.post_id"), this.get("model.next_revision"));
|
||||
},
|
||||
loadLastVersion() {
|
||||
this.refresh(this.get("model.post_id"), this.get("model.last_revision"));
|
||||
},
|
||||
|
||||
hideVersion() {
|
||||
this.hide(this.get("model.post_id"), this.get("model.current_revision"));
|
||||
},
|
||||
permanentlyDeleteVersions() {
|
||||
this.permanentlyDeleteRevisions(this.get("model.post_id"));
|
||||
},
|
||||
showVersion() {
|
||||
this.show(this.get("model.post_id"), this.get("model.current_revision"));
|
||||
},
|
||||
|
||||
editPost() {
|
||||
this.topicController.send("editPost", this.post);
|
||||
this.send("closeModal");
|
||||
},
|
||||
|
||||
revertToVersion() {
|
||||
this.revert(this.post, this.get("model.current_revision"));
|
||||
},
|
||||
},
|
||||
});
|
@ -10,12 +10,14 @@ import { setTopicId } from "discourse/lib/topic-list-tracker";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import TopicFlag from "discourse/lib/flag-targets/topic-flag";
|
||||
import PostFlag from "discourse/lib/flag-targets/post-flag";
|
||||
import HistoryModal from "discourse/components/modal/history";
|
||||
|
||||
const SCROLL_DELAY = 500;
|
||||
|
||||
const TopicRoute = DiscourseRoute.extend({
|
||||
composer: service(),
|
||||
screenTrack: service(),
|
||||
modal: service(),
|
||||
|
||||
scheduledReplace: null,
|
||||
lastScrollPos: null,
|
||||
@ -153,13 +155,14 @@ const TopicRoute = DiscourseRoute.extend({
|
||||
|
||||
@action
|
||||
showHistory(model, revision) {
|
||||
let historyController = showModal("history", {
|
||||
model,
|
||||
modalClass: "history-modal",
|
||||
this.modal.show(HistoryModal, {
|
||||
model: {
|
||||
postId: model.id,
|
||||
postVersion: revision || "latest",
|
||||
post: model,
|
||||
editPost: (post) => this.controllerFor("topic").send("editPost", post),
|
||||
},
|
||||
});
|
||||
historyController.refresh(model.get("id"), revision || "latest");
|
||||
historyController.set("post", model);
|
||||
historyController.set("topicController", this.controllerFor("topic"));
|
||||
},
|
||||
|
||||
@action
|
||||
|
@ -1,271 +0,0 @@
|
||||
<DModalBody @title={{this.modalTitleKey}}>
|
||||
<div id="revision">
|
||||
<div id="revision-details">
|
||||
{{d-icon "pencil-alt"}}
|
||||
<LinkTo @route="user" @model={{this.model.username}}>
|
||||
{{bound-avatar-template this.model.avatar_template "small"}}
|
||||
{{this.model.username}}
|
||||
</LinkTo>
|
||||
<PluginOutlet
|
||||
@name="revision-user-details-after"
|
||||
@outletArgs={{hash model=this.model}}
|
||||
/>
|
||||
<span class="date">{{bound-date this.model.created_at}}</span>
|
||||
{{#if this.model.edit_reason}}
|
||||
—
|
||||
<span class="edit-reason">{{this.model.edit_reason}}</span>
|
||||
{{/if}}
|
||||
{{#unless this.site.mobileView}}
|
||||
{{#if this.model.user_changes}}
|
||||
—
|
||||
{{bound-avatar-template
|
||||
this.model.user_changes.previous.avatar_template
|
||||
"small"
|
||||
}}
|
||||
{{this.model.user_changes.previous.username}}
|
||||
→
|
||||
{{bound-avatar-template
|
||||
this.model.user_changes.current.avatar_template
|
||||
"small"
|
||||
}}
|
||||
{{this.model.user_changes.current.username}}
|
||||
{{/if}}
|
||||
{{#if this.model.wiki_changes}}
|
||||
—
|
||||
<DisabledIcon @icon="far-edit" @disabled={{this.wikiDisabled}} />
|
||||
{{/if}}
|
||||
{{#if this.model.post_type_changes}}
|
||||
—
|
||||
<DisabledIcon
|
||||
@icon="shield-alt"
|
||||
@disabled={{this.postTypeDisabled}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if this.model.category_id_changes}}
|
||||
—
|
||||
{{html-safe this.previousCategory}}
|
||||
→
|
||||
{{html-safe this.currentCategory}}
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
</div>
|
||||
{{#unless this.site.mobileView}}
|
||||
<div id="display-modes">
|
||||
<ul class="nav nav-pills">
|
||||
<li>
|
||||
<a
|
||||
href
|
||||
class={{this.inlineClass}}
|
||||
{{on "click" this.displayInline}}
|
||||
title={{i18n "post.revisions.displays.inline.title"}}
|
||||
aria-label={{i18n "post.revisions.displays.inline.title"}}
|
||||
>
|
||||
{{d-icon "far-square"}}
|
||||
{{i18n "post.revisions.displays.inline.button"}}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href
|
||||
class={{this.sideBySideClass}}
|
||||
{{on "click" this.displaySideBySide}}
|
||||
title={{i18n "post.revisions.displays.side_by_side.title"}}
|
||||
aria-label={{i18n "post.revisions.displays.side_by_side.title"}}
|
||||
>
|
||||
{{d-icon "columns"}}
|
||||
{{i18n "post.revisions.displays.side_by_side.button"}}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href
|
||||
class={{this.sideBySideMarkdownClass}}
|
||||
{{on "click" this.displaySideBySideMarkdown}}
|
||||
title={{i18n
|
||||
"post.revisions.displays.side_by_side_markdown.title"
|
||||
}}
|
||||
aria-label={{i18n
|
||||
"post.revisions.displays.side_by_side_markdown.title"
|
||||
}}
|
||||
>
|
||||
{{d-icon "columns"}}
|
||||
{{i18n "post.revisions.displays.side_by_side_markdown.button"}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{/unless}}
|
||||
</div>
|
||||
<div
|
||||
id="revisions"
|
||||
data-post-id={{this.model.post_id}}
|
||||
class={{this.hiddenClasses}}
|
||||
>
|
||||
{{#if this.model.title_changes}}
|
||||
<div class="row">
|
||||
<h2>{{html-safe this.titleDiff}}</h2>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if this.site.mobileView}}
|
||||
{{#if this.user_changes}}
|
||||
<div class="row">
|
||||
{{bound-avatar-template
|
||||
this.model.user_changes.previous.avatar_template
|
||||
"small"
|
||||
}}
|
||||
{{this.model.user_changes.previous.username}}
|
||||
→
|
||||
{{bound-avatar-template
|
||||
this.model.user_changes.current.avatar_template
|
||||
"small"
|
||||
}}
|
||||
{{this.model.user_changes.current.username}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if this.model.wiki_changes}}
|
||||
<div class="row">
|
||||
<DisabledIcon @icon="far-edit" @disabled={{this.wikiDisabled}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if this.model.post_type_changes}}
|
||||
<div class="row">
|
||||
<DisabledIcon
|
||||
@icon="shield-alt"
|
||||
@disabled={{this.postTypeDisabled}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if this.model.category_id_changes}}
|
||||
<div class="row">
|
||||
{{html-safe this.previousCategory}}
|
||||
→
|
||||
{{html-safe this.currentCategory}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if this.model.tags_changes}}
|
||||
<div class="row">
|
||||
{{i18n "tagging.changed"}}
|
||||
{{#each this.previousTagChanges as |t|}}
|
||||
{{discourse-tag t.name style=(if t.deleted "diff-del")}}
|
||||
{{/each}}
|
||||
→
|
||||
{{#each this.currentTagChanges as |t|}}
|
||||
{{discourse-tag t.name style=(if t.inserted "diff-ins")}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if this.model.featured_link_changes}}
|
||||
<div class="row">
|
||||
{{this.model.featured_link_changes.previous}}
|
||||
→
|
||||
{{this.model.featured_link_changes.current}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<span>
|
||||
<PluginOutlet
|
||||
@name="post-revisions"
|
||||
@connectorTagName="div"
|
||||
@outletArgs={{hash model=this.model}}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<LinksRedirect @class="row">
|
||||
{{html-safe this.bodyDiff}}
|
||||
</LinksRedirect>
|
||||
</div>
|
||||
</DModalBody>
|
||||
{{#if this.topicController}}
|
||||
<div class="modal-footer">
|
||||
<div id="revision-controls">
|
||||
<DButton
|
||||
@class="btn-default"
|
||||
@action={{action "loadFirstVersion"}}
|
||||
@icon="fast-backward"
|
||||
@title="post.revisions.controls.first"
|
||||
@disabled={{this.loadFirstDisabled}}
|
||||
/>
|
||||
<DButton
|
||||
@class="btn-default"
|
||||
@action={{action "loadPreviousVersion"}}
|
||||
@icon="backward"
|
||||
@title="post.revisions.controls.previous"
|
||||
@disabled={{this.loadPreviousDisabled}}
|
||||
/>
|
||||
<div
|
||||
id="revision-numbers"
|
||||
class={{unless this.displayRevisions "invisible"}}
|
||||
>
|
||||
<ConditionalLoadingSpinner @condition={{this.loading}} @size="small">
|
||||
{{html-safe this.revisionsText}}
|
||||
</ConditionalLoadingSpinner>
|
||||
</div>
|
||||
<DButton
|
||||
@class="btn-default"
|
||||
@action={{action "loadNextVersion"}}
|
||||
@icon="forward"
|
||||
@title="post.revisions.controls.next"
|
||||
@disabled={{this.loadNextDisabled}}
|
||||
/>
|
||||
<DButton
|
||||
@class="btn-default"
|
||||
@action={{action "loadLastVersion"}}
|
||||
@icon="fast-forward"
|
||||
@title="post.revisions.controls.last"
|
||||
@disabled={{this.loadLastDisabled}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="revision-footer-buttons">
|
||||
{{#if this.displayEdit}}
|
||||
<DButton
|
||||
@action={{action "editPost"}}
|
||||
@icon="pencil-alt"
|
||||
@class="btn-default"
|
||||
@label={{this.editButtonLabel}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.displayRevert}}
|
||||
<DButton
|
||||
@action={{action "revertToVersion"}}
|
||||
@icon="undo"
|
||||
@translatedLabel={{this.revertToRevisionText}}
|
||||
@class="btn-danger"
|
||||
@disabled={{this.loading}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.displayHide}}
|
||||
<DButton
|
||||
@action={{action "hideVersion"}}
|
||||
@icon="far-eye-slash"
|
||||
@label="post.revisions.controls.hide"
|
||||
@class="btn-danger"
|
||||
@disabled={{this.loading}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.displayShow}}
|
||||
<DButton
|
||||
@action={{action "showVersion"}}
|
||||
@icon="far-eye"
|
||||
@label="post.revisions.controls.show"
|
||||
@class="btn-default"
|
||||
@disabled={{this.loading}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.displayPermanentlyDeleteButton}}
|
||||
<DButton
|
||||
@action={{action "permanentlyDeleteVersions"}}
|
||||
@icon="far-trash-alt"
|
||||
@label="post.revisions.controls.destroy"
|
||||
@class="btn-danger"
|
||||
@disabled={{this.loading}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
@ -0,0 +1,90 @@
|
||||
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { test } from "qunit";
|
||||
import { click, visit } from "@ember/test-helpers";
|
||||
|
||||
const revisionResponse = {
|
||||
created_at: "2021-11-24T10:59:36.163Z",
|
||||
post_id: 419,
|
||||
previous_hidden: false,
|
||||
current_hidden: false,
|
||||
first_revision: 1,
|
||||
previous_revision: 1,
|
||||
current_revision: 2,
|
||||
next_revision: null,
|
||||
last_revision: 2,
|
||||
current_version: 2,
|
||||
version_count: 2,
|
||||
username: "bianca",
|
||||
display_username: "bianca",
|
||||
avatar_template: "/letter_avatar_proxy/v4/letter/b/3be4f8/{size}.png",
|
||||
edit_reason: null,
|
||||
body_changes: {
|
||||
inline: '<div class="inline-diff"><p>Welcome to Discourse!</p</div>',
|
||||
side_by_side:
|
||||
'<div class="revision-content"><p>Welcome to Discourse!</p</div><div class="revision-content"><p>Welcome to Discourse!</p</div>',
|
||||
side_by_side_markdown:
|
||||
'<table class="markdown"><tr><td>Welcome to Discourse!</td><td>Welcome to Discourse!</td></tr></table>',
|
||||
},
|
||||
title_changes: {
|
||||
inline: '<div class="inline-diff"><div>Welcome to Discourse!</div></div>',
|
||||
side_by_side:
|
||||
'<div class="revision-content"><div>Welcome to Discourse!</div></div><div class="revision-content"><div>Welcome to Discourse!</div></div>',
|
||||
},
|
||||
user_changes: null,
|
||||
tags_changes: {
|
||||
previous: ["tag1", "tag2"],
|
||||
current: ["tag2", "tag3"],
|
||||
},
|
||||
wiki: false,
|
||||
can_edit: true,
|
||||
};
|
||||
|
||||
acceptance("History Modal - authorized", function (needs) {
|
||||
needs.user();
|
||||
needs.pretender((server, helper) => {
|
||||
server.get("/posts/419/revisions/latest.json", () => {
|
||||
return helper.response(revisionResponse);
|
||||
});
|
||||
|
||||
server.get("/posts/419/revisions/1.json", () => {
|
||||
return helper.response({ ...revisionResponse, current_revision: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
test("edit post button", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click("article[data-post-id='419'] .edits button");
|
||||
assert
|
||||
.dom(".history-modal #revision-footer-buttons .edit-post")
|
||||
.exists("displays the edit post button on the latest revision");
|
||||
});
|
||||
|
||||
test("edit post button - not last revision", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click("article[data-post-id='419'] .edits button");
|
||||
await click(".history-modal .previous-revision");
|
||||
assert
|
||||
.dom(".history-modal #revision-footer-buttons .edit-post")
|
||||
.doesNotExist(
|
||||
"hides the edit post button when not on the latest revision"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("History Modal - anonymous", function (needs) {
|
||||
needs.pretender((server, helper) => {
|
||||
server.get("/posts/419/revisions/latest.json", () => {
|
||||
return helper.response({ ...revisionResponse, can_edit: false });
|
||||
});
|
||||
});
|
||||
|
||||
test("edit post button", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click("article[data-post-id='419'] .edits button");
|
||||
assert
|
||||
.dom(".history-modal #revision-footer-buttons .edit-post")
|
||||
.doesNotExist(
|
||||
"it should not display edit button when user cannot edit the post"
|
||||
);
|
||||
});
|
||||
});
|
@ -1,120 +0,0 @@
|
||||
import { module, test } from "qunit";
|
||||
import { setupTest } from "ember-qunit";
|
||||
|
||||
module("Unit | Controller | history", function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test("displayEdit", async function (assert) {
|
||||
const controller = this.owner.lookup("controller:history");
|
||||
|
||||
controller.setProperties({
|
||||
model: { last_revision: 3, current_revision: 3, can_edit: false },
|
||||
topicController: {},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
controller.displayEdit,
|
||||
false,
|
||||
"it should not display edit button when user cannot edit the post"
|
||||
);
|
||||
|
||||
controller.set("model.can_edit", true);
|
||||
|
||||
assert.strictEqual(
|
||||
controller.displayEdit,
|
||||
true,
|
||||
"it should display edit button when user can edit the post"
|
||||
);
|
||||
|
||||
controller.set("topicController", null);
|
||||
assert.strictEqual(
|
||||
controller.displayEdit,
|
||||
false,
|
||||
"it should not display edit button when there is not topic controller"
|
||||
);
|
||||
controller.set("topicController", {});
|
||||
|
||||
controller.set("model.current_revision", 2);
|
||||
assert.strictEqual(
|
||||
controller.displayEdit,
|
||||
false,
|
||||
"it should only display the edit button on the latest revision"
|
||||
);
|
||||
|
||||
const html = `<div class="revision-content">
|
||||
<p><img src="/uploads/default/original/1X/6b963ffc13cb0c053bbb90c92e99d4fe71b286ef.jpg" alt="" class="diff-del"><img/src=x onerror=alert(document.domain)>" width="276" height="183"></p>
|
||||
</div>
|
||||
<aside class="onebox allowlistedgeneric">
|
||||
<header class="source">
|
||||
<img src="/uploads/default/original/1X/1b0984d7ee08bce90572f46a1950e1ced436d028.png" class="site-icon" width="32" height="32">
|
||||
<a href="https://meta.discourse.org/t/discourse-version-2-5/125302">Discourse Meta – 9 Aug 19</a>
|
||||
</header>
|
||||
<article class="onebox-body">
|
||||
<img src="/uploads/default/optimized/1X/ecc92a52ee7353e03d5c0d1ea6521ce4541d9c25_2_500x500.png" class="thumbnail onebox-avatar d-lazyload" width="500" height="500">
|
||||
<h3><a href="https://meta.discourse.org/t/discourse-version-2-5/125302" target="_blank">Discourse Version 2.5</a></h3>
|
||||
<div style="clear: both"></div>
|
||||
</article>
|
||||
</aside>
|
||||
<table background="javascript:alert(\"HACKEDXSS\")">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Column</th>
|
||||
<th style="text-align:left">Test</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td background="javascript:alert('HACKEDXSS')">Osama</td>
|
||||
<td style="text-align:right">Testing</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`;
|
||||
|
||||
const expectedOutput = `<div class="revision-content">
|
||||
<p><img src="/uploads/default/original/1X/6b963ffc13cb0c053bbb90c92e99d4fe71b286ef.jpg" alt class="diff-del">" width="276" height="183"></p>
|
||||
</div>
|
||||
<aside class="onebox allowlistedgeneric">
|
||||
<header class="source">
|
||||
<img src="/uploads/default/original/1X/1b0984d7ee08bce90572f46a1950e1ced436d028.png" class="site-icon" width="32" height="32">
|
||||
<a href="https://meta.discourse.org/t/discourse-version-2-5/125302">Discourse Meta – 9 Aug 19</a>
|
||||
</header>
|
||||
<article class="onebox-body">
|
||||
<img src="/uploads/default/optimized/1X/ecc92a52ee7353e03d5c0d1ea6521ce4541d9c25_2_500x500.png" class="thumbnail onebox-avatar d-lazyload" width="500" height="500">
|
||||
<h3><a href="https://meta.discourse.org/t/discourse-version-2-5/125302" target="_blank">Discourse Version 2.5</a></h3>
|
||||
<div style="clear: both"></div>
|
||||
</article>
|
||||
</aside>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Column</th>
|
||||
<th style="text-align:left">Test</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Osama</td>
|
||||
<td style="text-align:right">Testing</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`;
|
||||
|
||||
controller.setProperties({
|
||||
viewMode: "side_by_side",
|
||||
model: {
|
||||
body_changes: {
|
||||
side_by_side: html,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await controller.bodyDiffChanged();
|
||||
|
||||
const output = controller.bodyDiff;
|
||||
assert.strictEqual(
|
||||
output,
|
||||
expectedOutput,
|
||||
"it keeps HTML safe and doesn't strip onebox tags"
|
||||
);
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user