FEATURE: Flag count in post menu
This change shows a notification number besides the flag icon in the post menu if there is reviewable content associated with the post. Additionally, if there is pending stuff to review, the icon has a red background. We have also removed the list of links below a post with the flag status. A reviewer is meant to click the number beside the flag icon to view the flags. As a consequence of losing those links, we've removed the ability to undo or ignore flags below a post.
This commit is contained in:
parent
e6843afa9e
commit
31e100530f
|
@ -1,19 +1,5 @@
|
||||||
import { userPath } from "discourse/lib/url";
|
import { userPath } from "discourse/lib/url";
|
||||||
|
|
||||||
function actionDescription(action, acted, count) {
|
|
||||||
if (acted) {
|
|
||||||
if (count <= 1) {
|
|
||||||
return I18n.t(`post.actions.by_you.${action}`);
|
|
||||||
} else {
|
|
||||||
return I18n.t(`post.actions.by_you_and_others.${action}`, {
|
|
||||||
count: count - 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return I18n.t(`post.actions.by_others.${action}`, { count });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const _additionalAttributes = [];
|
const _additionalAttributes = [];
|
||||||
|
|
||||||
export function includeAttributes(...attributes) {
|
export function includeAttributes(...attributes) {
|
||||||
|
@ -62,6 +48,10 @@ export function transformBasicPost(post) {
|
||||||
canRecover: post.can_recover,
|
canRecover: post.can_recover,
|
||||||
canEdit: post.can_edit,
|
canEdit: post.can_edit,
|
||||||
canFlag: !Ember.isEmpty(post.get("flagsAvailable")),
|
canFlag: !Ember.isEmpty(post.get("flagsAvailable")),
|
||||||
|
canReviewTopic: false,
|
||||||
|
reviewableId: post.reviewable_id,
|
||||||
|
reviewableScoreCount: post.reviewable_score_count,
|
||||||
|
reviewableScorePendingCount: post.reviewable_score_pending_count,
|
||||||
version: post.version,
|
version: post.version,
|
||||||
canRecoverTopic: false,
|
canRecoverTopic: false,
|
||||||
canDeletedTopic: false,
|
canDeletedTopic: false,
|
||||||
|
@ -121,6 +111,7 @@ export default function transformPost(
|
||||||
postAtts.canViewRawEmail =
|
postAtts.canViewRawEmail =
|
||||||
currentUser && (currentUser.id === post.user_id || currentUser.staff);
|
currentUser && (currentUser.id === post.user_id || currentUser.staff);
|
||||||
postAtts.canReplyAsNewTopic = details.can_reply_as_new_topic;
|
postAtts.canReplyAsNewTopic = details.can_reply_as_new_topic;
|
||||||
|
postAtts.canReviewTopic = !!details.can_review_topic;
|
||||||
postAtts.isWarning = topic.is_warning;
|
postAtts.isWarning = topic.is_warning;
|
||||||
postAtts.links = post.get("internalLinks");
|
postAtts.links = post.get("internalLinks");
|
||||||
postAtts.replyDirectlyBelow =
|
postAtts.replyDirectlyBelow =
|
||||||
|
@ -208,22 +199,17 @@ export default function transformPost(
|
||||||
if (post.actions_summary) {
|
if (post.actions_summary) {
|
||||||
postAtts.actionsSummary = post.actions_summary
|
postAtts.actionsSummary = post.actions_summary
|
||||||
.filter(a => {
|
.filter(a => {
|
||||||
return a.actionType.name_key !== "like" && a.count > 0;
|
return a.actionType.name_key !== "like" && a.acted;
|
||||||
})
|
})
|
||||||
.map(a => {
|
.map(a => {
|
||||||
const acted = a.acted;
|
|
||||||
const action = a.actionType.name_key;
|
const action = a.actionType.name_key;
|
||||||
const count = a.count;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: a.id,
|
id: a.id,
|
||||||
postId: post.id,
|
postId: post.id,
|
||||||
action,
|
action,
|
||||||
acted,
|
|
||||||
count,
|
|
||||||
canUndo: a.can_undo,
|
canUndo: a.can_undo,
|
||||||
canIgnoreFlags: a.can_defer_flags,
|
description: I18n.t(`post.actions.by_you.${action}`)
|
||||||
description: actionDescription(action, acted, count)
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,8 +33,6 @@ export default RestModel.extend({
|
||||||
act(post, opts) {
|
act(post, opts) {
|
||||||
if (!opts) opts = {};
|
if (!opts) opts = {};
|
||||||
|
|
||||||
const action = this.get("actionType.name_key");
|
|
||||||
|
|
||||||
// Mark it as acted
|
// Mark it as acted
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
acted: true,
|
acted: true,
|
||||||
|
@ -43,13 +41,7 @@ export default RestModel.extend({
|
||||||
can_undo: true
|
can_undo: true
|
||||||
});
|
});
|
||||||
|
|
||||||
if (action === "notify_moderators" || action === "notify_user") {
|
|
||||||
this.set("can_undo", false);
|
|
||||||
this.set("can_defer_flags", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create our post action
|
// Create our post action
|
||||||
const self = this;
|
|
||||||
return ajax("/post_actions", {
|
return ajax("/post_actions", {
|
||||||
type: "POST",
|
type: "POST",
|
||||||
data: {
|
data: {
|
||||||
|
@ -62,8 +54,8 @@ export default RestModel.extend({
|
||||||
},
|
},
|
||||||
returnXHR: true
|
returnXHR: true
|
||||||
})
|
})
|
||||||
.then(function(data) {
|
.then(data => {
|
||||||
if (!self.get("flagTopic")) {
|
if (!this.get("flagTopic")) {
|
||||||
post.updateActionsSummary(data.result);
|
post.updateActionsSummary(data.result);
|
||||||
}
|
}
|
||||||
const remaining = parseInt(
|
const remaining = parseInt(
|
||||||
|
@ -74,9 +66,9 @@ export default RestModel.extend({
|
||||||
);
|
);
|
||||||
return { acted: true, remaining, max };
|
return { acted: true, remaining, max };
|
||||||
})
|
})
|
||||||
.catch(function(error) {
|
.catch(error => {
|
||||||
popupAjaxError(error);
|
popupAjaxError(error);
|
||||||
self.removeAction(post);
|
this.removeAction(post);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -92,12 +84,5 @@ export default RestModel.extend({
|
||||||
post.updateActionsSummary(result);
|
post.updateActionsSummary(result);
|
||||||
return { acted: false };
|
return { acted: false };
|
||||||
});
|
});
|
||||||
},
|
|
||||||
|
|
||||||
deferFlags(post) {
|
|
||||||
return ajax("/post_actions/defer_flags", {
|
|
||||||
type: "POST",
|
|
||||||
data: { post_action_type_id: this.get("id"), id: post.get("id") }
|
|
||||||
}).then(() => this.set("count", 0));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -74,57 +74,11 @@ createWidget("action-link", {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
createWidget("actions-summary-item", {
|
|
||||||
tagName: "div.post-action",
|
|
||||||
buildKey: attrs => `actions-summary-item-${attrs.id}`,
|
|
||||||
|
|
||||||
defaultState() {
|
|
||||||
return { users: null };
|
|
||||||
},
|
|
||||||
|
|
||||||
template: hbs`
|
|
||||||
{{#if state.users}}
|
|
||||||
{{small-user-list users=state.users description=(concat "post.actions.people." attrs.action)}}
|
|
||||||
{{else}}
|
|
||||||
{{action-link action="whoActed" text=attrs.description}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if attrs.canUndo}}
|
|
||||||
{{action-link action="undo" className="undo" text=(i18n (concat "post.actions.undo." attrs.action))}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if attrs.canIgnoreFlags}}
|
|
||||||
{{action-link action="deferFlags" className="defer-flags" text=(i18n "post.actions.defer_flags" count=attrs.count)}}
|
|
||||||
{{/if}}
|
|
||||||
`,
|
|
||||||
|
|
||||||
whoActed() {
|
|
||||||
const attrs = this.attrs;
|
|
||||||
const state = this.state;
|
|
||||||
return this.store
|
|
||||||
.find("post-action-user", {
|
|
||||||
id: attrs.postId,
|
|
||||||
post_action_type_id: attrs.id
|
|
||||||
})
|
|
||||||
.then(users => {
|
|
||||||
state.users = users.map(avatarAtts);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
undo() {
|
|
||||||
this.sendWidgetAction("undoPostAction", this.attrs.id);
|
|
||||||
},
|
|
||||||
|
|
||||||
deferFlags() {
|
|
||||||
this.sendWidgetAction("deferPostActionFlags", this.attrs.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default createWidget("actions-summary", {
|
export default createWidget("actions-summary", {
|
||||||
tagName: "section.post-actions",
|
tagName: "section.post-actions",
|
||||||
template: hbs`
|
template: hbs`
|
||||||
{{#each attrs.actionsSummary as |as|}}
|
{{#each attrs.actionsSummary as |as|}}
|
||||||
{{actions-summary-item attrs=as}}
|
<div class='post-action'>{{as.description}}</div>
|
||||||
<div class='clearfix'></div>
|
<div class='clearfix'></div>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{#if attrs.deleted_at}}
|
{{#if attrs.deleted_at}}
|
||||||
|
|
|
@ -64,7 +64,8 @@ registerButton("like", attrs => {
|
||||||
const button = {
|
const button = {
|
||||||
action: "like",
|
action: "like",
|
||||||
icon: attrs.liked ? "d-liked" : "d-unliked",
|
icon: attrs.liked ? "d-liked" : "d-unliked",
|
||||||
className
|
className,
|
||||||
|
before: "like-count"
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the user has already liked the post and doesn't have permission
|
// If the user has already liked the post and doesn't have permission
|
||||||
|
@ -99,7 +100,7 @@ registerButton("like-count", attrs => {
|
||||||
return {
|
return {
|
||||||
action: "toggleWhoLiked",
|
action: "toggleWhoLiked",
|
||||||
title,
|
title,
|
||||||
className: `like-count highlight-action ${additionalClass}`,
|
className: `button-count like-count highlight-action ${additionalClass}`,
|
||||||
contents: count,
|
contents: count,
|
||||||
icon,
|
icon,
|
||||||
iconRight: true,
|
iconRight: true,
|
||||||
|
@ -108,14 +109,30 @@ registerButton("like-count", attrs => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
registerButton("flag-count", attrs => {
|
||||||
|
let className = "button-count";
|
||||||
|
if (attrs.reviewableScorePendingCount > 0) {
|
||||||
|
className += " has-pending";
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
className,
|
||||||
|
contents: h("span", attrs.reviewableScoreCount.toString()),
|
||||||
|
url: `/review/${attrs.reviewableId}`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
registerButton("flag", attrs => {
|
registerButton("flag", attrs => {
|
||||||
if (attrs.canFlag && !attrs.hidden) {
|
if (attrs.reviewableId || (attrs.canFlag && !attrs.hidden)) {
|
||||||
return {
|
let button = {
|
||||||
action: "showFlags",
|
action: "showFlags",
|
||||||
title: "post.controls.flag",
|
title: "post.controls.flag",
|
||||||
icon: "flag",
|
icon: "flag",
|
||||||
className: "create-flag"
|
className: "create-flag"
|
||||||
};
|
};
|
||||||
|
if (attrs.reviewableId) {
|
||||||
|
button.before = "flag-count";
|
||||||
|
}
|
||||||
|
return button;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -323,7 +340,12 @@ export default createWidget("post-menu", {
|
||||||
attachButton(name) {
|
attachButton(name) {
|
||||||
let buttonAtts = buildButton(name, this);
|
let buttonAtts = buildButton(name, this);
|
||||||
if (buttonAtts) {
|
if (buttonAtts) {
|
||||||
return this.attach(this.settings.buttonType, buttonAtts);
|
let button = this.attach(this.settings.buttonType, buttonAtts);
|
||||||
|
if (buttonAtts.before) {
|
||||||
|
let before = this.attachButton(buttonAtts.before);
|
||||||
|
return h("div.double-button", [before, button]);
|
||||||
|
}
|
||||||
|
return button;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -359,22 +381,21 @@ export default createWidget("post-menu", {
|
||||||
replaceButton(orderedButtons, "reply", "wiki-edit");
|
replaceButton(orderedButtons, "reply", "wiki-edit");
|
||||||
}
|
}
|
||||||
|
|
||||||
orderedButtons
|
orderedButtons.forEach(i => {
|
||||||
.filter(x => x !== "like-count" && x !== "like")
|
const button = this.attachButton(i, attrs);
|
||||||
.forEach(i => {
|
|
||||||
const button = this.attachButton(i, attrs);
|
|
||||||
|
|
||||||
if (button) {
|
if (button) {
|
||||||
allButtons.push(button);
|
allButtons.push(button);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(attrs.yours && button.attrs.alwaysShowYours) ||
|
(attrs.yours && button.attrs && button.attrs.alwaysShowYours) ||
|
||||||
hiddenButtons.indexOf(i) === -1
|
(attrs.reviewableId && i === "flag") ||
|
||||||
) {
|
hiddenButtons.indexOf(i) === -1
|
||||||
visibleButtons.push(button);
|
) {
|
||||||
}
|
visibleButtons.push(button);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (!this.settings.collapseButtons) {
|
if (!this.settings.collapseButtons) {
|
||||||
visibleButtons = allButtons;
|
visibleButtons = allButtons;
|
||||||
|
@ -397,13 +418,6 @@ export default createWidget("post-menu", {
|
||||||
visibleButtons.splice(visibleButtons.length - 1, 0, showMore);
|
visibleButtons.splice(visibleButtons.length - 1, 0, showMore);
|
||||||
}
|
}
|
||||||
|
|
||||||
visibleButtons.unshift(
|
|
||||||
h("div.like-button", [
|
|
||||||
this.attachButton("like-count", attrs),
|
|
||||||
this.attachButton("like", attrs)
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
Object.values(_extraButtons).forEach(builder => {
|
Object.values(_extraButtons).forEach(builder => {
|
||||||
if (builder) {
|
if (builder) {
|
||||||
const buttonAtts = builder(attrs, this.state, this.siteSettings);
|
const buttonAtts = builder(attrs, this.state, this.siteSettings);
|
||||||
|
|
|
@ -711,21 +711,5 @@ export default createWidget("post", {
|
||||||
bootbox.alert(I18n.t("post.few_likes_left"));
|
bootbox.alert(I18n.t("post.few_likes_left"));
|
||||||
kvs.set({ key: "lastWarnedLikes", value: new Date().getTime() });
|
kvs.set({ key: "lastWarnedLikes", value: new Date().getTime() });
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
undoPostAction(typeId) {
|
|
||||||
const post = this.model;
|
|
||||||
return post
|
|
||||||
.get("actions_summary")
|
|
||||||
.findBy("id", typeId)
|
|
||||||
.undo(post);
|
|
||||||
},
|
|
||||||
|
|
||||||
deferPostActionFlags(typeId) {
|
|
||||||
const post = this.model;
|
|
||||||
return post
|
|
||||||
.get("actions_summary")
|
|
||||||
.findBy("id", typeId)
|
|
||||||
.deferFlags(post);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,14 @@
|
||||||
|
.button-count.has-pending {
|
||||||
|
span {
|
||||||
|
background-color: $danger;
|
||||||
|
color: $secondary;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.placeholder-avatar {
|
.placeholder-avatar {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 45px;
|
width: 45px;
|
||||||
|
@ -243,6 +254,7 @@ blockquote {
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-action {
|
.post-action {
|
||||||
|
color: $primary-medium;
|
||||||
.undo-action,
|
.undo-action,
|
||||||
.act-action {
|
.act-action {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
|
|
|
@ -46,19 +46,23 @@ section.post-menu-area {
|
||||||
|
|
||||||
nav.post-controls {
|
nav.post-controls {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
.like-button {
|
|
||||||
// Like button wrapper
|
// Some buttons can be doubled up, like likes or flags
|
||||||
|
.double-button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
color: $primary-low-mid;
|
color: $primary-low-mid;
|
||||||
margin-right: 0.15em;
|
margin-right: 0.15em;
|
||||||
&:hover {
|
&:hover {
|
||||||
// Like button wrapper on hover
|
|
||||||
button {
|
button {
|
||||||
background: $primary-low;
|
background: $primary-low;
|
||||||
color: $primary-medium;
|
color: $primary-medium;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
|
// It looks really confusing when one half a double button has an inner shadow on click.
|
||||||
|
&:active {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
&.my-likes {
|
&.my-likes {
|
||||||
|
@ -93,7 +97,7 @@ nav.post-controls {
|
||||||
// Disabled like button
|
// Disabled like button
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
&.like-count {
|
&.button-count {
|
||||||
// Like count button
|
// Like count button
|
||||||
&:not(.my-likes) {
|
&:not(.my-likes) {
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
|
|
|
@ -49,16 +49,6 @@ class PostActionsController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def defer_flags
|
|
||||||
guardian.ensure_can_defer_flags!(@post)
|
|
||||||
|
|
||||||
if reviewable = @post.reviewable_flag
|
|
||||||
reviewable.perform(current_user, :ignore)
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: { success: true }
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def fetch_post_from_params
|
def fetch_post_from_params
|
||||||
|
|
|
@ -470,10 +470,6 @@ class Post < ActiveRecord::Base
|
||||||
post_actions.where(post_action_type_id: PostActionType.flag_types_without_custom.values, deleted_at: nil).count != 0
|
post_actions.where(post_action_type_id: PostActionType.flag_types_without_custom.values, deleted_at: nil).count != 0
|
||||||
end
|
end
|
||||||
|
|
||||||
def active_flags
|
|
||||||
post_actions.active.where(post_action_type_id: PostActionType.flag_types_without_custom.values)
|
|
||||||
end
|
|
||||||
|
|
||||||
def reviewable_flag
|
def reviewable_flag
|
||||||
ReviewableFlaggedPost.pending.find_by(target: self)
|
ReviewableFlaggedPost.pending.find_by(target: self)
|
||||||
end
|
end
|
||||||
|
|
|
@ -73,7 +73,10 @@ class PostSerializer < BasicPostSerializer
|
||||||
:notice_args,
|
:notice_args,
|
||||||
:last_wiki_edit,
|
:last_wiki_edit,
|
||||||
:locked,
|
:locked,
|
||||||
:excerpt
|
:excerpt,
|
||||||
|
:reviewable_id,
|
||||||
|
:reviewable_score_count,
|
||||||
|
:reviewable_score_pending_count
|
||||||
|
|
||||||
def initialize(object, opts)
|
def initialize(object, opts)
|
||||||
super(object, opts)
|
super(object, opts)
|
||||||
|
@ -251,14 +254,6 @@ class PostSerializer < BasicPostSerializer
|
||||||
summary.delete(:can_act)
|
summary.delete(:can_act)
|
||||||
end
|
end
|
||||||
|
|
||||||
# The following only applies if you're logged in
|
|
||||||
if summary[:can_act] && scope.current_user.present?
|
|
||||||
summary[:can_defer_flags] = true if scope.is_staff? &&
|
|
||||||
PostActionType.flag_types_without_custom.values.include?(id) &&
|
|
||||||
active_flags.present? && active_flags.has_key?(id) &&
|
|
||||||
active_flags[id] > 0
|
|
||||||
end
|
|
||||||
|
|
||||||
if actions.present? && actions.has_key?(id)
|
if actions.present? && actions.has_key?(id)
|
||||||
summary[:acted] = true
|
summary[:acted] = true
|
||||||
summary[:can_undo] = true if scope.can_delete?(actions[id])
|
summary[:can_undo] = true if scope.can_delete?(actions[id])
|
||||||
|
@ -416,7 +411,62 @@ class PostSerializer < BasicPostSerializer
|
||||||
object.hidden
|
object.hidden
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
# If we have a topic view, it has bulk values for the reviewable content we can use
|
||||||
|
def reviewable_id
|
||||||
|
if @topic_view.present?
|
||||||
|
for_post = @topic_view.reviewable_counts[object.id]
|
||||||
|
return for_post ? for_post[:reviewable_id] : 0
|
||||||
|
end
|
||||||
|
|
||||||
|
reviewable&.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_reviewable_id?
|
||||||
|
can_review_topic?
|
||||||
|
end
|
||||||
|
|
||||||
|
def reviewable_score_count
|
||||||
|
if @topic_view.present?
|
||||||
|
for_post = @topic_view.reviewable_counts[object.id]
|
||||||
|
return for_post ? for_post[:total] : 0
|
||||||
|
end
|
||||||
|
|
||||||
|
reviewable_scores.size
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_reviewable_score_count?
|
||||||
|
can_review_topic?
|
||||||
|
end
|
||||||
|
|
||||||
|
def reviewable_score_pending_count
|
||||||
|
if @topic_view.present?
|
||||||
|
for_post = @topic_view.reviewable_counts[object.id]
|
||||||
|
return for_post ? for_post[:pending] : 0
|
||||||
|
end
|
||||||
|
|
||||||
|
reviewable_scores.count { |rs| rs.pending? }
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_reviewable_score_pending_count?
|
||||||
|
can_review_topic?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def can_review_topic?
|
||||||
|
return @can_review_topic unless @can_review_topic.nil?
|
||||||
|
@can_review_topic = @topic_view&.can_review_topic
|
||||||
|
@can_review_topic ||= scope.can_review_topic?(object.topic)
|
||||||
|
@can_review_topic
|
||||||
|
end
|
||||||
|
|
||||||
|
def reviewable
|
||||||
|
@reviewable ||= Reviewable.where(target: object).includes(:reviewable_scores).first
|
||||||
|
end
|
||||||
|
|
||||||
|
def reviewable_scores
|
||||||
|
reviewable&.reviewable_scores&.to_a || []
|
||||||
|
end
|
||||||
|
|
||||||
def user_custom_fields_object
|
def user_custom_fields_object
|
||||||
(@topic_view&.user_custom_fields || @options[:user_custom_fields] || {})
|
(@topic_view&.user_custom_fields || @options[:user_custom_fields] || {})
|
||||||
|
@ -432,10 +482,6 @@ class PostSerializer < BasicPostSerializer
|
||||||
@post_actions ||= (@topic_view&.all_post_actions || {})[object.id]
|
@post_actions ||= (@topic_view&.all_post_actions || {})[object.id]
|
||||||
end
|
end
|
||||||
|
|
||||||
def active_flags
|
|
||||||
@active_flags ||= (@topic_view&.all_active_flags || {})[object.id]
|
|
||||||
end
|
|
||||||
|
|
||||||
def post_custom_fields
|
def post_custom_fields
|
||||||
@post_custom_fields ||= if @topic_view
|
@post_custom_fields ||= if @topic_view
|
||||||
(@topic_view.post_custom_fields || {})[object.id] || {}
|
(@topic_view.post_custom_fields || {})[object.id] || {}
|
||||||
|
|
|
@ -11,7 +11,8 @@ class TopicViewDetailsSerializer < ApplicationSerializer
|
||||||
:can_create_post,
|
:can_create_post,
|
||||||
:can_reply_as_new_topic,
|
:can_reply_as_new_topic,
|
||||||
:can_flag_topic,
|
:can_flag_topic,
|
||||||
:can_convert_topic]
|
:can_convert_topic,
|
||||||
|
:can_review_topic]
|
||||||
end
|
end
|
||||||
|
|
||||||
attributes(
|
attributes(
|
||||||
|
@ -77,6 +78,10 @@ class TopicViewDetailsSerializer < ApplicationSerializer
|
||||||
define_method(ca) { true }
|
define_method(ca) { true }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def include_can_review_topic?
|
||||||
|
scope.can_review_topic?(object.topic)
|
||||||
|
end
|
||||||
|
|
||||||
def include_can_move_posts?
|
def include_can_move_posts?
|
||||||
scope.can_move_posts?(object.topic)
|
scope.can_move_posts?(object.topic)
|
||||||
end
|
end
|
||||||
|
|
|
@ -190,10 +190,9 @@ basic:
|
||||||
post_menu:
|
post_menu:
|
||||||
client: true
|
client: true
|
||||||
type: list
|
type: list
|
||||||
default: "like-count|like|share|flag|edit|bookmark|delete|admin|reply"
|
default: "like|share|flag|edit|bookmark|delete|admin|reply"
|
||||||
allow_any: false
|
allow_any: false
|
||||||
choices:
|
choices:
|
||||||
- like-count
|
|
||||||
- like
|
- like
|
||||||
- edit
|
- edit
|
||||||
- flag
|
- flag
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
class RemoveLikeCountFromPostMenu < ActiveRecord::Migration[5.2]
|
||||||
|
def up
|
||||||
|
execute(<<~SQL)
|
||||||
|
UPDATE site_settings
|
||||||
|
SET value = REGEXP_REPLACE(REPLACE(REPLACE(value, 'like-count', ''), '||', '|'), '^\\|', '')
|
||||||
|
WHERE name = 'post_menu'
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
|
@ -79,10 +79,6 @@ module PostGuardian
|
||||||
can_see_post?(post) && is_staff?
|
can_see_post?(post) && is_staff?
|
||||||
end
|
end
|
||||||
|
|
||||||
def can_defer_flags?(post)
|
|
||||||
can_see_post?(post) && is_staff? && post
|
|
||||||
end
|
|
||||||
|
|
||||||
# Can we see who acted on a post in a particular way?
|
# Can we see who acted on a post in a particular way?
|
||||||
def can_see_post_actors?(topic, post_action_type_id)
|
def can_see_post_actors?(topic, post_action_type_id)
|
||||||
return true if is_admin?
|
return true if is_admin?
|
||||||
|
|
|
@ -10,6 +10,16 @@ module TopicGuardian
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def can_review_topic?(topic)
|
||||||
|
return false if anonymous? || topic.nil?
|
||||||
|
return true if is_staff?
|
||||||
|
|
||||||
|
SiteSetting.enable_category_group_review? &&
|
||||||
|
topic.category.present? &&
|
||||||
|
topic.category.reviewable_by_group_id.present? &&
|
||||||
|
GroupUser.where(group_id: topic.category.reviewable_by_group_id, user_id: user.id).exists?
|
||||||
|
end
|
||||||
|
|
||||||
def can_create_shared_draft?
|
def can_create_shared_draft?
|
||||||
is_staff? && SiteSetting.shared_drafts_enabled?
|
is_staff? && SiteSetting.shared_drafts_enabled?
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,7 +15,8 @@ class TopicView
|
||||||
:print,
|
:print,
|
||||||
:message_bus_last_id,
|
:message_bus_last_id,
|
||||||
:queued_posts_enabled,
|
:queued_posts_enabled,
|
||||||
:personal_message
|
:personal_message,
|
||||||
|
:can_review_topic
|
||||||
)
|
)
|
||||||
|
|
||||||
attr_accessor(
|
attr_accessor(
|
||||||
|
@ -100,6 +101,7 @@ class TopicView
|
||||||
@draft_key = @topic.draft_key
|
@draft_key = @topic.draft_key
|
||||||
@draft_sequence = DraftSequence.current(@user, @draft_key)
|
@draft_sequence = DraftSequence.current(@user, @draft_key)
|
||||||
|
|
||||||
|
@can_review_topic = @guardian.can_review_topic?(@topic)
|
||||||
@queued_posts_enabled = NewPostManager.queue_enabled?
|
@queued_posts_enabled = NewPostManager.queue_enabled?
|
||||||
@personal_message = @topic.private_message?
|
@personal_message = @topic.private_message?
|
||||||
end
|
end
|
||||||
|
@ -410,16 +412,32 @@ class TopicView
|
||||||
@all_post_actions ||= PostAction.counts_for(@posts, @user)
|
@all_post_actions ||= PostAction.counts_for(@posts, @user)
|
||||||
end
|
end
|
||||||
|
|
||||||
def all_active_flags
|
|
||||||
@all_active_flags ||= ReviewableFlaggedPost.counts_for(@posts)
|
|
||||||
end
|
|
||||||
|
|
||||||
def links
|
def links
|
||||||
@links ||= TopicLink.topic_map(@guardian, @topic.id)
|
@links ||= TopicLink.topic_map(@guardian, @topic.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def reviewable_counts
|
||||||
|
if @reviewable_counts.blank?
|
||||||
|
|
||||||
|
# Create a hash with counts by post so we can quickly look up whether there is reviewable content.
|
||||||
|
@reviewable_counts = {}
|
||||||
|
Reviewable.
|
||||||
|
where(target_type: 'Post', target_id: filtered_post_ids).
|
||||||
|
includes(:reviewable_scores).each do |r|
|
||||||
|
|
||||||
|
for_post = (@reviewable_counts[r.target_id] ||= { total: 0, pending: 0, reviewable_id: r.id })
|
||||||
|
r.reviewable_scores.each do |s|
|
||||||
|
for_post[:total] += 1
|
||||||
|
for_post[:pending] += 1 if s.pending?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@reviewable_counts
|
||||||
|
end
|
||||||
|
|
||||||
def pending_posts
|
def pending_posts
|
||||||
ReviewableQueuedPost.pending.where(created_by: @user, topic: @topic).order(:created_at)
|
@pending_posts ||= ReviewableQueuedPost.pending.where(created_by: @user, topic: @topic).order(:created_at)
|
||||||
end
|
end
|
||||||
|
|
||||||
def actions_summary
|
def actions_summary
|
||||||
|
|
|
@ -249,29 +249,6 @@ describe Guardian do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "can_defer_flags" do
|
|
||||||
let(:post) { Fabricate(:post) }
|
|
||||||
let(:user) { post.user }
|
|
||||||
let(:moderator) { Fabricate(:moderator) }
|
|
||||||
|
|
||||||
it "returns false when the user is nil" do
|
|
||||||
expect(Guardian.new(nil).can_defer_flags?(post)).to be_falsey
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns false when the post is nil" do
|
|
||||||
expect(Guardian.new(moderator).can_defer_flags?(nil)).to be_falsey
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns false when the user is not a moderator" do
|
|
||||||
expect(Guardian.new(user).can_defer_flags?(post)).to be_falsey
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns true when the user is a moderator" do
|
|
||||||
expect(Guardian.new(moderator).can_defer_flags?(post)).to be_truthy
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'can_send_private_message' do
|
describe 'can_send_private_message' do
|
||||||
let(:user) { Fabricate(:user) }
|
let(:user) { Fabricate(:user) }
|
||||||
let(:another_user) { Fabricate(:user) }
|
let(:another_user) { Fabricate(:user) }
|
||||||
|
@ -1672,6 +1649,28 @@ describe Guardian do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "can_review_topic?" do
|
||||||
|
it 'returns false with a nil object' do
|
||||||
|
expect(Guardian.new(user).can_review_topic?(nil)).to eq(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true for a staff user' do
|
||||||
|
expect(Guardian.new(moderator).can_review_topic?(topic)).to eq(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false for a regular user' do
|
||||||
|
expect(Guardian.new(user).can_review_topic?(topic)).to eq(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false for a regular user' do
|
||||||
|
SiteSetting.enable_category_group_review = true
|
||||||
|
group = Fabricate(:group)
|
||||||
|
GroupUser.create!(group_id: group.id, user_id: user.id)
|
||||||
|
topic.category.update!(reviewable_by_group_id: group.id)
|
||||||
|
expect(Guardian.new(user).can_review_topic?(topic)).to eq(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'can_move_posts?' do
|
context 'can_move_posts?' do
|
||||||
|
|
||||||
it 'returns false with a nil object' do
|
it 'returns false with a nil object' do
|
||||||
|
|
|
@ -307,28 +307,6 @@ describe TopicView do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context '.all_active_flags' do
|
|
||||||
it 'is blank at first' do
|
|
||||||
expect(topic_view.all_active_flags).to be_blank
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns the active flags' do
|
|
||||||
PostActionCreator.off_topic(moderator, p1)
|
|
||||||
PostActionCreator.off_topic(evil_trout, p1)
|
|
||||||
|
|
||||||
expect(topic_view.all_active_flags[p1.id][PostActionType.types[:off_topic]]).to eq(2)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns only the active flags' do
|
|
||||||
reviewable = PostActionCreator.off_topic(moderator, p1).reviewable
|
|
||||||
PostActionCreator.off_topic(evil_trout, p1)
|
|
||||||
|
|
||||||
reviewable.perform(moderator, :ignore)
|
|
||||||
|
|
||||||
expect(topic_view.all_active_flags[p1.id]).to eq(nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context '.read?' do
|
context '.read?' do
|
||||||
it 'tracks correctly' do
|
it 'tracks correctly' do
|
||||||
# anon is assumed to have read everything
|
# anon is assumed to have read everything
|
||||||
|
|
|
@ -261,62 +261,4 @@ RSpec.describe PostActionsController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#defer_flags' do
|
|
||||||
let(:flagged_post) { Fabricate(:post, user: Fabricate(:coding_horror)) }
|
|
||||||
let!(:reviewable) do
|
|
||||||
PostActionCreator.spam(Fabricate(:user), flagged_post).reviewable
|
|
||||||
end
|
|
||||||
|
|
||||||
context "not logged in" do
|
|
||||||
it "should not allow them to clear flags" do
|
|
||||||
post "/post_actions/defer_flags.json", params: { id: flagged_post.id }
|
|
||||||
expect(response.status).to eq(403)
|
|
||||||
expect(reviewable.reload).not_to be_ignored
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'logged in' do
|
|
||||||
let!(:user) { sign_in(Fabricate(:moderator)) }
|
|
||||||
|
|
||||||
it "raises an error without a post_action_type_id" do
|
|
||||||
post "/post_actions/defer_flags.json", params: { id: flagged_post.id }
|
|
||||||
expect(response.status).to eq(400)
|
|
||||||
expect(reviewable.reload).not_to be_ignored
|
|
||||||
end
|
|
||||||
|
|
||||||
it "raises an error when the user doesn't have access" do
|
|
||||||
sign_in(Fabricate(:user))
|
|
||||||
|
|
||||||
post "/post_actions/defer_flags.json", params: {
|
|
||||||
id: flagged_post.id, post_action_type_id: PostActionType.types[:spam]
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(response).to be_forbidden
|
|
||||||
expect(reviewable.reload).not_to be_ignored
|
|
||||||
end
|
|
||||||
|
|
||||||
context "success" do
|
|
||||||
it "performs the ignore" do
|
|
||||||
post "/post_actions/defer_flags.json", params: {
|
|
||||||
id: flagged_post.id, post_action_type_id: PostActionType.types[:spam]
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(response.status).to eq(200)
|
|
||||||
expect(reviewable.reload).to be_ignored
|
|
||||||
end
|
|
||||||
|
|
||||||
it "works with a deleted post" do
|
|
||||||
flagged_post.trash!(user)
|
|
||||||
|
|
||||||
post "/post_actions/defer_flags.json", params: {
|
|
||||||
id: flagged_post.id, post_action_type_id: PostActionType.types[:spam]
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(response.status).to eq(200)
|
|
||||||
expect(reviewable.reload).to be_ignored
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -44,6 +44,18 @@ describe PostSerializer do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "a post with reviewable content" do
|
||||||
|
let!(:post) { Fabricate(:post, user: Fabricate(:user)) }
|
||||||
|
let!(:reviewable) { PostActionCreator.spam(Fabricate(:user), post).reviewable }
|
||||||
|
|
||||||
|
it "includes the reviewable data" do
|
||||||
|
json = PostSerializer.new(post, scope: Guardian.new(Fabricate(:moderator)), root: false).as_json
|
||||||
|
expect(json[:reviewable_id]).to eq(reviewable.id)
|
||||||
|
expect(json[:reviewable_score_count]).to eq(1)
|
||||||
|
expect(json[:reviewable_score_pending_count]).to eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "a post by a nuked user" do
|
context "a post by a nuked user" do
|
||||||
let!(:post) { Fabricate(:post, user: Fabricate(:user), deleted_at: Time.zone.now) }
|
let!(:post) { Fabricate(:post, user: Fabricate(:user), deleted_at: Time.zone.now) }
|
||||||
|
|
||||||
|
|
|
@ -134,6 +134,27 @@ describe TopicViewSerializer do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "with flags" do
|
||||||
|
let!(:post) { Fabricate(:post, topic: topic) }
|
||||||
|
let!(:other_post) { Fabricate(:post, topic: topic) }
|
||||||
|
|
||||||
|
it "will return reviewable counts on posts" do
|
||||||
|
r = PostActionCreator.inappropriate(Fabricate(:user), post).reviewable
|
||||||
|
r.perform(admin, :agree_and_keep)
|
||||||
|
PostActionCreator.spam(Fabricate(:user), post)
|
||||||
|
|
||||||
|
json = serialize_topic(topic, admin)
|
||||||
|
p0 = json[:post_stream][:posts][0]
|
||||||
|
expect(p0[:id]).to eq(post.id)
|
||||||
|
expect(p0[:reviewable_score_count]).to eq(2)
|
||||||
|
expect(p0[:reviewable_score_pending_count]).to eq(1)
|
||||||
|
|
||||||
|
p1 = json[:post_stream][:posts][1]
|
||||||
|
expect(p1[:reviewable_score_count]).to eq(0)
|
||||||
|
expect(p1[:reviewable_score_pending_count]).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "pending posts" do
|
describe "pending posts" do
|
||||||
context "when the queue is enabled" do
|
context "when the queue is enabled" do
|
||||||
before do
|
before do
|
||||||
|
@ -185,6 +206,7 @@ describe TopicViewSerializer do
|
||||||
expect(details[:notification_level]).to be_present
|
expect(details[:notification_level]).to be_present
|
||||||
expect(details[:can_move_posts]).to eq(true)
|
expect(details[:can_move_posts]).to eq(true)
|
||||||
expect(details[:can_flag_topic]).to eq(true)
|
expect(details[:can_flag_topic]).to eq(true)
|
||||||
|
expect(details[:can_review_topic]).to eq(true)
|
||||||
expect(details[:links][0][:clicks]).to eq(100)
|
expect(details[:links][0][:clicks]).to eq(100)
|
||||||
|
|
||||||
participant = details[:participants].find { |p| p[:id] == user.id }
|
participant = details[:participants].find { |p| p[:id] == user.id }
|
||||||
|
|
|
@ -31,13 +31,13 @@ export default {
|
||||||
raw:
|
raw:
|
||||||
"Any plans to support localization of UI elements, so that I (for example) could set up a completely German speaking forum?",
|
"Any plans to support localization of UI elements, so that I (for example) could set up a completely German speaking forum?",
|
||||||
actions_summary: [
|
actions_summary: [
|
||||||
{ id: 2, count: 0, hidden: false, can_act: true, can_defer_flags: false },
|
{ id: 2, count: 0, hidden: false, can_act: true },
|
||||||
{ id: 3, count: 0, hidden: false, can_act: true, can_defer_flags: false },
|
{ id: 3, count: 0, hidden: false, can_act: true },
|
||||||
{ id: 4, count: 0, hidden: false, can_act: true, can_defer_flags: false },
|
{ id: 4, count: 0, hidden: false, can_act: true },
|
||||||
{ id: 5, count: 0, hidden: true, can_act: true, can_defer_flags: false },
|
{ id: 5, count: 0, hidden: true, can_act: true },
|
||||||
{ id: 6, count: 0, hidden: false, can_act: true, can_defer_flags: false },
|
{ id: 6, count: 0, hidden: false, can_act: true },
|
||||||
{ id: 7, count: 0, hidden: false, can_act: true, can_defer_flags: false },
|
{ id: 7, count: 0, hidden: false, can_act: true },
|
||||||
{ id: 8, count: 0, hidden: false, can_act: true, can_defer_flags: false }
|
{ id: 8, count: 0, hidden: false, can_act: true }
|
||||||
],
|
],
|
||||||
moderator: false,
|
moderator: false,
|
||||||
admin: false,
|
admin: false,
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -2,73 +2,6 @@ import { moduleForWidget, widgetTest } from "helpers/widget-test";
|
||||||
|
|
||||||
moduleForWidget("actions-summary");
|
moduleForWidget("actions-summary");
|
||||||
|
|
||||||
widgetTest("listing actions", {
|
|
||||||
template: '{{mount-widget widget="actions-summary" args=args}}',
|
|
||||||
beforeEach() {
|
|
||||||
this.set("args", {
|
|
||||||
actionsSummary: [
|
|
||||||
{ id: 1, action: "off_topic", description: "very off topic" },
|
|
||||||
{ id: 2, action: "spam", description: "suspicious message" }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async test(assert) {
|
|
||||||
assert.equal(find(".post-actions .post-action").length, 2);
|
|
||||||
|
|
||||||
await click(".post-action:eq(0) .action-link a");
|
|
||||||
assert.equal(
|
|
||||||
find(".post-action:eq(0) img.avatar").length,
|
|
||||||
1,
|
|
||||||
"clicking it shows the user"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
widgetTest("undo", {
|
|
||||||
template:
|
|
||||||
'{{mount-widget widget="actions-summary" args=args undoPostAction=undoPostAction}}',
|
|
||||||
beforeEach() {
|
|
||||||
this.set("args", {
|
|
||||||
actionsSummary: [
|
|
||||||
{ action: "off_topic", description: "very off topic", canUndo: true }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
this.set("undoPostAction", () => (this.undid = true));
|
|
||||||
},
|
|
||||||
async test(assert) {
|
|
||||||
assert.equal(find(".post-actions .post-action").length, 1);
|
|
||||||
|
|
||||||
await click(".action-link.undo");
|
|
||||||
assert.ok(this.undid, "it triggered the action");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
widgetTest("deferFlags", {
|
|
||||||
template:
|
|
||||||
'{{mount-widget widget="actions-summary" args=args deferPostActionFlags=(action "deferPostActionFlags")}}',
|
|
||||||
beforeEach() {
|
|
||||||
this.set("args", {
|
|
||||||
actionsSummary: [
|
|
||||||
{
|
|
||||||
action: "off_topic",
|
|
||||||
description: "very off topic",
|
|
||||||
canIgnoreFlags: true,
|
|
||||||
count: 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
this.on("deferPostActionFlags", () => (this.deferred = true));
|
|
||||||
},
|
|
||||||
async test(assert) {
|
|
||||||
assert.equal(find(".post-actions .post-action").length, 1);
|
|
||||||
|
|
||||||
await click(".action-link.defer-flags");
|
|
||||||
assert.ok(this.deferred, "it triggered the action");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
widgetTest("post deleted", {
|
widgetTest("post deleted", {
|
||||||
template: '{{mount-widget widget="actions-summary" args=args}}',
|
template: '{{mount-widget widget="actions-summary" args=args}}',
|
||||||
beforeEach() {
|
beforeEach() {
|
||||||
|
|
Loading…
Reference in New Issue