mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
DEV: Modernize the post menu from widgets to Glimmer components (#28670)
This commit modernizes the post menu by migrating it from the existing widget-based implementation to Glimmer components. This transition aims to improve the maintainability, performance, and overall developer experience. It also introduces a new DAG-based transformer API for customizations that aims to be more flexible than the widget base one. --------- Co-authored-by: David Taylor <david@taylorhq.com>
This commit is contained in:
@@ -13,6 +13,10 @@ const DEPRECATION_WORKFLOW = [
|
||||
handler: "silence",
|
||||
matchId: "discourse.fontawesome-6-upgrade",
|
||||
},
|
||||
{
|
||||
handler: "silence",
|
||||
matchId: "discourse.post-menu-widget-overrides",
|
||||
},
|
||||
];
|
||||
|
||||
export default DEPRECATION_WORKFLOW;
|
||||
|
||||
@@ -14,13 +14,13 @@ export default class AdminPostMenu extends Component {
|
||||
@service adminPostMenuButtons;
|
||||
|
||||
get reviewUrl() {
|
||||
return `/review?topic_id=${this.args.data.transformedPost.id}&status=all`;
|
||||
return `/review?topic_id=${this.args.data.post.id}&status=all`;
|
||||
}
|
||||
|
||||
get extraButtons() {
|
||||
return this.adminPostMenuButtons.callbacks
|
||||
.map((callback) => {
|
||||
return callback(this.args.data.transformedPost);
|
||||
return callback(this.args.data.post);
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
@@ -36,14 +36,14 @@ export default class AdminPostMenu extends Component {
|
||||
console.error(`Unknown error while attempting \`${actionName}\`:`, error);
|
||||
}
|
||||
|
||||
await this.args.data.scheduleRerender();
|
||||
await this.args.data.scheduleRerender?.();
|
||||
}
|
||||
|
||||
@action
|
||||
async extraAction(button) {
|
||||
await this.args.close();
|
||||
await button.action(this.args.data.post);
|
||||
await this.args.data.scheduleRerender();
|
||||
await this.args.data.scheduleRerender?.();
|
||||
}
|
||||
|
||||
<template>
|
||||
@@ -59,45 +59,45 @@ export default class AdminPostMenu extends Component {
|
||||
</dropdown.item>
|
||||
{{/if}}
|
||||
|
||||
{{#if (and this.currentUser.staff (not @data.transformedPost.isWhisper))}}
|
||||
{{#if (and this.currentUser.staff (not @data.post.isWhisper))}}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@label={{if
|
||||
@data.transformedPost.isModeratorAction
|
||||
@data.post.isModeratorAction
|
||||
"post.controls.revert_to_regular"
|
||||
"post.controls.convert_to_moderator"
|
||||
}}
|
||||
@icon="shield-halved"
|
||||
class={{concatClass
|
||||
"btn btn-transparent toggle-post-type"
|
||||
(if @data.transformedPost.isModeratorAction "btn-success")
|
||||
(if @data.post.isModeratorAction "btn-success")
|
||||
}}
|
||||
@action={{fn this.topicAction "togglePostType"}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
{{/if}}
|
||||
|
||||
{{#if @data.transformedPost.canEditStaffNotes}}
|
||||
{{#if @data.post.canEditStaffNotes}}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@icon="user-shield"
|
||||
@label={{if
|
||||
@data.transformedPost.notice
|
||||
@data.post.notice
|
||||
"post.controls.change_post_notice"
|
||||
"post.controls.add_post_notice"
|
||||
}}
|
||||
@title="post.controls.unhide"
|
||||
class={{concatClass
|
||||
"btn btn-transparent"
|
||||
(if @data.transformedPost.notice "change-notice" "add-notice")
|
||||
(if @data.transformedPost.notice "btn-success")
|
||||
(if @data.post.notice "change-notice" "add-notice")
|
||||
(if @data.post.notice "btn-success")
|
||||
}}
|
||||
@action={{fn this.topicAction "changeNotice"}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
{{/if}}
|
||||
|
||||
{{#if (and this.currentUser.staff @data.transformedPost.hidden)}}
|
||||
{{#if (and this.currentUser.staff @data.post.hidden)}}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@label="post.controls.unhide"
|
||||
@@ -128,7 +128,7 @@ export default class AdminPostMenu extends Component {
|
||||
</dropdown.item>
|
||||
{{/if}}
|
||||
|
||||
{{#if (and @data.transformedPost.user_id this.currentUser.staff)}}
|
||||
{{#if (and @data.post.user_id this.currentUser.staff)}}
|
||||
{{#if this.siteSettings.enable_badges}}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@@ -140,7 +140,7 @@ export default class AdminPostMenu extends Component {
|
||||
</dropdown.item>
|
||||
{{/if}}
|
||||
|
||||
{{#if @data.transformedPost.locked}}
|
||||
{{#if @data.post.locked}}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@label="post.controls.unlock_post"
|
||||
@@ -166,7 +166,7 @@ export default class AdminPostMenu extends Component {
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if @data.transformedPost.canPermanentlyDelete}}
|
||||
{{#if @data.post.canPermanentlyDelete}}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@label="post.controls.permanently_delete"
|
||||
@@ -177,15 +177,15 @@ export default class AdminPostMenu extends Component {
|
||||
</dropdown.item>
|
||||
{{/if}}
|
||||
|
||||
{{#if (or @data.transformedPost.canManage @data.transformedPost.canWiki)}}
|
||||
{{#if @data.transformedPost.wiki}}
|
||||
{{#if (or @data.post.canManage @data.post.can_wiki)}}
|
||||
{{#if @data.post.wiki}}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@label="post.controls.unwiki"
|
||||
@icon="far-pen-to-square"
|
||||
class={{concatClass
|
||||
"btn btn-transparent wiki wikied"
|
||||
(if @data.transformedPost.wiki "btn-success")
|
||||
(if @data.post.wiki "btn-success")
|
||||
}}
|
||||
@action={{fn this.topicAction "toggleWiki"}}
|
||||
/>
|
||||
@@ -202,7 +202,7 @@ export default class AdminPostMenu extends Component {
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if @data.transformedPost.canPublishPage}}
|
||||
{{#if @data.post.canPublishPage}}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@label="post.controls.publish_page"
|
||||
@@ -213,7 +213,7 @@ export default class AdminPostMenu extends Component {
|
||||
</dropdown.item>
|
||||
{{/if}}
|
||||
|
||||
{{#if @data.transformedPost.canManage}}
|
||||
{{#if @data.post.canManage}}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@label="post.controls.rebake"
|
||||
|
||||
@@ -236,6 +236,7 @@ export default class BookmarkMenu extends Component {
|
||||
<template>
|
||||
<DMenu
|
||||
{{didInsert this.setReminderShortcuts}}
|
||||
...attributes
|
||||
@identifier="bookmark-menu"
|
||||
@triggers={{array "click"}}
|
||||
class={{this.buttonClasses}}
|
||||
|
||||
@@ -80,6 +80,15 @@ export default class DButton extends GlimmerComponentWithDeprecatedParentView {
|
||||
}
|
||||
}
|
||||
|
||||
get computedAriaPressed() {
|
||||
if (this.args.ariaPressed === true) {
|
||||
return "true";
|
||||
}
|
||||
if (this.args.ariaPressed === false) {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
keyDown(e) {
|
||||
if (this.args.onKeyDown) {
|
||||
@@ -177,6 +186,7 @@ export default class DButton extends GlimmerComponentWithDeprecatedParentView {
|
||||
form={{@form}}
|
||||
aria-controls={{@ariaControls}}
|
||||
aria-expanded={{this.computedAriaExpanded}}
|
||||
aria-pressed={{this.computedAriaPressed}}
|
||||
tabindex={{@tabindex}}
|
||||
disabled={{this.isDisabled}}
|
||||
title={{this.computedTitle}}
|
||||
|
||||
677
app/assets/javascripts/discourse/app/components/post/menu.gjs
Normal file
677
app/assets/javascripts/discourse/app/components/post/menu.gjs
Normal file
@@ -0,0 +1,677 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { cached, tracked } from "@glimmer/tracking";
|
||||
import { hash } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { getOwner } from "@ember/owner";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { isEmpty, isPresent } from "@ember/utils";
|
||||
import { and, eq } from "truth-helpers";
|
||||
import AdminPostMenu from "discourse/components/admin-post-menu";
|
||||
import DeleteTopicDisallowedModal from "discourse/components/modal/delete-topic-disallowed";
|
||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||
import SmallUserList from "discourse/components/small-user-list";
|
||||
import UserTip from "discourse/components/user-tip";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import DAG from "discourse/lib/dag";
|
||||
import { applyMutableValueTransformer } from "discourse/lib/transformer";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import PostMenuButtonConfig from "./menu/button-config";
|
||||
import PostMenuButtonWrapper from "./menu/button-wrapper";
|
||||
import PostMenuAdminButton from "./menu/buttons/admin";
|
||||
import PostMenuBookmarkButton from "./menu/buttons/bookmark";
|
||||
import PostMenuCopyLinkButton from "./menu/buttons/copy-link";
|
||||
import PostMenuDeleteButton from "./menu/buttons/delete";
|
||||
import PostMenuEditButton from "./menu/buttons/edit";
|
||||
import PostMenuFlagButton from "./menu/buttons/flag";
|
||||
import PostMenuLikeButton from "./menu/buttons/like";
|
||||
import PostMenuReadButton from "./menu/buttons/read";
|
||||
import PostMenuRepliesButton from "./menu/buttons/replies";
|
||||
import PostMenuReplyButton from "./menu/buttons/reply";
|
||||
import PostMenuShareButton from "./menu/buttons/share";
|
||||
import PostMenuShowMoreButton from "./menu/buttons/show-more";
|
||||
|
||||
const LIKE_ACTION = 2;
|
||||
const VIBRATE_DURATION = 5;
|
||||
|
||||
const buttonKeys = Object.freeze({
|
||||
ADMIN: "admin",
|
||||
BOOKMARK: "bookmark",
|
||||
COPY_LINK: "copyLink",
|
||||
DELETE: "delete",
|
||||
EDIT: "edit",
|
||||
FLAG: "flag",
|
||||
LIKE: "like",
|
||||
READ: "read",
|
||||
REPLIES: "replies",
|
||||
REPLY: "reply",
|
||||
SHARE: "share",
|
||||
SHOW_MORE: "showMore",
|
||||
});
|
||||
|
||||
const coreButtonComponents = new Map([
|
||||
[buttonKeys.ADMIN, PostMenuAdminButton],
|
||||
[buttonKeys.BOOKMARK, PostMenuBookmarkButton],
|
||||
[buttonKeys.COPY_LINK, PostMenuCopyLinkButton],
|
||||
[buttonKeys.DELETE, PostMenuDeleteButton],
|
||||
[buttonKeys.EDIT, PostMenuEditButton],
|
||||
[buttonKeys.FLAG, PostMenuFlagButton],
|
||||
[buttonKeys.LIKE, PostMenuLikeButton],
|
||||
[buttonKeys.READ, PostMenuReadButton],
|
||||
[buttonKeys.REPLIES, PostMenuRepliesButton],
|
||||
[buttonKeys.REPLY, PostMenuReplyButton],
|
||||
[buttonKeys.SHARE, PostMenuShareButton],
|
||||
[buttonKeys.SHOW_MORE, PostMenuShowMoreButton],
|
||||
]);
|
||||
|
||||
function smallUserAttributes(user) {
|
||||
return {
|
||||
template: user.avatar_template,
|
||||
username: user.username,
|
||||
post_url: user.post_url,
|
||||
url: userPath(user.username_lower),
|
||||
unknown: user.unknown,
|
||||
};
|
||||
}
|
||||
|
||||
const defaultDagOptions = {
|
||||
defaultPosition: { before: buttonKeys.SHOW_MORE },
|
||||
throwErrorOnCycle: false,
|
||||
};
|
||||
|
||||
export default class PostMenu extends Component {
|
||||
@service appEvents;
|
||||
@service capabilities;
|
||||
@service currentUser;
|
||||
@service keyValueStore;
|
||||
@service modal;
|
||||
@service menu;
|
||||
@service site;
|
||||
@service siteSettings;
|
||||
@service store;
|
||||
|
||||
@tracked collapsed = true; // TODO (glimmer-post-menu): Some plugins will need a value transformer
|
||||
@tracked isWhoLikedVisible = false;
|
||||
@tracked likedUsers = [];
|
||||
@tracked totalLikedUsers;
|
||||
@tracked isWhoReadVisible = false;
|
||||
@tracked readers = [];
|
||||
@tracked totalReaders;
|
||||
|
||||
@cached
|
||||
get buttonActions() {
|
||||
return {
|
||||
copyLink: this.args.copyLink,
|
||||
deletePost: this.args.deletePost,
|
||||
editPost: this.args.editPost,
|
||||
toggleLike: this.toggleLike,
|
||||
openAdminMenu: this.openAdminMenu,
|
||||
recoverPost: this.args.recoverPost,
|
||||
replyToPost: this.args.replyToPost,
|
||||
setCollapsed: (value) => (this.collapsed = value),
|
||||
share: this.args.share,
|
||||
showDeleteTopicModal: this.showDeleteTopicModal,
|
||||
showFlags: this.args.showFlags,
|
||||
showMoreActions: this.showMoreActions,
|
||||
toggleReplies: this.args.toggleReplies,
|
||||
toggleWhoLiked: this.toggleWhoLiked,
|
||||
toggleWhoRead: this.toggleWhoRead,
|
||||
};
|
||||
}
|
||||
|
||||
@cached
|
||||
get staticMethodsState() {
|
||||
return Object.freeze({
|
||||
canCreatePost: this.args.canCreatePost,
|
||||
collapsed: this.collapsed,
|
||||
currentUser: this.currentUser,
|
||||
filteredRepliesView: this.args.filteredRepliesView,
|
||||
isWhoLikedVisible: this.isWhoLikedVisible,
|
||||
isWhoReadVisible: this.isWhoReadVisible,
|
||||
isWikiMode: this.isWikiMode,
|
||||
repliesShown: this.args.repliesShown,
|
||||
replyDirectlyBelow:
|
||||
this.args.nextPost?.reply_to_post_number ===
|
||||
this.args.post.post_number &&
|
||||
this.args.post.post_number !== this.args.post.filteredRepliesPostNumber,
|
||||
showReadIndicator: this.args.showReadIndicator,
|
||||
suppressReplyDirectlyBelow:
|
||||
this.siteSettings.suppress_reply_directly_below,
|
||||
});
|
||||
}
|
||||
|
||||
@cached
|
||||
get staticMethodsArgs() {
|
||||
return {
|
||||
post: this.args.post,
|
||||
state: this.staticMethodsState,
|
||||
};
|
||||
}
|
||||
|
||||
@cached
|
||||
get state() {
|
||||
return Object.freeze({
|
||||
...this.staticMethodsState,
|
||||
collapsedButtons: this.renderableCollapsedButtons,
|
||||
});
|
||||
}
|
||||
|
||||
@cached
|
||||
get registeredButtons() {
|
||||
let addedKeys;
|
||||
const replacementMap = new WeakMap();
|
||||
|
||||
const configuredItems = this.configuredItems;
|
||||
const configuredPositions = this.configuredPositions;
|
||||
|
||||
// it's important to sort the buttons in the order they were configured, so we can feed them in the correct order
|
||||
// to initialize the DAG because the results are affected by the order of the items were added
|
||||
const sortedButtons = Array.from(coreButtonComponents.entries()).sort(
|
||||
([keyA], [keyB]) => {
|
||||
const indexA = configuredItems.indexOf(keyA);
|
||||
const indexB = configuredItems.indexOf(keyB);
|
||||
|
||||
if (indexA === -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return indexA - indexB;
|
||||
}
|
||||
);
|
||||
|
||||
const dag = DAG.from(
|
||||
Array.from(sortedButtons).map(([key, ButtonComponent]) => {
|
||||
const configuredIndex = configuredItems.indexOf(key);
|
||||
|
||||
const position =
|
||||
configuredIndex !== -1 ? configuredPositions.get(key) : null;
|
||||
|
||||
return [key, ButtonComponent, position];
|
||||
}),
|
||||
{
|
||||
...defaultDagOptions,
|
||||
// we need to keep track of the buttons that were added by plugins because they won't respect the values in
|
||||
// the post_menu setting
|
||||
onAddItem(key) {
|
||||
addedKeys?.add(key);
|
||||
},
|
||||
onDeleteItem(key) {
|
||||
addedKeys?.delete(key);
|
||||
},
|
||||
// when an item is replaced, we want the new button to inherit the properties defined by static methods in the
|
||||
// original button if they're not defined. To achieve this we keep track of the replacements in a map
|
||||
onReplaceItem: (key, newComponent, oldComponent) => {
|
||||
if (newComponent !== oldComponent) {
|
||||
replacementMap.set(newComponent, oldComponent);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// the map is initialized here, to ensure only the buttons manipulated by plugins using the API are tracked
|
||||
addedKeys = new Set();
|
||||
|
||||
// map to keep track of the labels that should be shown for each button if the plugins wants to override the default
|
||||
const buttonLabels = new Map();
|
||||
|
||||
const showMoreButtonPosition = configuredItems.indexOf(
|
||||
buttonKeys.SHOW_MORE
|
||||
);
|
||||
|
||||
const hiddenButtonKeys = this.configuredItems.filter((key) =>
|
||||
this.#hiddenItems.includes(key)
|
||||
);
|
||||
|
||||
// the DAG is not resolved now, instead we just use the object for convenience to pass a nice DAG API to be used
|
||||
// in the value transformer, and extract the data to be used later to resolve the DAG order
|
||||
const buttonsRegistry = applyMutableValueTransformer(
|
||||
"post-menu-buttons",
|
||||
dag,
|
||||
{
|
||||
...this.staticMethodsArgs,
|
||||
buttonLabels: {
|
||||
hide(key) {
|
||||
buttonLabels.set(key, false);
|
||||
},
|
||||
show(key) {
|
||||
buttonLabels.set(key, true);
|
||||
},
|
||||
default(key) {
|
||||
return buttonLabels.delete(key);
|
||||
},
|
||||
},
|
||||
buttonKeys,
|
||||
firstButtonKey: this.configuredItems[0],
|
||||
lastHiddenButtonKey: hiddenButtonKeys.length
|
||||
? hiddenButtonKeys[hiddenButtonKeys.length - 1]
|
||||
: null,
|
||||
lastItemBeforeMoreItemsButtonKey:
|
||||
showMoreButtonPosition > 0
|
||||
? this.configuredItems[showMoreButtonPosition - 1]
|
||||
: null,
|
||||
secondLastHiddenButtonKey:
|
||||
hiddenButtonKeys.length > 1
|
||||
? hiddenButtonKeys[hiddenButtonKeys.length - 2]
|
||||
: null,
|
||||
}
|
||||
);
|
||||
|
||||
return new Map(
|
||||
buttonsRegistry.entries().map(([key, ButtonComponent, position]) => {
|
||||
const config = new PostMenuButtonConfig({
|
||||
key,
|
||||
Component: ButtonComponent,
|
||||
apiAdded: addedKeys.has(key), // flag indicating if the button was added using the API
|
||||
owner: getOwner(this), // to be passed as argument to the static methods
|
||||
position,
|
||||
replacementMap,
|
||||
showLabel: buttonLabels.get(key),
|
||||
});
|
||||
|
||||
return [key, config];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@cached
|
||||
get configuredItems() {
|
||||
const list = this.siteSettings.post_menu
|
||||
.split("|")
|
||||
.filter(Boolean)
|
||||
.map((key) => {
|
||||
// if the post is a wiki, make Edit more prominent
|
||||
if (this.isWikiMode) {
|
||||
switch (key) {
|
||||
case buttonKeys.EDIT:
|
||||
return buttonKeys.REPLY;
|
||||
case buttonKeys.REPLY:
|
||||
return buttonKeys.EDIT;
|
||||
}
|
||||
}
|
||||
|
||||
return key;
|
||||
});
|
||||
|
||||
if (list.length > 0 && !list.includes(buttonKeys.SHOW_MORE)) {
|
||||
list.splice(list.length - 1, 0, buttonKeys.SHOW_MORE);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@cached
|
||||
get configuredPositions() {
|
||||
let referencePosition = "before";
|
||||
|
||||
const configuredItems = this.configuredItems;
|
||||
|
||||
return new Map(
|
||||
configuredItems.map((key, index) => {
|
||||
if (key === buttonKeys.SHOW_MORE) {
|
||||
referencePosition = "after";
|
||||
return [key, null];
|
||||
} else if (
|
||||
referencePosition === "before" &&
|
||||
index < configuredItems.length - 1
|
||||
) {
|
||||
return [
|
||||
key,
|
||||
{
|
||||
[referencePosition]: [buttonKeys.SHOW_MORE],
|
||||
},
|
||||
];
|
||||
} else if (referencePosition === "after" && index > 0) {
|
||||
return [
|
||||
key,
|
||||
{
|
||||
[referencePosition]: [buttonKeys.SHOW_MORE],
|
||||
},
|
||||
];
|
||||
} else {
|
||||
return [key, null];
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@cached
|
||||
get extraControls() {
|
||||
const items = Array.from(this.registeredButtons.values())
|
||||
.filter(
|
||||
(button) =>
|
||||
isPresent(button) && button.extraControls(this.staticMethodsArgs)
|
||||
)
|
||||
.map((button) => [button.key, button, button.position]);
|
||||
|
||||
return DAG.from(items, defaultDagOptions)
|
||||
.resolve()
|
||||
.map(({ value }) => value);
|
||||
}
|
||||
|
||||
@cached
|
||||
get availableButtons() {
|
||||
const items = this.configuredItems;
|
||||
|
||||
return Array.from(this.registeredButtons.values()).filter(
|
||||
(button) =>
|
||||
(button.apiAdded || items.includes(button.key)) &&
|
||||
!button.extraControls(this.staticMethodsArgs)
|
||||
);
|
||||
}
|
||||
|
||||
@cached
|
||||
get availableCollapsedButtons() {
|
||||
const hiddenItems = this.#hiddenItems;
|
||||
|
||||
if (
|
||||
isEmpty(hiddenItems) ||
|
||||
!this.collapsed ||
|
||||
!this.isShowMoreButtonAvailable
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items = this.availableButtons.filter((button) => {
|
||||
const hidden = button.hidden(this.staticMethodsArgs);
|
||||
|
||||
// when the value returned by hidden is explicitly false we ignore the hidden items specified in the
|
||||
// site setting
|
||||
if (hidden === false || button.key === buttonKeys.SHOW_MORE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.args.post.reviewable_id && button.key === buttonKeys.FLAG) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hidden || hiddenItems.includes(button.key);
|
||||
});
|
||||
|
||||
if (items.length <= 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
@cached
|
||||
get renderableCollapsedButtons() {
|
||||
return this.availableCollapsedButtons.filter((button) =>
|
||||
button.shouldRender(this.staticMethodsArgs)
|
||||
);
|
||||
}
|
||||
|
||||
@cached
|
||||
get visibleButtons() {
|
||||
const nonCollapsed = this.availableButtons.filter((button) => {
|
||||
return !this.availableCollapsedButtons.includes(button);
|
||||
});
|
||||
|
||||
return DAG.from(
|
||||
nonCollapsed.map((button) => [button.key, button, button.position]),
|
||||
defaultDagOptions
|
||||
)
|
||||
.resolve()
|
||||
.map(({ value }) => value);
|
||||
}
|
||||
|
||||
get repliesButton() {
|
||||
return this.registeredButtons.get(buttonKeys.REPLIES);
|
||||
}
|
||||
|
||||
get showMoreButton() {
|
||||
return this.registeredButtons.get(buttonKeys.SHOW_MORE);
|
||||
}
|
||||
|
||||
get remainingLikedUsers() {
|
||||
return this.totalLikedUsers - this.likedUsers.length;
|
||||
}
|
||||
|
||||
get remainingReaders() {
|
||||
return this.totalReaders - this.readers.length;
|
||||
}
|
||||
|
||||
get isWikiMode() {
|
||||
return this.args.post.wiki && this.args.post.can_edit;
|
||||
}
|
||||
|
||||
get isShowMoreButtonAvailable() {
|
||||
return (
|
||||
this.availableButtons.some(
|
||||
(button) => button.key === buttonKeys.SHOW_MORE
|
||||
) ||
|
||||
this.extraControls.some((button) => button.key === buttonKeys.SHOW_MORE)
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
async toggleLike() {
|
||||
if (!this.currentUser) {
|
||||
this.keyValueStore &&
|
||||
this.keyValueStore.set({
|
||||
key: "likedPostId",
|
||||
value: this.args.post.id,
|
||||
});
|
||||
|
||||
this.args.showLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.capabilities.userHasBeenActive && this.capabilities.canVibrate) {
|
||||
navigator.vibrate(VIBRATE_DURATION);
|
||||
}
|
||||
|
||||
await this.args.toggleLike();
|
||||
|
||||
if (!this.collapsed) {
|
||||
await this.#fetchWhoLiked();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
openAdminMenu(_, event) {
|
||||
this.menu.show(event.target, {
|
||||
identifier: "admin-post-menu",
|
||||
component: AdminPostMenu,
|
||||
modalForMobile: true,
|
||||
autofocus: true,
|
||||
data: {
|
||||
post: this.args.post,
|
||||
changeNotice: this.args.changeNotice,
|
||||
changePostOwner: this.args.changePostOwner,
|
||||
grantBadge: this.args.grantBadge,
|
||||
lockPost: this.args.lockPost,
|
||||
permanentlyDeletePost: this.args.permanentlyDeletePost,
|
||||
rebakePost: this.args.rebakePost,
|
||||
showPagePublish: this.args.showPagePublish,
|
||||
togglePostType: this.args.togglePostType,
|
||||
toggleWiki: this.args.toggleWiki,
|
||||
unhidePost: this.args.unhidePost,
|
||||
unlockPost: this.args.unlockPost,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
refreshReaders() {
|
||||
if (this.readers.length) {
|
||||
return this.#fetchWhoRead();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
showDeleteTopicModal() {
|
||||
this.modal.show(DeleteTopicDisallowedModal);
|
||||
}
|
||||
|
||||
@action
|
||||
async showMoreActions() {
|
||||
this.collapsed = false;
|
||||
|
||||
const fetchData = [
|
||||
!this.isWhoLikedVisible && this.#fetchWhoLiked(),
|
||||
!this.isWhoReadVisible &&
|
||||
this.args.showReadIndicator &&
|
||||
this.#fetchWhoRead(),
|
||||
].filter(Boolean);
|
||||
|
||||
await Promise.all(fetchData);
|
||||
}
|
||||
|
||||
@action
|
||||
toggleWhoLiked() {
|
||||
if (this.isWhoLikedVisible) {
|
||||
this.isWhoLikedVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.#fetchWhoLiked();
|
||||
}
|
||||
|
||||
@action
|
||||
toggleWhoRead() {
|
||||
if (this.isWhoReadVisible) {
|
||||
this.isWhoReadVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.#fetchWhoRead();
|
||||
}
|
||||
|
||||
@cached
|
||||
get #hiddenItems() {
|
||||
const setting = this.siteSettings.post_menu_hidden_items;
|
||||
|
||||
if (isEmpty(setting)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return setting
|
||||
.split("|")
|
||||
.filter(
|
||||
(itemKey) =>
|
||||
!this.args.post.bookmarked || itemKey !== buttonKeys.BOOKMARK
|
||||
);
|
||||
}
|
||||
|
||||
async #fetchWhoLiked() {
|
||||
const users = await this.store.find("post-action-user", {
|
||||
id: this.args.post.id,
|
||||
post_action_type_id: LIKE_ACTION,
|
||||
});
|
||||
|
||||
this.likedUsers = users.map(smallUserAttributes);
|
||||
this.totalLikedUsers = users.totalRows;
|
||||
this.isWhoLikedVisible = true;
|
||||
}
|
||||
|
||||
async #fetchWhoRead() {
|
||||
const users = await this.store.find("post-reader", {
|
||||
id: this.args.post.id,
|
||||
});
|
||||
|
||||
this.readers = users.map(smallUserAttributes);
|
||||
this.totalReaders = users.totalRows;
|
||||
this.isWhoReadVisible = true;
|
||||
}
|
||||
|
||||
<template>
|
||||
{{! The section tag can't be include while we're still using the widget shim }}
|
||||
{{! <section class="post-menu-area clearfix"> }}
|
||||
<PluginOutlet
|
||||
@name="post-menu"
|
||||
@outletArgs={{hash post=@post state=this.state}}
|
||||
>
|
||||
<nav
|
||||
class={{concatClass
|
||||
"post-controls"
|
||||
"glimmer-post-menu"
|
||||
(if
|
||||
(and
|
||||
(this.showMoreButton.shouldRender
|
||||
(hash post=this.post state=this.state)
|
||||
)
|
||||
this.collapsed
|
||||
)
|
||||
"collapsed"
|
||||
"expanded"
|
||||
)
|
||||
(if
|
||||
this.siteSettings.enable_filtered_replies_view
|
||||
"replies-button-visible"
|
||||
)
|
||||
}}
|
||||
>
|
||||
{{! do not include PluginOutlets here, use the PostMenu DAG API instead }}
|
||||
{{#each this.extraControls key="key" as |extraControl|}}
|
||||
<PostMenuButtonWrapper
|
||||
@buttonActions={{this.buttonActions}}
|
||||
@buttonConfig={{extraControl}}
|
||||
@post={{@post}}
|
||||
@state={{this.state}}
|
||||
/>
|
||||
{{/each}}
|
||||
<div class="actions">
|
||||
{{#each this.visibleButtons key="key" as |button|}}
|
||||
<PostMenuButtonWrapper
|
||||
@buttonActions={{this.buttonActions}}
|
||||
@buttonConfig={{button}}
|
||||
@post={{@post}}
|
||||
@state={{this.state}}
|
||||
/>
|
||||
{{/each}}
|
||||
</div>
|
||||
</nav>
|
||||
{{#if this.isWhoReadVisible}}
|
||||
<SmallUserList
|
||||
class="who-read"
|
||||
@addSelf={{false}}
|
||||
@ariaLabel={{i18n
|
||||
"post.actions.people.sr_post_readers_list_description"
|
||||
}}
|
||||
@count={{if
|
||||
this.remainingReaders
|
||||
this.remainingReaders
|
||||
this.totalReaders
|
||||
}}
|
||||
@description={{if
|
||||
this.remainingReaders
|
||||
"post.actions.people.read_capped"
|
||||
"post.actions.people.read"
|
||||
}}
|
||||
@users={{this.readers}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if this.isWhoLikedVisible}}
|
||||
<SmallUserList
|
||||
class="who-liked"
|
||||
@addSelf={{and @post.liked (eq this.remainingLikedUsers 0)}}
|
||||
@ariaLabel={{i18n
|
||||
"post.actions.people.sr_post_likers_list_description"
|
||||
}}
|
||||
@count={{if
|
||||
this.remainingLikedUsers
|
||||
this.remainingLikedUsers
|
||||
this.totalLikedUsers
|
||||
}}
|
||||
@description={{if
|
||||
this.remainingLikedUsers
|
||||
"post.actions.people.like_capped"
|
||||
"post.actions.people.like"
|
||||
}}
|
||||
@users={{this.likedUsers}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if this.collapsedButtons}}
|
||||
<UserTip
|
||||
@id="post_menu"
|
||||
@triggerSelector=".post-controls .actions .show-more-actions"
|
||||
@placement="top"
|
||||
@titleText={{i18n "user_tips.post_menu.title"}}
|
||||
@contentText={{i18n "user_tips.post_menu.content"}}
|
||||
/>
|
||||
{{/if}}
|
||||
</PluginOutlet>
|
||||
{{! </section> }}
|
||||
</template>
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { helperContext } from "discourse-common/lib/helpers";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
|
||||
export default class PostMenuButtonConfig {
|
||||
#Component;
|
||||
#apiAdded;
|
||||
#key;
|
||||
#owner;
|
||||
#position;
|
||||
#replacementMap;
|
||||
#showLabel;
|
||||
|
||||
constructor({
|
||||
key,
|
||||
Component,
|
||||
apiAdded,
|
||||
owner,
|
||||
position,
|
||||
replacementMap,
|
||||
showLabel,
|
||||
}) {
|
||||
this.#Component = Component;
|
||||
this.#apiAdded = apiAdded;
|
||||
this.#key = key;
|
||||
this.#owner = owner;
|
||||
this.#position = position;
|
||||
this.#replacementMap = replacementMap;
|
||||
this.#showLabel = showLabel;
|
||||
}
|
||||
|
||||
get Component() {
|
||||
return this.#Component;
|
||||
}
|
||||
|
||||
get apiAdded() {
|
||||
return this.#apiAdded;
|
||||
}
|
||||
|
||||
@bind
|
||||
hidden(args) {
|
||||
return this.#staticPropertyWithReplacementFallback({
|
||||
property: "hidden",
|
||||
args,
|
||||
defaultValue: null,
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
delegateShouldRenderToTemplate(args) {
|
||||
return this.#staticPropertyWithReplacementFallback({
|
||||
property: "delegateShouldRenderToTemplate",
|
||||
args,
|
||||
defaultValue: false,
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
extraControls(args) {
|
||||
return this.#staticPropertyWithReplacementFallback({
|
||||
property: "extraControls",
|
||||
args,
|
||||
defaultValue: false,
|
||||
});
|
||||
}
|
||||
|
||||
get key() {
|
||||
return this.#key;
|
||||
}
|
||||
|
||||
get position() {
|
||||
return this.#position;
|
||||
}
|
||||
|
||||
@bind
|
||||
setShowLabel(value) {
|
||||
this.#showLabel = value;
|
||||
}
|
||||
|
||||
@bind
|
||||
shouldRender(args) {
|
||||
return this.#staticPropertyWithReplacementFallback({
|
||||
property: "shouldRender",
|
||||
args,
|
||||
defaultValue: true,
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
showLabel(args) {
|
||||
return (
|
||||
this.#showLabel ??
|
||||
this.#staticPropertyWithReplacementFallback({
|
||||
property: "showLabel",
|
||||
args,
|
||||
defaultValue: null,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#staticPropertyWithReplacementFallback(
|
||||
{ klass = this.#Component, property, args, defaultValue },
|
||||
_usedKlasses = new WeakSet()
|
||||
) {
|
||||
// fallback to the default value if the klass is not defined, i.e., the button was not replaced
|
||||
// or if the klass was already used to avoid an infinite recursion in case of a circular reference
|
||||
if (!klass || _usedKlasses.has(klass)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
let value;
|
||||
|
||||
if (typeof klass[property] === "undefined") {
|
||||
// fallback to the replacement map if the property is not defined
|
||||
return this.#staticPropertyWithReplacementFallback(
|
||||
{
|
||||
klass: this.#replacementMap.get(klass) || null, // passing null explicitly to avoid using the default value
|
||||
property,
|
||||
args,
|
||||
defaultValue,
|
||||
},
|
||||
_usedKlasses.add(klass)
|
||||
);
|
||||
} else if (typeof klass[property] === "function") {
|
||||
value = klass[property](args, helperContext(), this.#owner);
|
||||
} else {
|
||||
value = klass[property];
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import { or } from "truth-helpers";
|
||||
import { showAlert } from "../../../lib/post-action-feedback";
|
||||
|
||||
export default class PostMenuButtonWrapper extends Component {
|
||||
#element;
|
||||
|
||||
get delegateShouldRenderToTemplate() {
|
||||
return this.args.buttonConfig.delegateShouldRenderToTemplate(this.args);
|
||||
}
|
||||
|
||||
get hidden() {
|
||||
return this.args.buttonConfig.hidden(this.args);
|
||||
}
|
||||
|
||||
get shouldRender() {
|
||||
if (this.delegateShouldRenderToTemplate) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.args.buttonConfig.shouldRender(this.args);
|
||||
}
|
||||
|
||||
get showLabel() {
|
||||
return this.args.buttonConfig.showLabel(this.args);
|
||||
}
|
||||
|
||||
@action
|
||||
setElement(element) {
|
||||
this.#element = element;
|
||||
}
|
||||
|
||||
@action
|
||||
sharedBehaviorOnClick(event) {
|
||||
event.currentTarget?.blur();
|
||||
}
|
||||
|
||||
@action
|
||||
showFeedback(messageKey) {
|
||||
if (this.#element) {
|
||||
showAlert(this.args.post.id, this.args.buttonConfig.key, messageKey, {
|
||||
actionBtn: this.#element,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if (or this.shouldRender this.delegateShouldRenderToTemplate)}}
|
||||
<@buttonConfig.Component
|
||||
class="btn-flat"
|
||||
@buttonActions={{@buttonActions}}
|
||||
@hidden={{this.hidden}}
|
||||
@post={{@post}}
|
||||
@shouldRender={{this.shouldRender}}
|
||||
@showFeedback={{this.showFeedback}}
|
||||
@showLabel={{this.showLabel}}
|
||||
@state={{@state}}
|
||||
{{didInsert this.setElement}}
|
||||
{{on "click" this.sharedBehaviorOnClick}}
|
||||
/>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import Component from "@glimmer/component";
|
||||
import DButton from "discourse/components/d-button";
|
||||
|
||||
export default class PostMenuAdminButton extends Component {
|
||||
static shouldRender(args) {
|
||||
return (
|
||||
args.post.canManage || args.post.can_wiki || args.post.canEditStaffNotes
|
||||
);
|
||||
}
|
||||
|
||||
<template>
|
||||
<DButton
|
||||
class="post-action-menu__admin show-post-admin-menu"
|
||||
...attributes
|
||||
@action={{@buttonActions.openAdminMenu}}
|
||||
@forwardEvent={{true}}
|
||||
@icon="wrench"
|
||||
@label={{if @showLabel "post.controls.admin_action"}}
|
||||
@title="post.controls.admin"
|
||||
/>
|
||||
</template>
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { cached } from "@glimmer/tracking";
|
||||
import { getOwner } from "@ember/owner";
|
||||
import BookmarkMenu from "discourse/components/bookmark-menu";
|
||||
import PostBookmarkManager from "discourse/lib/post-bookmark-manager";
|
||||
|
||||
export default class PostMenuBookmarkButton extends Component {
|
||||
static shouldRender(args) {
|
||||
return !!args.post.canBookmark;
|
||||
}
|
||||
|
||||
@cached
|
||||
get bookmarkManager() {
|
||||
return new PostBookmarkManager(getOwner(this), this.args.post);
|
||||
}
|
||||
|
||||
<template>
|
||||
<BookmarkMenu
|
||||
class="post-action-menu__bookmark"
|
||||
...attributes
|
||||
@bookmarkManager={{this.bookmarkManager}}
|
||||
@showLabel={{@showLabel}}
|
||||
/>
|
||||
</template>
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import DButton from "discourse/components/d-button";
|
||||
|
||||
const PostMenuCopyLinkButton = <template>
|
||||
<DButton
|
||||
class="post-action-menu__copy-link"
|
||||
...attributes
|
||||
@action={{@buttonActions.copyLink}}
|
||||
@icon="d-post-share"
|
||||
@label={{if @showLabel "post.controls.copy_action"}}
|
||||
@title="post.controls.copy_title"
|
||||
/>
|
||||
</template>;
|
||||
|
||||
export default PostMenuCopyLinkButton;
|
||||
@@ -0,0 +1,133 @@
|
||||
import Component from "@glimmer/component";
|
||||
import DButton from "discourse/components/d-button";
|
||||
|
||||
export const BUTTON_ACTION_MODE_DELETE = "delete";
|
||||
export const BUTTON_ACTION_MODE_DELETE_TOPIC = "delete-topic";
|
||||
export const BUTTON_ACTION_MODE_SHOW_FLAG_DELETE = "show-flag-delete";
|
||||
export const BUTTON_ACTION_MODE_RECOVER = "recover";
|
||||
export const BUTTON_ACTION_MODE_RECOVER_TOPIC = "recover-topic";
|
||||
export const BUTTON_ACTION_MODE_RECOVERING = "recovering";
|
||||
export const BUTTON_ACTION_MODE_RECOVERING_TOPIC = "recovering-topic";
|
||||
|
||||
export default class PostMenuDeleteButton extends Component {
|
||||
static shouldRender(args) {
|
||||
return !!PostMenuDeleteButton.modeFor(args.post);
|
||||
}
|
||||
|
||||
static modeFor(post) {
|
||||
if (post.canRecoverTopic) {
|
||||
return BUTTON_ACTION_MODE_RECOVER_TOPIC;
|
||||
} else if (post.canDeleteTopic) {
|
||||
return BUTTON_ACTION_MODE_DELETE_TOPIC;
|
||||
} else if (post.canRecover) {
|
||||
return BUTTON_ACTION_MODE_RECOVER;
|
||||
} else if (post.canDelete) {
|
||||
return BUTTON_ACTION_MODE_DELETE;
|
||||
} else if (post.showFlagDelete) {
|
||||
return BUTTON_ACTION_MODE_SHOW_FLAG_DELETE;
|
||||
} else if (post.isRecovering) {
|
||||
return BUTTON_ACTION_MODE_RECOVERING;
|
||||
} else if (post.isRecoveringTopic) {
|
||||
return BUTTON_ACTION_MODE_RECOVERING_TOPIC;
|
||||
}
|
||||
}
|
||||
|
||||
get className() {
|
||||
switch (this.#activeMode) {
|
||||
case BUTTON_ACTION_MODE_RECOVER:
|
||||
case BUTTON_ACTION_MODE_RECOVER_TOPIC:
|
||||
case BUTTON_ACTION_MODE_RECOVERING:
|
||||
case BUTTON_ACTION_MODE_RECOVERING_TOPIC:
|
||||
return "post-action-menu__recover recover";
|
||||
default:
|
||||
return "post-action-menu__delete delete";
|
||||
}
|
||||
}
|
||||
|
||||
get icon() {
|
||||
switch (this.#activeMode) {
|
||||
case BUTTON_ACTION_MODE_RECOVER:
|
||||
case BUTTON_ACTION_MODE_RECOVER_TOPIC:
|
||||
case BUTTON_ACTION_MODE_RECOVERING:
|
||||
case BUTTON_ACTION_MODE_RECOVERING_TOPIC:
|
||||
return "arrow-rotate-left";
|
||||
|
||||
default:
|
||||
return "trash-can";
|
||||
}
|
||||
}
|
||||
|
||||
get label() {
|
||||
switch (this.#activeMode) {
|
||||
case BUTTON_ACTION_MODE_DELETE:
|
||||
return "post.controls.delete_action";
|
||||
|
||||
case BUTTON_ACTION_MODE_DELETE_TOPIC:
|
||||
case BUTTON_ACTION_MODE_SHOW_FLAG_DELETE:
|
||||
return "topic.actions.delete";
|
||||
|
||||
case BUTTON_ACTION_MODE_RECOVER:
|
||||
case BUTTON_ACTION_MODE_RECOVERING:
|
||||
return "post.controls.undelete_action";
|
||||
|
||||
case BUTTON_ACTION_MODE_RECOVER_TOPIC:
|
||||
case BUTTON_ACTION_MODE_RECOVERING_TOPIC:
|
||||
return "topic.actions.recover";
|
||||
}
|
||||
}
|
||||
|
||||
get title() {
|
||||
switch (this.#activeMode) {
|
||||
case BUTTON_ACTION_MODE_DELETE:
|
||||
return "post.controls.delete";
|
||||
|
||||
case BUTTON_ACTION_MODE_DELETE_TOPIC:
|
||||
return "post.controls.delete_topic";
|
||||
|
||||
case BUTTON_ACTION_MODE_SHOW_FLAG_DELETE:
|
||||
return "post.controls.delete_topic_disallowed";
|
||||
|
||||
case BUTTON_ACTION_MODE_RECOVER:
|
||||
case BUTTON_ACTION_MODE_RECOVERING:
|
||||
return "post.controls.undelete";
|
||||
|
||||
case BUTTON_ACTION_MODE_RECOVER_TOPIC:
|
||||
case BUTTON_ACTION_MODE_RECOVERING_TOPIC:
|
||||
return "topic.actions.recover";
|
||||
}
|
||||
}
|
||||
|
||||
get activeAction() {
|
||||
if (this.args.post.canRecoverTopic) {
|
||||
return this.args.buttonActions.recoverPost;
|
||||
} else if (this.args.post.canDeleteTopic) {
|
||||
return this.args.buttonActions.deletePost;
|
||||
} else if (this.args.post.canRecover) {
|
||||
return this.args.buttonActions.recoverPost;
|
||||
} else if (this.args.post.canDelete) {
|
||||
return this.args.buttonActions.deletePost;
|
||||
} else if (this.args.post.showFlagDelete) {
|
||||
return this.args.buttonActions.showDeleteTopicModal;
|
||||
}
|
||||
}
|
||||
|
||||
get disabled() {
|
||||
return !this.activeAction;
|
||||
}
|
||||
|
||||
get #activeMode() {
|
||||
return this.constructor.modeFor(this.args.post);
|
||||
}
|
||||
|
||||
<template>
|
||||
<DButton
|
||||
class={{this.className}}
|
||||
...attributes
|
||||
disabled={{this.disabled}}
|
||||
@action={{this.activeAction}}
|
||||
@icon={{this.icon}}
|
||||
@label={{if @showLabel this.label}}
|
||||
@title={{this.title}}
|
||||
/>
|
||||
</template>
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
|
||||
export default class PostMenuEditButton extends Component {
|
||||
static hidden(args) {
|
||||
if (args.state.isWikiMode || (args.post.can_edit && args.post.yours)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// returning null here allows collapseByDefault to fallback to the value configured in the settings for the button
|
||||
return null;
|
||||
}
|
||||
|
||||
static shouldRender(args) {
|
||||
return args.post.can_edit;
|
||||
}
|
||||
|
||||
@service site;
|
||||
|
||||
get showLabel() {
|
||||
return (
|
||||
this.args.showLabel ??
|
||||
(this.site.desktopView && this.args.state.isWikiMode)
|
||||
);
|
||||
}
|
||||
|
||||
<template>
|
||||
<DButton
|
||||
class={{concatClass
|
||||
"post-action-menu__edit"
|
||||
"edit"
|
||||
(if @post.wiki "create")
|
||||
}}
|
||||
...attributes
|
||||
@action={{@buttonActions.editPost}}
|
||||
@icon={{if @post.wiki "far-edit" "pencil-alt"}}
|
||||
@label={{if this.showLabel "post.controls.edit_action"}}
|
||||
@title="post.controls.edit"
|
||||
/>
|
||||
</template>
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import { gt } from "truth-helpers";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
|
||||
export default class PostMenuFlagButton extends Component {
|
||||
static shouldRender(args) {
|
||||
const { reviewable_id, canFlag, hidden } = args.post;
|
||||
return reviewable_id || (canFlag && !hidden);
|
||||
}
|
||||
|
||||
@action
|
||||
navigateToReviewable() {
|
||||
DiscourseURL.routeTo(`/review/${this.args.post.reviewable_id}`);
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="double-button">
|
||||
{{#if @post.reviewable_id}}
|
||||
<DButton
|
||||
class={{concatClass
|
||||
"button-count"
|
||||
(if (gt @post.reviewable_score_pending_count 0) "has-pending")
|
||||
}}
|
||||
...attributes
|
||||
@action={{this.navigateToReviewable}}
|
||||
>
|
||||
<span>{{@post.reviewable_score_count}}</span>
|
||||
</DButton>
|
||||
{{/if}}
|
||||
<DButton
|
||||
class="post-action-menu__flag create-flag"
|
||||
...attributes
|
||||
@action={{@buttonActions.showFlags}}
|
||||
@icon="flag"
|
||||
@label={{if @showLabel "post.controls.flag_action"}}
|
||||
@title="post.controls.flag"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import discourseLater from "discourse-common/lib/later";
|
||||
|
||||
export default class PostMenuLikeButton extends Component {
|
||||
static shouldRender(args) {
|
||||
return args.post.showLike || args.post.likeCount > 0;
|
||||
}
|
||||
|
||||
@service currentUser;
|
||||
|
||||
@tracked isAnimated = false;
|
||||
|
||||
get disabled() {
|
||||
return this.currentUser && !this.args.post.canToggleLike;
|
||||
}
|
||||
|
||||
get title() {
|
||||
// If the user has already liked the post and doesn't have permission
|
||||
// to undo that operation, then indicate via the title that they've liked it
|
||||
// and disable the button. Otherwise, set the title even if the user
|
||||
// is anonymous (meaning they don't currently have permission to like);
|
||||
// this is important for accessibility.
|
||||
|
||||
if (this.args.post.liked && !this.args.post.canToggleLike) {
|
||||
return "post.controls.has_liked";
|
||||
}
|
||||
|
||||
return this.args.post.liked
|
||||
? "post.controls.undo_like"
|
||||
: "post.controls.like";
|
||||
}
|
||||
|
||||
@action
|
||||
async toggleLike() {
|
||||
this.isAnimated = true;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
discourseLater(async () => {
|
||||
this.isAnimated = false;
|
||||
await this.args.buttonActions.toggleLike();
|
||||
resolve();
|
||||
}, 400);
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if @post.showLike}}
|
||||
<div class="double-button">
|
||||
<LikeCount
|
||||
...attributes
|
||||
@action={{@buttonActions.toggleWhoLiked}}
|
||||
@state={{@state}}
|
||||
@post={{@post}}
|
||||
/>
|
||||
<DButton
|
||||
class={{concatClass
|
||||
"post-action-menu__like"
|
||||
"toggle-like"
|
||||
"btn-icon"
|
||||
(if this.isAnimated "heart-animation")
|
||||
(if @post.liked "has-like fade-out" "like")
|
||||
}}
|
||||
...attributes
|
||||
data-post-id={{@post.id}}
|
||||
disabled={{this.disabled}}
|
||||
@action={{this.toggleLike}}
|
||||
@icon={{if @post.liked "d-liked" "d-unliked"}}
|
||||
@label={{if @showLabel "post.controls.like_action"}}
|
||||
@title={{this.title}}
|
||||
/>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="double-button">
|
||||
<LikeCount
|
||||
...attributes
|
||||
@action={{@buttonActions.toggleWhoLiked}}
|
||||
@state={{@state}}
|
||||
@post={{@post}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
||||
|
||||
class LikeCount extends Component {
|
||||
get icon() {
|
||||
if (!this.args.post.showLike) {
|
||||
return this.args.post.yours ? "d-liked" : "d-unliked";
|
||||
}
|
||||
|
||||
if (this.args.post.yours) {
|
||||
return "d-liked";
|
||||
}
|
||||
}
|
||||
|
||||
get translatedTitle() {
|
||||
let title;
|
||||
|
||||
if (this.args.post.liked) {
|
||||
title =
|
||||
this.args.post.likeCount === 1
|
||||
? "post.has_likes_title_only_you"
|
||||
: "post.has_likes_title_you";
|
||||
} else {
|
||||
title = "post.has_likes_title";
|
||||
}
|
||||
|
||||
return i18n(title, {
|
||||
count: this.args.post.liked
|
||||
? this.args.post.likeCount - 1
|
||||
: this.args.post.likeCount,
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if @post.likeCount}}
|
||||
<DButton
|
||||
class={{concatClass
|
||||
"post-action-menu__like-count"
|
||||
"like-count"
|
||||
"button-count"
|
||||
"highlight-action"
|
||||
(if @post.yours "my-likes" "regular-likes")
|
||||
}}
|
||||
...attributes
|
||||
@ariaPressed={{@state.isWhoReadVisible}}
|
||||
@translatedAriaLabel={{i18n
|
||||
"post.sr_post_like_count_button"
|
||||
count=@post.likeCount
|
||||
}}
|
||||
@translatedTitle={{this.translatedTitle}}
|
||||
@action={{@action}}
|
||||
>
|
||||
{{@post.likeCount}}
|
||||
{{!--
|
||||
When displayed, the icon on the Like Count button is aligned to the right
|
||||
To get the desired effect will use the {{yield}} in the DButton component to our advantage
|
||||
introducing manually the icon after the label
|
||||
--}}
|
||||
{{#if this.icon}}
|
||||
{{~icon this.icon~}}
|
||||
{{/if}}
|
||||
</DButton>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import Component from "@glimmer/component";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
|
||||
export default class PostMenuReadButton extends Component {
|
||||
static shouldRender(args) {
|
||||
return args.state.showReadIndicator && args.post.readers_count > 0;
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="double-button">
|
||||
<DButton
|
||||
class="post-action-menu__read read-indicator button-count"
|
||||
...attributes
|
||||
@ariaPressed={{@state.isWhoReadVisible}}
|
||||
@action={{@buttonActions.toggleWhoRead}}
|
||||
@translatedAriaLabel={{i18n
|
||||
"post.sr_post_read_count_button"
|
||||
count=@post.readers_count
|
||||
}}
|
||||
@title="post.controls.read_indicator"
|
||||
>
|
||||
{{@post.readers_count}}
|
||||
</DButton>
|
||||
<DButton
|
||||
...attributes
|
||||
@action={{@buttonActions.toggleWhoRead}}
|
||||
@icon="book-reader"
|
||||
@title="post.controls.read_indicator"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { concat } from "@ember/helper";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { and, not } from "truth-helpers";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
|
||||
export default class PostMenuRepliesButton extends Component {
|
||||
static extraControls = true;
|
||||
|
||||
static shouldRender(args) {
|
||||
const replyCount = args.post.reply_count;
|
||||
|
||||
if (!replyCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !(
|
||||
replyCount === 1 &&
|
||||
args.state.replyDirectlyBelow &&
|
||||
args.state.suppressReplyDirectlyBelow
|
||||
);
|
||||
}
|
||||
|
||||
@service site;
|
||||
|
||||
get disabled() {
|
||||
return !!this.args.post.deleted;
|
||||
}
|
||||
|
||||
get translatedTitle() {
|
||||
if (!this.args.state.filteredRepliesView) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.args.state.repliesShown
|
||||
? i18n("post.view_all_posts")
|
||||
: i18n("post.filtered_replies_hint", {
|
||||
count: this.args.post.reply_count,
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
<DButton
|
||||
class="show-replies btn-icon-text"
|
||||
...attributes
|
||||
disabled={{this.disabled}}
|
||||
@action={{@buttonActions.toggleReplies}}
|
||||
@ariaControls={{concat "embedded-posts__bottom--" @post.post_number}}
|
||||
@ariaExpanded={{and @state.repliesShown (not @state.filteredRepliesView)}}
|
||||
@ariaPressed={{unless @state.filteredRepliesView @state.repliesShown}}
|
||||
@translatedAriaLabel={{i18n
|
||||
"post.sr_expand_replies"
|
||||
count=@post.reply_count
|
||||
}}
|
||||
@translatedLabel={{i18n
|
||||
(if this.site.mobileView "post.has_replies_count" "post.has_replies")
|
||||
count=@post.reply_count
|
||||
}}
|
||||
@translatedTitle={{this.translatedTitle}}
|
||||
>
|
||||
{{!--
|
||||
The icon on the replies button is aligned to the right
|
||||
To get the desired effect will use the {{yield}} in the DButton component to our advantage
|
||||
introducing manually the icon after the label
|
||||
--}}
|
||||
{{~icon (if @state.repliesShown "chevron-up" "chevron-down")~}}
|
||||
</DButton>
|
||||
</template>
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
|
||||
export default class PostMenuReplyButton extends Component {
|
||||
static shouldRender(args) {
|
||||
return args.state.canCreatePost;
|
||||
}
|
||||
|
||||
@service site;
|
||||
|
||||
get showLabel() {
|
||||
return (
|
||||
this.args.showLabel ??
|
||||
(this.site.desktopView && !this.args.state.isWikiMode)
|
||||
);
|
||||
}
|
||||
|
||||
<template>
|
||||
<DButton
|
||||
class={{concatClass
|
||||
"post-action-menu__reply"
|
||||
"reply"
|
||||
(if this.showLabel "create fade-out")
|
||||
}}
|
||||
...attributes
|
||||
@action={{@buttonActions.replyToPost}}
|
||||
@icon="reply"
|
||||
@label={{if this.showLabel "topic.reply.title"}}
|
||||
@title="post.controls.reply"
|
||||
@translatedAriaLabel={{i18n
|
||||
"post.sr_reply_to"
|
||||
post_number=@post.post_number
|
||||
username=@post.username
|
||||
}}
|
||||
/>
|
||||
</template>
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import DButton from "discourse/components/d-button";
|
||||
|
||||
const PostMenuShareButton = <template>
|
||||
<DButton
|
||||
class="post-action-menu__share share"
|
||||
...attributes
|
||||
@action={{@buttonActions.share}}
|
||||
@icon="d-post-share"
|
||||
@label={{if @showLabel "post.controls.share_action"}}
|
||||
@title="post.controls.share"
|
||||
/>
|
||||
</template>;
|
||||
|
||||
export default PostMenuShareButton;
|
||||
@@ -0,0 +1,18 @@
|
||||
import Component from "@glimmer/component";
|
||||
import DButton from "discourse/components/d-button";
|
||||
|
||||
export default class PostMenuShowMoreButton extends Component {
|
||||
static shouldRender(args) {
|
||||
return args.state.collapsedButtons.length && args.state.collapsed;
|
||||
}
|
||||
|
||||
<template>
|
||||
<DButton
|
||||
class="post-action-menu__show-more show-more-actions"
|
||||
...attributes
|
||||
@action={{@buttonActions.showMoreActions}}
|
||||
@icon="ellipsis-h"
|
||||
@title="show_more"
|
||||
/>
|
||||
</template>
|
||||
}
|
||||
@@ -19,10 +19,10 @@ export default class SmallUserList extends Component {
|
||||
}
|
||||
|
||||
get users() {
|
||||
let users = this.args.data.users;
|
||||
let users = this.args.users;
|
||||
|
||||
if (
|
||||
this.args.data.addSelf &&
|
||||
this.args.addSelf &&
|
||||
!users.some((u) => u.username === this.currentUser.username)
|
||||
) {
|
||||
users = users.concat(this.smallUserAtts(this.currentUser));
|
||||
@@ -38,38 +38,48 @@ export default class SmallUserList extends Component {
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#each this.users as |user|}}
|
||||
{{#if user.unknown}}
|
||||
<div
|
||||
title={{i18n "post.unknown_user"}}
|
||||
class="unknown"
|
||||
role="listitem"
|
||||
></div>
|
||||
{{else}}
|
||||
<a
|
||||
class="trigger-user-card"
|
||||
data-user-card={{user.username}}
|
||||
title={{user.username}}
|
||||
aria-hidden="false"
|
||||
role="listitem"
|
||||
{{#if this.users}}
|
||||
<div class="clearfix small-user-list" ...attributes>
|
||||
<span
|
||||
class="small-user-list-content"
|
||||
aria-label={{@ariaLabel}}
|
||||
role="list"
|
||||
>
|
||||
{{avatar user.template "tiny"}}
|
||||
</a>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{#each this.users key="username" as |user|}}
|
||||
{{#if user.unknown}}
|
||||
<div
|
||||
title={{i18n "post.unknown_user"}}
|
||||
class="unknown"
|
||||
role="listitem"
|
||||
></div>
|
||||
{{else}}
|
||||
<a
|
||||
class="trigger-user-card"
|
||||
data-user-card={{user.username}}
|
||||
title={{user.username}}
|
||||
aria-hidden="false"
|
||||
role="listitem"
|
||||
>
|
||||
{{avatar user.template "tiny"}}
|
||||
</a>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
{{#if @data.description}}
|
||||
{{#if this.postUrl}}
|
||||
<a href={{this.postUrl}}>
|
||||
<span aria-hidden="true" class="list-description">
|
||||
{{i18n @data.description count=@data.count}}
|
||||
</span>
|
||||
</a>
|
||||
{{else}}
|
||||
<span aria-hidden="true" class="list-description">
|
||||
{{i18n @data.description count=@data.count}}
|
||||
{{#if @description}}
|
||||
{{#if this.postUrl}}
|
||||
<a href={{this.postUrl}}>
|
||||
<span aria-hidden="true" class="list-description">
|
||||
{{i18n @description count=@count}}
|
||||
</span>
|
||||
</a>
|
||||
{{else}}
|
||||
<span aria-hidden="true" class="list-description">
|
||||
{{i18n @description count=@count}}
|
||||
</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import PostBookmarkManager from "discourse/lib/post-bookmark-manager";
|
||||
import { withSilencedDeprecations } from "discourse-common/lib/deprecated";
|
||||
|
||||
export default {
|
||||
name: "discourse-bookmark-menu",
|
||||
@@ -9,18 +10,20 @@ export default {
|
||||
|
||||
withPluginApi("0.10.1", (api) => {
|
||||
if (currentUser) {
|
||||
api.replacePostMenuButton("bookmark", {
|
||||
name: "bookmark-menu-shim",
|
||||
shouldRender: () => true,
|
||||
buildAttrs: (widget) => {
|
||||
return {
|
||||
post: widget.findAncestorModel(),
|
||||
bookmarkManager: new PostBookmarkManager(
|
||||
container,
|
||||
widget.findAncestorModel()
|
||||
),
|
||||
};
|
||||
},
|
||||
withSilencedDeprecations("discourse.post-menu-widget-overrides", () => {
|
||||
api.replacePostMenuButton("bookmark", {
|
||||
name: "bookmark-menu-shim",
|
||||
shouldRender: () => true,
|
||||
buildAttrs: (widget) => {
|
||||
return {
|
||||
post: widget.findAncestorModel(),
|
||||
bookmarkManager: new PostBookmarkManager(
|
||||
container,
|
||||
widget.findAncestorModel()
|
||||
),
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,16 +6,73 @@ function ensureArray(val) {
|
||||
}
|
||||
|
||||
export default class DAG {
|
||||
/**
|
||||
* Creates a new DAG instance from an iterable of entries.
|
||||
*
|
||||
* @param {Iterable<[string, any, Object?]>} entriesLike - An iterable of key-value-position tuples to initialize the DAG.
|
||||
* @param {Object} [opts] - Optional configuration object.
|
||||
* @param {Object} [opts.defaultPosition] - Default positioning rules for new items.
|
||||
* @param {boolean} [opts.throwErrorOnCycle=true] - Flag indicating whether to throw an error when a cycle is detected. Default is true. When false, the default position will be used instead.
|
||||
* @param {string|string[]} [opts.defaultPosition.before] - A key or array of keys of items which should appear before this one.
|
||||
* @param {string|string[]} [opts.defaultPosition.after] - A key or array of keys of items which should appear after this one.
|
||||
* @param {(key: string, value: any, position: {before?: string|string[], after?: string|string[]}) => void} [opts.onAddItem] - Callback function to be called when an item is added.
|
||||
* @param {(key: string) => void} [opts.onDeleteItem] - Callback function to be called when an item is removed.
|
||||
* @param {(key: string, newValue: any, oldValue: any, newPosition: {before?: string|string[], after?: string|string[]}, oldPosition: {before?: string|string[], after?: string|string[]}) => void} [opts.onReplaceItem] - Callback function to be called when an item is replaced.
|
||||
* @param {(key: string, newPosition: {before?: string|string[], after?: string|string[]}, oldPosition: {before?: string|string[], after?: string|string[]}) => void} [opts.onRepositionItem] - Callback function to be called when an item is repositioned.
|
||||
* @returns {DAG} A new DAG instance.
|
||||
*/
|
||||
static from(entriesLike, opts) {
|
||||
const dag = new this(opts);
|
||||
|
||||
for (const [key, value, position] of entriesLike) {
|
||||
dag.add(key, value, position);
|
||||
}
|
||||
|
||||
return dag;
|
||||
}
|
||||
|
||||
#defaultPosition;
|
||||
#onAddItem;
|
||||
#onDeleteItem;
|
||||
#onReplaceItem;
|
||||
#onRepositionItem;
|
||||
#throwErrorOnCycle;
|
||||
|
||||
#rawData = new Map();
|
||||
#dag = new DAGMap();
|
||||
|
||||
constructor(args) {
|
||||
/**
|
||||
* Creates a new Directed Acyclic Graph (DAG) instance.
|
||||
*
|
||||
* @param {Object} [opts] - Optional configuration object.
|
||||
* @param {Object} [opts.defaultPosition] - Default positioning rules for new items.
|
||||
* @param {boolean} [opts.throwErrorOnCycle=true] - Flag indicating whether to throw an error when a cycle is detected. When false, the default position will be used instead.
|
||||
* @param {string|string[]} [opts.defaultPosition.before] - A key or array of keys of items which should appear before this one.
|
||||
* @param {string|string[]} [opts.defaultPosition.after] - A key or array of keys of items which should appear after this one.
|
||||
* @param {(key: string, value: any, position: {before?: string|string[], after?: string|string[]}) => void} [opts.onAddItem] - Callback function to be called when an item is added.
|
||||
* @param {(key: string) => void} [opts.onDeleteItem] - Callback function to be called when an item is removed.
|
||||
* @param {(key: string, newValue: any, oldValue: any, newPosition: {before?: string|string[], after?: string|string[]}, oldPosition: {before?: string|string[], after?: string|string[]}) => void} [opts.onReplaceItem] - Callback function to be called when an item is replaced.
|
||||
* @param {(key: string, newPosition: {before?: string|string[], after?: string|string[]}, oldPosition: {before?: string|string[], after?: string|string[]}) => void} [opts.onRepositionItem] - Callback function to be called when an item is repositioned.
|
||||
*/
|
||||
constructor(opts) {
|
||||
// allows for custom default positioning of new items added to the DAG, eg
|
||||
// new DAG({ defaultPosition: { before: "foo", after: "bar" } });
|
||||
this.#defaultPosition = args?.defaultPosition || {};
|
||||
this.#defaultPosition = opts?.defaultPosition || {};
|
||||
this.#throwErrorOnCycle = opts?.throwErrorOnCycle ?? true;
|
||||
|
||||
this.#onAddItem = opts?.onAddItem;
|
||||
this.#onDeleteItem = opts?.onDeleteItem;
|
||||
this.#onReplaceItem = opts?.onReplaceItem;
|
||||
this.#onRepositionItem = opts?.onRepositionItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default position for a given key, excluding the key itself from the before/after arrays.
|
||||
*
|
||||
* @param {string} key - The key to get the default position for.
|
||||
* @returns {Object} The default position object.
|
||||
* @private
|
||||
*/
|
||||
#defaultPositionForKey(key) {
|
||||
const pos = { ...this.#defaultPosition };
|
||||
if (ensureArray(pos.before).includes(key)) {
|
||||
@@ -28,15 +85,20 @@ export default class DAG {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a key/value pair to the map. Can optionally specify before/after position requirements.
|
||||
* Adds a key/value pair to the DAG map. Can optionally specify before/after position requirements.
|
||||
*
|
||||
* @param {string} key The key of the item to be added. Can be referenced by other member's position parameters.
|
||||
* @param {any} value
|
||||
* @param {Object} position
|
||||
* @param {string | string[]} position.before A key or array of keys of items which should appear before this one.
|
||||
* @param {string | string[]} position.after A key or array of keys of items which should appear after this one.
|
||||
* @param {string} key - The key of the item to be added. Can be referenced by other member's position parameters.
|
||||
* @param {any} value - The value of the item to be added.
|
||||
* @param {Object} [position] - The position object specifying before/after requirements.
|
||||
* @param {string|string[]} [position.before] - A key or array of keys of items which should appear before this one.
|
||||
* @param {string|string[]} [position.after] - A key or array of keys of items which should appear after this one.
|
||||
* @returns {boolean} True if the item was added, false if the key already exists.
|
||||
*/
|
||||
add(key, value, position) {
|
||||
if (this.has(key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
position ||= this.#defaultPositionForKey(key);
|
||||
const { before, after } = position;
|
||||
this.#rawData.set(key, {
|
||||
@@ -44,49 +106,88 @@ export default class DAG {
|
||||
before,
|
||||
after,
|
||||
});
|
||||
this.#dag.add(key, value, before, after);
|
||||
|
||||
this.#addHandlingCycles(this.#dag, key, value, before, after);
|
||||
this.#onAddItem?.(key, value, position);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item from the map by key. no-op if the key does not exist.
|
||||
* Remove an item from the map by key.
|
||||
*
|
||||
* @param {string} key The key of the item to be removed.
|
||||
* @param {string} key - The key of the item to be removed.
|
||||
* @returns {boolean} True if the item was deleted, false otherwise.
|
||||
*/
|
||||
delete(key) {
|
||||
this.#rawData.delete(key);
|
||||
const deleted = this.#rawData.delete(key);
|
||||
this.#refreshDAG();
|
||||
|
||||
if (deleted) {
|
||||
this.#onDeleteItem?.(key);
|
||||
}
|
||||
|
||||
return deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the positioning rules of an existing item in the map. Will replace all existing rules. No-op if the key does not exist.
|
||||
* Replace an existing item in the map.
|
||||
*
|
||||
* @param {string} key
|
||||
* @param {string | string[]} position.before A key or array of keys of items which should appear before this one.
|
||||
* @param {string | string[]} position.after A key or array of keys of items which should appear after this one.
|
||||
* @param {string} key - The key of the item to be replaced.
|
||||
* @param {any} value - The new value of the item.
|
||||
* @param {Object} position - The new position object specifying before/after requirements.
|
||||
* @param {string|string[]} [position.before] - A key or array of keys of items which should appear before this one.
|
||||
* @param {string|string[]} [position.after] - A key or array of keys of items which should appear after this one.
|
||||
* @returns {boolean} True if the item was replaced, false otherwise.
|
||||
*/
|
||||
reposition(key, { before, after }) {
|
||||
const node = this.#rawData.get(key);
|
||||
if (node) {
|
||||
node.before = before;
|
||||
node.after = after;
|
||||
replace(key, value, position) {
|
||||
return this.#replace(key, value, position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the positioning rules of an existing item in the map. Will replace all existing rules.
|
||||
*
|
||||
* @param {string} key - The key of the item to reposition.
|
||||
* @param {Object} position - The new position object specifying before/after requirements.
|
||||
* @param {string|string[]} [position.before] - A key or array of keys of items which should appear before this one.
|
||||
* @param {string|string[]} [position.after] - A key or array of keys of items which should appear after this one.
|
||||
* @returns {boolean} True if the item was repositioned, false otherwise.
|
||||
*/
|
||||
reposition(key, position) {
|
||||
if (!this.has(key) || !position) {
|
||||
return false;
|
||||
}
|
||||
this.#refreshDAG();
|
||||
|
||||
const { value } = this.#rawData.get(key);
|
||||
|
||||
return this.#replace(key, value, position, { repositionOnly: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an item exists in the map.
|
||||
* @param {string} key
|
||||
* @returns {boolean}
|
||||
*
|
||||
* @param {string} key - The key to check for existence.
|
||||
* @returns {boolean} True if the item exists, false otherwise.
|
||||
*/
|
||||
has(key) {
|
||||
return this.#rawData.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the resolved key/value pairs in the map. The order of the pairs is determined by the before/after rules.
|
||||
* @returns {Array<[key: string, value: any]}>} An array of key/value pairs.
|
||||
* Returns an array of entries in the DAG. Each entry is a tuple containing the key, value, and position object.
|
||||
*
|
||||
* @returns {Array<[string, any, {before?: string|string[], after?: string|string[]}]>} An array of key-value-position tuples.
|
||||
*/
|
||||
entries() {
|
||||
return Array.from(this.#rawData.entries()).map(
|
||||
([key, { value, before, after }]) => [key, value, { before, after }]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the resolved key/value pairs in the map. The order of the pairs is determined by the before/after rules.
|
||||
*
|
||||
* @returns {Array<{key: string, value: any, position: {before?: string|string[], after?: string|string[]}}>} An array of key/value/position objects.
|
||||
*/
|
||||
@bind
|
||||
resolve() {
|
||||
@@ -96,20 +197,100 @@ export default class DAG {
|
||||
// dependencies, for example if an item has a "before: search" dependency, the "search" vertex will be included
|
||||
// even if it was explicitly excluded from the raw data.
|
||||
if (this.has(key)) {
|
||||
result.push({ key, value });
|
||||
const { before, after } = this.#rawData.get(key);
|
||||
result.push({ key, value, position: { before, after } });
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* DAGMap doesn't support removing or modifying keys, so we
|
||||
* need to completely recreate it from the raw data
|
||||
* Adds a key/value pair to the DAG map while handling potential cycles.
|
||||
*
|
||||
* @param {DAGMap} dag - The DAG map instance to add the key/value pair to.
|
||||
* @param {string} key - The key of the item to be added.
|
||||
* @param {any} value - The value of the item to be added.
|
||||
* @param {string|string[]} [before] - A key or array of keys of items which should appear before this one.
|
||||
* @param {string|string[]} [after] - A key or array of keys of items which should appear after this one.
|
||||
* @throws {Error} Throws an error if a cycle is detected and `throwErrorOnCycle` is true.
|
||||
* @private
|
||||
*/
|
||||
#addHandlingCycles(dag, key, value, before, after) {
|
||||
if (this.#throwErrorOnCycle) {
|
||||
dag.add(key, value, before, after);
|
||||
} else {
|
||||
try {
|
||||
dag.add(key, value, before, after);
|
||||
} catch (e) {
|
||||
if (e.message.match(/cycle/i)) {
|
||||
const { before: newBefore, after: newAfter } =
|
||||
this.#defaultPositionForKey(key);
|
||||
|
||||
// if even the default position causes a cycle, an error will be thrown
|
||||
dag.add(key, value, newBefore, newAfter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace an existing item in the map.
|
||||
*
|
||||
* @param {string} key - The key of the item to be replaced.
|
||||
* @param {any} value - The new value of the item.
|
||||
* @param {Object} position - The new position object specifying before/after requirements.
|
||||
* @param {string|string[]} [position.before] - A key or array of keys of items which should appear before this one.
|
||||
* @param {string|string[]} [position.after] - A key or array of keys of items which should appear after this one.
|
||||
* @param {Object} [options] - Additional options.
|
||||
* @param {boolean} [options.repositionOnly=false] - Whether the replacement is for repositioning only.
|
||||
* @returns {boolean} True if the item was replaced, false otherwise.
|
||||
* @private
|
||||
*/
|
||||
#replace(
|
||||
key,
|
||||
value,
|
||||
position,
|
||||
{ repositionOnly } = { repositionOnly: false }
|
||||
) {
|
||||
if (!this.has(key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const existingItem = this.#rawData.get(key);
|
||||
const oldValue = existingItem.value;
|
||||
const oldPosition = {
|
||||
before: existingItem.before,
|
||||
after: existingItem.after,
|
||||
};
|
||||
|
||||
// mutating the existing item keeps the position in the map in case before/after weren't explicitly set
|
||||
existingItem.value = value;
|
||||
|
||||
if (position) {
|
||||
existingItem.before = position.before;
|
||||
existingItem.after = position.after;
|
||||
}
|
||||
|
||||
this.#refreshDAG();
|
||||
|
||||
if (repositionOnly) {
|
||||
this.#onRepositionItem?.(key, position, oldPosition);
|
||||
} else {
|
||||
this.#onReplaceItem?.(key, value, oldValue, position, oldPosition);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the DAG by recreating it from the raw data.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
#refreshDAG() {
|
||||
const newDAG = new DAGMap();
|
||||
for (const [key, { value, before, after }] of this.#rawData) {
|
||||
newDAG.add(key, value, before, after);
|
||||
this.#addHandlingCycles(newDAG, key, value, before, after);
|
||||
}
|
||||
this.#dag = newDAG;
|
||||
}
|
||||
|
||||
@@ -164,6 +164,12 @@ import { addImageWrapperButton } from "discourse-markdown-it/features/image-cont
|
||||
import { CUSTOM_USER_SEARCH_OPTIONS } from "select-kit/components/user-chooser";
|
||||
import { modifySelectKit } from "select-kit/mixins/plugin-api";
|
||||
|
||||
const DEPRECATED_POST_MENU_WIDGETS = [
|
||||
"post-menu",
|
||||
"post-user-tip-shim",
|
||||
"small-user-list",
|
||||
];
|
||||
|
||||
const appliedModificationIds = new WeakMap();
|
||||
|
||||
// This helper prevents us from applying the same `modifyClass` over and over in test mode.
|
||||
@@ -418,9 +424,10 @@ class PluginApi {
|
||||
* behavior. Notice that this includes the default behavior and if next() is not called in your transformer's callback
|
||||
* the default behavior will be completely overridden
|
||||
* @param {*} [behaviorCallback.context] the optional context in which the behavior is being transformed
|
||||
* @returns {boolean} True if the transformer exists, false otherwise.
|
||||
*/
|
||||
registerBehaviorTransformer(transformerName, behaviorCallback) {
|
||||
_registerTransformer(
|
||||
return _registerTransformer(
|
||||
transformerName,
|
||||
transformerTypes.BEHAVIOR,
|
||||
behaviorCallback
|
||||
@@ -485,9 +492,10 @@ class PluginApi {
|
||||
* mutating the input value, return the same output for the same input and not have any side effects.
|
||||
* @param {*} valueCallback.value the value to be transformed
|
||||
* @param {*} [valueCallback.context] the optional context in which the value is being transformed
|
||||
* @returns {boolean} True if the transformer exists, false otherwise.
|
||||
*/
|
||||
registerValueTransformer(transformerName, valueCallback) {
|
||||
_registerTransformer(
|
||||
return _registerTransformer(
|
||||
transformerName,
|
||||
transformerTypes.VALUE,
|
||||
valueCallback
|
||||
@@ -832,6 +840,14 @@ class PluginApi {
|
||||
* }
|
||||
**/
|
||||
addPostMenuButton(name, callback) {
|
||||
deprecated(
|
||||
"`api.addPostMenuButton` has been deprecated. Use the value transformer `post-menu-buttons` instead.",
|
||||
{
|
||||
since: "v3.4.0.beta3-dev",
|
||||
id: "discourse.post-menu-widget-overrides",
|
||||
}
|
||||
);
|
||||
|
||||
apiExtraButtons[name] = callback;
|
||||
addButton(name, callback);
|
||||
}
|
||||
@@ -902,6 +918,14 @@ class PluginApi {
|
||||
* ```
|
||||
**/
|
||||
removePostMenuButton(name, callback) {
|
||||
deprecated(
|
||||
"`api.removePostMenuButton` has been deprecated. Use the value transformer `post-menu-buttons` instead.",
|
||||
{
|
||||
since: "v3.4.0.beta3-dev",
|
||||
id: "discourse.post-menu-widget-overrides",
|
||||
}
|
||||
);
|
||||
|
||||
removeButton(name, callback);
|
||||
}
|
||||
|
||||
@@ -922,6 +946,14 @@ class PluginApi {
|
||||
* });
|
||||
**/
|
||||
replacePostMenuButton(name, widget) {
|
||||
deprecated(
|
||||
"`api.replacePostMenuButton` has been deprecated. Use the value transformer `post-menu-buttons` instead.",
|
||||
{
|
||||
since: "v3.4.0.beta3-dev",
|
||||
id: "discourse.post-menu-widget-overrides",
|
||||
}
|
||||
);
|
||||
|
||||
replaceButton(name, widget);
|
||||
}
|
||||
|
||||
@@ -3345,6 +3377,16 @@ class PluginApi {
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
|
||||
if (DEPRECATED_POST_MENU_WIDGETS.includes(widgetName)) {
|
||||
deprecated(
|
||||
`The ${widgetName} widget has been deprecated and ${override} is no longer a supported override.`,
|
||||
{
|
||||
since: "v3.4.0.beta3-dev",
|
||||
id: "discourse.post-menu-widget-overrides",
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,9 +35,10 @@ export function recentlyCopied(postId, actionClass) {
|
||||
);
|
||||
}
|
||||
|
||||
export function showAlert(postId, actionClass, messageKey) {
|
||||
export function showAlert(postId, actionClass, messageKey, opts = {}) {
|
||||
const postSelector = `article[data-post-id='${postId}']`;
|
||||
const actionBtn = document.querySelector(`${postSelector} .${actionClass}`);
|
||||
const actionBtn =
|
||||
opts.actionBtn || document.querySelector(`${postSelector} .${actionClass}`);
|
||||
|
||||
actionBtn?.classList.add("post-action-feedback-button");
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export function transformBasicPost(post) {
|
||||
deleted: post.get("deleted"),
|
||||
deleted_at: post.deleted_at,
|
||||
user_deleted: post.user_deleted,
|
||||
isDeleted: post.deleted_at || post.user_deleted,
|
||||
isDeleted: post.deleted,
|
||||
deletedByAvatarTemplate: null,
|
||||
deletedByUsername: null,
|
||||
primary_group_name: post.primary_group_name,
|
||||
@@ -124,27 +124,32 @@ export default function transformPost(
|
||||
postAtts.post_type = postType;
|
||||
postAtts.via_email = post.via_email;
|
||||
postAtts.isAutoGenerated = post.is_auto_generated;
|
||||
postAtts.isModeratorAction = postType === postTypes.moderator_action;
|
||||
postAtts.isWhisper = postType === postTypes.whisper;
|
||||
postAtts.isModeratorAction = post.isModeratorAction;
|
||||
postAtts.isWhisper = post.isWhisper;
|
||||
postAtts.isSmallAction =
|
||||
postType === postTypes.small_action || post.action_code === "split_topic";
|
||||
postAtts.canBookmark = !!currentUser;
|
||||
postAtts.canManage = currentUser && currentUser.get("canManageTopic");
|
||||
postAtts.canManage = post.canManage;
|
||||
postAtts.canViewRawEmail = currentUser && currentUser.can_view_raw_email;
|
||||
postAtts.canArchiveTopic = !!details.can_archive_topic;
|
||||
postAtts.canCloseTopic = !!details.can_close_topic;
|
||||
postAtts.canSplitMergeTopic = !!details.can_split_merge_topic;
|
||||
postAtts.canEditStaffNotes = !!details.can_edit_staff_notes;
|
||||
postAtts.canEditStaffNotes = post.canEditStaffNotes;
|
||||
postAtts.canReplyAsNewTopic = !!details.can_reply_as_new_topic;
|
||||
postAtts.canReviewTopic = !!details.can_review_topic;
|
||||
postAtts.canPublishPage =
|
||||
!!details.can_publish_page && post.post_number === 1;
|
||||
postAtts.canPublishPage = post.canPublishPage;
|
||||
postAtts.isWarning = topic.is_warning;
|
||||
postAtts.links = post.get("internalLinks");
|
||||
|
||||
// on the glimmer post menu this logic was implemented on the PostMenu component as it depends on
|
||||
// two post object instances
|
||||
// if you change the logic replyDirectlyBelow here, while the widget post menu is still around, please
|
||||
// be sure to also update the logic on the Glimmer PostMenu component
|
||||
postAtts.replyDirectlyBelow =
|
||||
nextPost &&
|
||||
nextPost.reply_to_post_number === post.post_number &&
|
||||
post.post_number !== filteredRepliesPostNumber;
|
||||
|
||||
postAtts.replyDirectlyAbove =
|
||||
prevPost &&
|
||||
post.id !== filteredUpwardsPostID &&
|
||||
@@ -205,9 +210,10 @@ export default function transformPost(
|
||||
});
|
||||
}
|
||||
|
||||
postAtts.liked = post.liked;
|
||||
|
||||
const likeAction = post.likeAction;
|
||||
if (likeAction) {
|
||||
postAtts.liked = likeAction.acted;
|
||||
postAtts.canToggleLike = likeAction.get("canToggle");
|
||||
postAtts.showLike = postAtts.liked || postAtts.canToggleLike;
|
||||
postAtts.likeCount = likeAction.count;
|
||||
@@ -218,12 +224,14 @@ export default function transformPost(
|
||||
postAtts.showLike = true;
|
||||
}
|
||||
|
||||
postAtts.canDelete = post.canDelete;
|
||||
postAtts.canDeleteTopic = post.canDeleteTopic;
|
||||
postAtts.canPermanentlyDelete = post.canPermanentlyDelete;
|
||||
postAtts.canRecover = post.canRecover;
|
||||
postAtts.canRecoverTopic = post.canRecoverTopic;
|
||||
|
||||
if (postAtts.post_number === 1) {
|
||||
postAtts.canRecoverTopic = postAtts.isDeleted && details.can_recover;
|
||||
postAtts.canDeleteTopic = !postAtts.isDeleted && details.can_delete;
|
||||
postAtts.expandablePost = topic.expandable_first_post;
|
||||
postAtts.canPermanentlyDelete =
|
||||
postAtts.isDeleted && details.can_permanently_delete;
|
||||
|
||||
// Show a "Flag to delete" message if not staff and you can't
|
||||
// otherwise delete it.
|
||||
@@ -234,14 +242,11 @@ export default function transformPost(
|
||||
currentUser &&
|
||||
!currentUser.staff;
|
||||
} else {
|
||||
postAtts.canRecover = postAtts.isDeleted && postAtts.canRecover;
|
||||
postAtts.canDelete =
|
||||
postAtts.canDelete &&
|
||||
!post.deleted_at &&
|
||||
currentUser &&
|
||||
(currentUser.staff || !post.user_deleted);
|
||||
postAtts.canPermanentlyDelete =
|
||||
postAtts.isDeleted && post.can_permanently_delete;
|
||||
}
|
||||
|
||||
_additionalAttributes.forEach((a) => (postAtts[a] = post[a]));
|
||||
|
||||
@@ -152,6 +152,7 @@ export function _addTransformerName(name, transformerType) {
|
||||
* @param {string} transformerName the name of the transformer
|
||||
* @param {string} transformerType the type of the transformer being registered
|
||||
* @param {function} callback callback that will transform the value.
|
||||
* @returns {boolean} True if the transformer exists, false otherwise.
|
||||
*/
|
||||
export function _registerTransformer(
|
||||
transformerName,
|
||||
@@ -183,6 +184,8 @@ export function _registerTransformer(
|
||||
`${prefix}: transformer "${transformerName}" is unknown and will be ignored. ` +
|
||||
"Is the name correct? Are you using the correct API for the transformer type?"
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof callback !== "function") {
|
||||
@@ -197,6 +200,8 @@ export function _registerTransformer(
|
||||
existingTransformers.push(callback);
|
||||
|
||||
transformersRegistry.set(normalizedTransformerName, existingTransformers);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function applyBehaviorTransformer(
|
||||
|
||||
@@ -12,4 +12,5 @@ export const VALUE_TRANSFORMERS = Object.freeze([
|
||||
"home-logo-image-url",
|
||||
"mentions-class",
|
||||
"more-topics-tabs",
|
||||
"post-menu-buttons",
|
||||
]);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import EmberObject, { get } from "@ember/object";
|
||||
import { and, equal, not, or } from "@ember/object/computed";
|
||||
import { alias, and, equal, not, or } from "@ember/object/computed";
|
||||
import { service } from "@ember/service";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { Promise } from "rsvp";
|
||||
@@ -104,14 +105,29 @@ export default class Post extends RestModel {
|
||||
}
|
||||
|
||||
@service currentUser;
|
||||
@service site;
|
||||
|
||||
@tracked bookmarked;
|
||||
@tracked can_delete;
|
||||
@tracked can_edit;
|
||||
@tracked can_permanently_delete;
|
||||
@tracked can_recover;
|
||||
@tracked deleted_at;
|
||||
@tracked likeAction;
|
||||
@tracked post_type;
|
||||
@tracked user_deleted;
|
||||
@tracked user_id;
|
||||
@tracked yours;
|
||||
|
||||
customShare = null;
|
||||
|
||||
@alias("can_edit") canEdit; // for compatibility with existing code
|
||||
@equal("trust_level", 0) new_user;
|
||||
@equal("post_number", 1) firstPost;
|
||||
@or("deleted_at", "deletedViaTopic") deleted;
|
||||
@not("deleted") notDeleted;
|
||||
@propertyEqual("topic.details.created_by.id", "user_id") topicOwner;
|
||||
@alias("topic.details.created_by.id") topicCreatedById;
|
||||
|
||||
// Posts can show up as deleted if the topic is deleted
|
||||
@and("firstPost", "topic.deleted_at") deletedViaTopic;
|
||||
@@ -200,6 +216,112 @@ export default class Post extends RestModel {
|
||||
return fancyTitle(title, this.siteSettings.support_mixed_text_direction);
|
||||
}
|
||||
|
||||
get canBookmark() {
|
||||
return !!this.currentUser;
|
||||
}
|
||||
|
||||
get canDelete() {
|
||||
return (
|
||||
this.can_delete &&
|
||||
!this.deleted_at &&
|
||||
this.currentUser &&
|
||||
(this.currentUser.staff || !this.user_deleted)
|
||||
);
|
||||
}
|
||||
|
||||
get canDeleteTopic() {
|
||||
return this.firstPost && !this.deleted && this.topic.details.can_delete;
|
||||
}
|
||||
|
||||
get canEditStaffNotes() {
|
||||
return !!this.topic.details.can_edit_staff_notes;
|
||||
}
|
||||
|
||||
get canFlag() {
|
||||
return !this.topic.deleted && !isEmpty(this.flagsAvailable);
|
||||
}
|
||||
|
||||
get canManage() {
|
||||
return this.currentUser?.canManageTopic;
|
||||
}
|
||||
|
||||
get canPermanentlyDelete() {
|
||||
return (
|
||||
this.deleted &&
|
||||
(this.firstPost
|
||||
? this.topic.details.can_permanently_delete
|
||||
: this.can_permanently_delete)
|
||||
);
|
||||
}
|
||||
|
||||
get canPublishPage() {
|
||||
return this.firstPost && !!this.topic.details.can_publish_page;
|
||||
}
|
||||
|
||||
get canRecover() {
|
||||
return this.deleted && this.can_recover;
|
||||
}
|
||||
|
||||
get isRecovering() {
|
||||
return !this.deleted && this.can_recover;
|
||||
}
|
||||
|
||||
get canRecoverTopic() {
|
||||
return this.firstPost && this.deleted && this.topic.details.can_recover;
|
||||
}
|
||||
|
||||
get isRecoveringTopic() {
|
||||
return this.firstPost && !this.deleted && this.topic.details.can_recover;
|
||||
}
|
||||
|
||||
get canToggleLike() {
|
||||
return !!this.likeAction?.get("canToggle");
|
||||
}
|
||||
|
||||
get filteredRepliesPostNumber() {
|
||||
return this.topic.get("postStream.filterRepliesToPostNumber");
|
||||
}
|
||||
|
||||
get isWhisper() {
|
||||
return this.post_type === this.site.post_types.whisper;
|
||||
}
|
||||
|
||||
get isModeratorAction() {
|
||||
return this.post_type === this.site.post_types.moderator_action;
|
||||
}
|
||||
|
||||
get liked() {
|
||||
return !!this.likeAction?.get("acted");
|
||||
}
|
||||
|
||||
get likeCount() {
|
||||
return this.likeAction?.get("count");
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a "Flag to delete" message if not staff and you can't otherwise delete it.
|
||||
*/
|
||||
get showFlagDelete() {
|
||||
return (
|
||||
!this.canDelete &&
|
||||
this.yours &&
|
||||
this.canFlag &&
|
||||
this.currentUser &&
|
||||
!this.currentUser.staff
|
||||
);
|
||||
}
|
||||
|
||||
get showLike() {
|
||||
if (
|
||||
!this.currentUser ||
|
||||
(this.topic?.get("archived") && this.user_id !== this.currentUser.id)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.likeAction && (this.liked || this.canToggleLike);
|
||||
}
|
||||
|
||||
afterUpdate(res) {
|
||||
if (res.category) {
|
||||
this.site.updateCategory(res.category);
|
||||
@@ -277,9 +399,9 @@ export default class Post extends RestModel {
|
||||
}
|
||||
|
||||
/**
|
||||
Changes the state of the post to be deleted. Does not call the server, that should be
|
||||
done elsewhere.
|
||||
**/
|
||||
Changes the state of the post to be deleted. Does not call the server, that should be
|
||||
done elsewhere.
|
||||
**/
|
||||
setDeletedState(deletedBy) {
|
||||
let promise;
|
||||
this.set("oldCooked", this.cooked);
|
||||
@@ -316,10 +438,10 @@ export default class Post extends RestModel {
|
||||
}
|
||||
|
||||
/**
|
||||
Changes the state of the post to NOT be deleted. Does not call the server.
|
||||
This can only be called after setDeletedState was called, but the delete
|
||||
failed on the server.
|
||||
**/
|
||||
Changes the state of the post to NOT be deleted. Does not call the server.
|
||||
This can only be called after setDeletedState was called, but the delete
|
||||
failed on the server.
|
||||
**/
|
||||
undoDeleteState() {
|
||||
if (this.oldCooked) {
|
||||
this.setProperties({
|
||||
@@ -344,9 +466,9 @@ export default class Post extends RestModel {
|
||||
}
|
||||
|
||||
/**
|
||||
Updates a post from another's attributes. This will normally happen when a post is loading but
|
||||
is already found in an identity map.
|
||||
**/
|
||||
Updates a post from another's attributes. This will normally happen when a post is loading but
|
||||
is already found in an identity map.
|
||||
**/
|
||||
updateFromPost(otherPost) {
|
||||
Object.keys(otherPost).forEach((key) => {
|
||||
let value = otherPost[key],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import EmberObject from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
@@ -11,6 +12,13 @@ import RestModel from "discourse/models/rest";
|
||||
export default class TopicDetails extends RestModel {
|
||||
@service store;
|
||||
|
||||
@tracked can_delete;
|
||||
@tracked can_edit_staff_notes;
|
||||
@tracked can_permanently_delete;
|
||||
@tracked can_publish_page;
|
||||
@tracked created_by;
|
||||
@tracked notification_level;
|
||||
|
||||
loaded = false;
|
||||
|
||||
updateFromJson(details) {
|
||||
|
||||
@@ -1,40 +1,23 @@
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
import RenderGlimmer, {
|
||||
registerWidgetShim,
|
||||
} from "discourse/widgets/render-glimmer";
|
||||
import { createWidget } from "discourse/widgets/widget";
|
||||
import { registerWidgetShim } from "discourse/widgets/render-glimmer";
|
||||
|
||||
createWidget("small-user-list", {
|
||||
tagName: "div.clearfix.small-user-list",
|
||||
|
||||
buildClasses(atts) {
|
||||
return atts.listClassName;
|
||||
},
|
||||
|
||||
buildAttributes(attrs) {
|
||||
const attributes = { role: "list" };
|
||||
if (attrs.ariaLabel) {
|
||||
attributes["aria-label"] = attrs.ariaLabel;
|
||||
}
|
||||
return attributes;
|
||||
},
|
||||
|
||||
html(attrs) {
|
||||
return [
|
||||
new RenderGlimmer(
|
||||
this,
|
||||
"span.small-user-list-content",
|
||||
hbs`<SmallUserList @data={{@data.attrs}}/>`,
|
||||
{
|
||||
attrs,
|
||||
}
|
||||
),
|
||||
];
|
||||
},
|
||||
});
|
||||
// This shim is nesting everything into a DIV and changing the HTML but only thw two voting plugins
|
||||
// are using this widget outside of core.
|
||||
registerWidgetShim(
|
||||
"small-user-list",
|
||||
"div",
|
||||
hbs`
|
||||
<SmallUserList class={{@data.listClassName}}
|
||||
@ariaLabel={{@data.ariaLabel}}
|
||||
@users={{@data.users}}
|
||||
@addSelf={{@data.addSelf}}
|
||||
@count={{@data.count}}
|
||||
@description={{@data.description}}/>`
|
||||
);
|
||||
|
||||
registerWidgetShim(
|
||||
"actions-summary",
|
||||
"section.post-actions",
|
||||
hbs`<ActionsSummary @data={{@data}} /> `
|
||||
hbs`
|
||||
<ActionsSummary @data={{@data}} /> `
|
||||
);
|
||||
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
NO_REMINDER_ICON,
|
||||
WITH_REMINDER_ICON,
|
||||
} from "discourse/models/bookmark";
|
||||
import RenderGlimmer from "discourse/widgets/render-glimmer";
|
||||
import RenderGlimmer, {
|
||||
registerWidgetShim,
|
||||
} from "discourse/widgets/render-glimmer";
|
||||
import { applyDecorators, createWidget } from "discourse/widgets/widget";
|
||||
import discourseLater from "discourse-common/lib/later";
|
||||
import I18n from "discourse-i18n";
|
||||
@@ -944,3 +946,40 @@ export default createWidget("post-menu", {
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// TODO (glimmer-post-menu): Once this widget is removed the `<section>...</section>` tag needs to be added to the PostMenu component
|
||||
registerWidgetShim(
|
||||
"glimmer-post-menu",
|
||||
"section.post-menu-area.clearfix",
|
||||
hbs`
|
||||
<Post::Menu
|
||||
@canCreatePost={{@data.canCreatePost}}
|
||||
@filteredRepliesView={{@data.filteredRepliesView}}
|
||||
@nextPost={{@data.nextPost}}
|
||||
@post={{@data.post}}
|
||||
@prevPost={{@data.prevPost}}
|
||||
@repliesShown={{@data.repliesShown}}
|
||||
@showReadIndicator={{@data.showReadIndicator}}
|
||||
@changeNotice={{@data.changeNotice}}
|
||||
@changePostOwner={{@data.changePostOwner}}
|
||||
@copyLink={{@data.copyLink}}
|
||||
@deletePost={{@data.deletePost}}
|
||||
@editPost={{@data.editPost}}
|
||||
@grantBadge={{@data.grantBadge}}
|
||||
@lockPost={{@data.lockPost}}
|
||||
@permanentlyDeletePost={{@data.permanentlyDeletePost}}
|
||||
@rebakePost={{@data.rebakePost}}
|
||||
@recoverPost={{@data.recoverPost}}
|
||||
@replyToPost={{@data.replyToPost}}
|
||||
@share={{@data.share}}
|
||||
@showFlags={{@data.showFlags}}
|
||||
@showLogin={{@data.showLogin}}
|
||||
@showPagePublish={{@data.showPagePublish}}
|
||||
@toggleLike={{@data.toggleLike}}
|
||||
@togglePostType={{@data.togglePostType}}
|
||||
@toggleReplies={{@data.toggleReplies}}
|
||||
@toggleWiki={{@data.toggleWiki}}
|
||||
@unhidePost={{@data.unhidePost}}
|
||||
@unlockPost={{@data.unlockPost}}
|
||||
/>`
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ import { iconNode } from "discourse-common/lib/icon-library";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
let transformCallbacks = null;
|
||||
|
||||
export function postTransformCallbacks(transformed) {
|
||||
if (transformCallbacks === null) {
|
||||
return;
|
||||
@@ -22,6 +23,7 @@ export function postTransformCallbacks(transformed) {
|
||||
transformCallbacks[i].call(this, transformed);
|
||||
}
|
||||
}
|
||||
|
||||
export function addPostTransformCallback(callback) {
|
||||
transformCallbacks = transformCallbacks || [];
|
||||
transformCallbacks.push(callback);
|
||||
@@ -223,6 +225,8 @@ export default createWidget("post-stream", {
|
||||
nextPost
|
||||
);
|
||||
transformed.canCreatePost = attrs.canCreatePost;
|
||||
transformed.prevPost = prevPost;
|
||||
transformed.nextPost = nextPost;
|
||||
transformed.mobileView = mobileView;
|
||||
|
||||
if (transformed.canManage || transformed.canSplitMergeTopic) {
|
||||
@@ -258,7 +262,8 @@ export default createWidget("post-stream", {
|
||||
new RenderGlimmer(
|
||||
this,
|
||||
"div.time-gap.small-action",
|
||||
hbs`<TimeGap @daysSince={{@data.daysSince}} />`,
|
||||
hbs`
|
||||
<TimeGap @daysSince={{@data.daysSince}} />`,
|
||||
{ daysSince }
|
||||
)
|
||||
);
|
||||
@@ -277,6 +282,9 @@ export default createWidget("post-stream", {
|
||||
);
|
||||
} else {
|
||||
transformed.showReadIndicator = attrs.showReadIndicator;
|
||||
// The following properties will have to be untangled from the transformed model when
|
||||
// converting this widget to a Glimmer component:
|
||||
// canCreatePost, showReadIndicator, prevPost, nextPost
|
||||
result.push(this.attach("post", transformed, { model: post }));
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
prioritizeNameFallback,
|
||||
prioritizeNameInUx,
|
||||
} from "discourse/lib/settings";
|
||||
import { consolePrefix } from "discourse/lib/source-identifier";
|
||||
import { transformBasicPost } from "discourse/lib/transform-post";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import { clipboardCopy, formatUsername } from "discourse/lib/utilities";
|
||||
@@ -24,6 +25,7 @@ import RenderGlimmer from "discourse/widgets/render-glimmer";
|
||||
import { applyDecorators, createWidget } from "discourse/widgets/widget";
|
||||
import { isTesting } from "discourse-common/config/environment";
|
||||
import { avatarUrl, translateSize } from "discourse-common/lib/avatar-utils";
|
||||
import { registerDeprecationHandler } from "discourse-common/lib/deprecated";
|
||||
import getURL, {
|
||||
getAbsoluteURL,
|
||||
getURLWithCDN,
|
||||
@@ -41,6 +43,19 @@ function transformWithCallbacks(post, topicUrl, store) {
|
||||
return transformed;
|
||||
}
|
||||
|
||||
let postMenuWidgetExtensionsAdded = null;
|
||||
let postMenuConsoleWarningLogged = false;
|
||||
|
||||
registerDeprecationHandler((_, opts) => {
|
||||
if (opts?.id === "discourse.post-menu-widget-overrides") {
|
||||
if (!postMenuWidgetExtensionsAdded) {
|
||||
postMenuWidgetExtensionsAdded = new Set();
|
||||
}
|
||||
|
||||
postMenuWidgetExtensionsAdded.add(consolePrefix().slice(1, -1));
|
||||
}
|
||||
});
|
||||
|
||||
export function avatarImg(wanted, attrs) {
|
||||
const size = translateSize(wanted);
|
||||
const url = avatarUrl(attrs.template, size);
|
||||
@@ -532,7 +547,94 @@ createWidget("post-contents", {
|
||||
filteredRepliesShown: state.filteredRepliesShown,
|
||||
},
|
||||
};
|
||||
result.push(this.attach("post-menu", attrs, extraState));
|
||||
|
||||
if (
|
||||
this.siteSettings.glimmer_post_menu_mode === "enabled" ||
|
||||
((this.siteSettings.glimmer_post_menu_mode === "auto" ||
|
||||
this.currentUser?.use_auto_glimmer_post_menu) &&
|
||||
!postMenuWidgetExtensionsAdded)
|
||||
) {
|
||||
if (!postMenuConsoleWarningLogged) {
|
||||
if (postMenuWidgetExtensionsAdded) {
|
||||
postMenuConsoleWarningLogged = true;
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
[
|
||||
"Using the new 'glimmer' post menu, even though there are themes and/or plugins using deprecated APIs (glimmer_post_menu_mode = enabled).\n" +
|
||||
"The following plugins and/or themes are using deprecated APIs, their post menu customizations are broken and may cause your site to not work properly:",
|
||||
...Array.from(postMenuWidgetExtensionsAdded).sort(),
|
||||
// TODO (glimmer-post-menu): add link to meta topic here when the roadmap for the update is announced
|
||||
].join("\n- ")
|
||||
);
|
||||
} else if (this.currentUser?.use_auto_glimmer_post_menu) {
|
||||
// TODO (glimmer-post-menu): remove this else if block when removing the site setting glimmer_post_menu_groups
|
||||
postMenuConsoleWarningLogged = true;
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("✅ Using the new 'glimmer' post menu!");
|
||||
}
|
||||
}
|
||||
|
||||
const filteredRepliesView =
|
||||
this.siteSettings.enable_filtered_replies_view;
|
||||
result.push(
|
||||
this.attach("glimmer-post-menu", {
|
||||
canCreatePost: attrs.canCreatePost,
|
||||
filteredRepliesView,
|
||||
nextPost: attrs.nextPost,
|
||||
post: this.findAncestorModel(),
|
||||
prevPost: attrs.prevPost,
|
||||
repliesShown: filteredRepliesView
|
||||
? extraState.state.filteredRepliesShown
|
||||
: extraState.state.repliesShown,
|
||||
showReadIndicator: attrs.showReadIndicator,
|
||||
changeNotice: () => this.sendWidgetAction("changeNotice"), // this action comes from the post stream
|
||||
changePostOwner: () => this.sendWidgetAction("changePostOwner"), // this action comes from the post stream
|
||||
copyLink: () => this.sendWidgetAction("copyLink"),
|
||||
deletePost: () => this.sendWidgetAction("deletePost"), // this action comes from the post stream
|
||||
editPost: () => this.sendWidgetAction("editPost"), // this action comes from the post stream
|
||||
grantBadge: () => this.sendWidgetAction("grantBadge"), // this action comes from the post stream
|
||||
lockPost: () => this.sendWidgetAction("lockPost"), // this action comes from the post stream
|
||||
permanentlyDeletePost: () =>
|
||||
this.sendWidgetAction("permanentlyDeletePost"),
|
||||
rebakePost: () => this.sendWidgetAction("rebakePost"), // this action comes from the post stream
|
||||
recoverPost: () => this.sendWidgetAction("recoverPost"), // this action comes from the post stream
|
||||
replyToPost: () => this.sendWidgetAction("replyToPost"), // this action comes from the post stream
|
||||
share: () => this.sendWidgetAction("share"),
|
||||
showFlags: () => this.sendWidgetAction("showFlags"), // this action comes from the post stream
|
||||
showLogin: () => this.sendWidgetAction("showLogin"), // this action comes from application route
|
||||
showPagePublish: () => this.sendWidgetAction("showPagePublish"), // this action comes from the post stream
|
||||
toggleLike: () => this.sendWidgetAction("toggleLike"),
|
||||
togglePostType: () => this.sendWidgetAction("togglePostType"), // this action comes from the post stream
|
||||
toggleReplies: filteredRepliesView
|
||||
? () => this.sendWidgetAction("toggleFilteredRepliesView")
|
||||
: () => this.sendWidgetAction("toggleRepliesBelow"),
|
||||
toggleWiki: () => this.sendWidgetAction("toggleWiki"), // this action comes from the post stream
|
||||
unhidePost: () => this.sendWidgetAction("unhidePost"), // this action comes from the post stream
|
||||
unlockPost: () => this.sendWidgetAction("unlockPost"), // this action comes from the post stream
|
||||
})
|
||||
);
|
||||
} else {
|
||||
if (
|
||||
(this.siteSettings.glimmer_post_menu_mode !== "disabled" ||
|
||||
this.currentUser?.use_auto_glimmer_post_menu) &&
|
||||
postMenuWidgetExtensionsAdded &&
|
||||
!postMenuConsoleWarningLogged
|
||||
) {
|
||||
postMenuConsoleWarningLogged = true;
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
[
|
||||
"Using the legacy 'widget' post menu because the following plugins and/or themes are using deprecated APIs:",
|
||||
...Array.from(postMenuWidgetExtensionsAdded).sort(),
|
||||
// TODO (glimmer-post-menu): add link to meta topic here when the roadmap for the update is announced
|
||||
].join("\n- ")
|
||||
);
|
||||
}
|
||||
|
||||
result.push(this.attach("post-menu", attrs, extraState));
|
||||
}
|
||||
|
||||
const repliesBelow = state.repliesBelow;
|
||||
if (repliesBelow.length) {
|
||||
@@ -942,6 +1044,7 @@ createWidget("post-article", {
|
||||
});
|
||||
|
||||
let addPostClassesCallbacks = null;
|
||||
|
||||
export function addPostClassesCallback(callback) {
|
||||
addPostClassesCallbacks = addPostClassesCallbacks || [];
|
||||
addPostClassesCallbacks.push(callback);
|
||||
@@ -1025,15 +1128,15 @@ export default createWidget("post", {
|
||||
return [this.attach("post-article", attrs)];
|
||||
},
|
||||
|
||||
toggleLike() {
|
||||
async toggleLike() {
|
||||
const post = this.model;
|
||||
const likeAction = post.get("likeAction");
|
||||
|
||||
if (likeAction && likeAction.get("canToggle")) {
|
||||
return likeAction.togglePromise(post).then((result) => {
|
||||
this.appEvents.trigger("page:like-toggled", post, likeAction);
|
||||
return this._warnIfClose(result);
|
||||
});
|
||||
const result = await likeAction.togglePromise(post);
|
||||
|
||||
this.appEvents.trigger("page:like-toggled", post, likeAction);
|
||||
return this._warnIfClose(result);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -24,10 +24,10 @@ acceptance("Post controls", function () {
|
||||
.hasAria("pressed", "true", "show likes button is now pressed");
|
||||
|
||||
assert
|
||||
.dom("#post_2 .small-user-list.who-liked")
|
||||
.dom("#post_2 .small-user-list.who-liked .small-user-list-content")
|
||||
.hasAttribute("role", "list", "likes container has list role");
|
||||
assert
|
||||
.dom("#post_2 .small-user-list.who-liked")
|
||||
.dom("#post_2 .small-user-list.who-liked .small-user-list-content")
|
||||
.hasAria(
|
||||
"label",
|
||||
I18n.t("post.actions.people.sr_post_likers_list_description"),
|
||||
|
||||
@@ -6,6 +6,7 @@ import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { resetPostMenuExtraButtons } from "discourse/widgets/post-menu";
|
||||
import { createWidget } from "discourse/widgets/widget";
|
||||
import { withSilencedDeprecations } from "discourse-common/lib/deprecated";
|
||||
|
||||
module("Integration | Component | Widget | post-menu", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
@@ -17,18 +18,21 @@ module("Integration | Component | Widget | post-menu", function (hooks) {
|
||||
test("add extra button", async function (assert) {
|
||||
this.set("args", {});
|
||||
withPluginApi("0.14.0", (api) => {
|
||||
api.addPostMenuButton("coffee", () => {
|
||||
return {
|
||||
action: "drinkCoffee",
|
||||
icon: "mug-saucer",
|
||||
className: "hot-coffee",
|
||||
title: "coffee.title",
|
||||
position: "first",
|
||||
};
|
||||
withSilencedDeprecations("discourse.post-menu-widget-overrides", () => {
|
||||
api.addPostMenuButton("coffee", () => {
|
||||
return {
|
||||
action: "drinkCoffee",
|
||||
icon: "mug-saucer",
|
||||
className: "hot-coffee",
|
||||
title: "coffee.title",
|
||||
position: "first",
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await render(hbs`<MountWidget @widget="post-menu" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post-menu" @args={{this.args}} />`);
|
||||
|
||||
assert
|
||||
.dom(".actions .extra-buttons .hot-coffee")
|
||||
@@ -41,25 +45,27 @@ module("Integration | Component | Widget | post-menu", function (hooks) {
|
||||
let testPost = null;
|
||||
|
||||
withPluginApi("0.14.0", (api) => {
|
||||
api.addPostMenuButton("coffee", () => {
|
||||
return {
|
||||
action: ({ post, showFeedback }) => {
|
||||
testPost = post;
|
||||
showFeedback("coffee.drink");
|
||||
},
|
||||
icon: "mug-saucer",
|
||||
className: "hot-coffee",
|
||||
title: "coffee.title",
|
||||
position: "first",
|
||||
actionParam: { id: 123 }, // hack for testing
|
||||
};
|
||||
withSilencedDeprecations("discourse.post-menu-widget-overrides", () => {
|
||||
api.addPostMenuButton("coffee", () => {
|
||||
return {
|
||||
action: ({ post, showFeedback }) => {
|
||||
testPost = post;
|
||||
showFeedback("coffee.drink");
|
||||
},
|
||||
icon: "mug-saucer",
|
||||
className: "hot-coffee",
|
||||
title: "coffee.title",
|
||||
position: "first",
|
||||
actionParam: { id: 123 }, // hack for testing
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
<article data-post-id="123">
|
||||
<MountWidget @widget="post-menu" @args={{this.args}} />
|
||||
</article>`);
|
||||
<article data-post-id="123">
|
||||
<MountWidget @widget="post-menu" @args={{this.args}} />
|
||||
</article>`);
|
||||
|
||||
await click(".hot-coffee");
|
||||
|
||||
@@ -75,12 +81,15 @@ module("Integration | Component | Widget | post-menu", function (hooks) {
|
||||
this.set("args", { canCreatePost: true, canRemoveReply: true });
|
||||
|
||||
withPluginApi("0.14.0", (api) => {
|
||||
api.removePostMenuButton("reply", (attrs) => {
|
||||
return attrs.canRemoveReply;
|
||||
withSilencedDeprecations("discourse.post-menu-widget-overrides", () => {
|
||||
api.removePostMenuButton("reply", (attrs) => {
|
||||
return attrs.canRemoveReply;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await render(hbs`<MountWidget @widget="post-menu" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post-menu" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".actions .reply").doesNotExist("removes reply button");
|
||||
});
|
||||
@@ -89,12 +98,15 @@ module("Integration | Component | Widget | post-menu", function (hooks) {
|
||||
this.set("args", { canCreatePost: true, canRemoveReply: false });
|
||||
|
||||
withPluginApi("0.14.0", (api) => {
|
||||
api.removePostMenuButton("reply", (attrs) => {
|
||||
return attrs.canRemoveReply;
|
||||
withSilencedDeprecations("discourse.post-menu-widget-overrides", () => {
|
||||
api.removePostMenuButton("reply", (attrs) => {
|
||||
return attrs.canRemoveReply;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await render(hbs`<MountWidget @widget="post-menu" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post-menu" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".actions .reply").exists("does not remove reply button");
|
||||
});
|
||||
@@ -103,10 +115,13 @@ module("Integration | Component | Widget | post-menu", function (hooks) {
|
||||
this.set("args", { canCreatePost: true });
|
||||
|
||||
withPluginApi("0.14.0", (api) => {
|
||||
api.removePostMenuButton("reply");
|
||||
withSilencedDeprecations("discourse.post-menu-widget-overrides", () => {
|
||||
api.removePostMenuButton("reply");
|
||||
});
|
||||
});
|
||||
|
||||
await render(hbs`<MountWidget @widget="post-menu" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post-menu" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".actions .reply").doesNotExist("removes reply button");
|
||||
});
|
||||
@@ -115,11 +130,14 @@ module("Integration | Component | Widget | post-menu", function (hooks) {
|
||||
this.set("args", {});
|
||||
|
||||
withPluginApi("0.14.0", (api) => {
|
||||
api.removePostMenuButton("reply", () => true);
|
||||
api.removePostMenuButton("reply", () => false);
|
||||
withSilencedDeprecations("discourse.post-menu-widget-overrides", () => {
|
||||
api.removePostMenuButton("reply", () => true);
|
||||
api.removePostMenuButton("reply", () => false);
|
||||
});
|
||||
});
|
||||
|
||||
await render(hbs`<MountWidget @widget="post-menu" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post-menu" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".actions .reply").doesNotExist("removes reply button");
|
||||
});
|
||||
@@ -134,16 +152,19 @@ module("Integration | Component | Widget | post-menu", function (hooks) {
|
||||
this.set("args", { id: 1, canCreatePost: true });
|
||||
|
||||
withPluginApi("0.14.0", (api) => {
|
||||
api.replacePostMenuButton("reply", {
|
||||
name: "post-menu-replacement",
|
||||
buildAttrs: (widget) => {
|
||||
return widget.attrs;
|
||||
},
|
||||
shouldRender: (widget) => widget.attrs.id === 1, // true!
|
||||
withSilencedDeprecations("discourse.post-menu-widget-overrides", () => {
|
||||
api.replacePostMenuButton("reply", {
|
||||
name: "post-menu-replacement",
|
||||
buildAttrs: (widget) => {
|
||||
return widget.attrs;
|
||||
},
|
||||
shouldRender: (widget) => widget.attrs.id === 1, // true!
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await render(hbs`<MountWidget @widget="post-menu" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post-menu" @args={{this.args}} />`);
|
||||
|
||||
assert.dom("h1.post-menu-replacement").exists("replacement is rendered");
|
||||
assert
|
||||
@@ -155,16 +176,19 @@ module("Integration | Component | Widget | post-menu", function (hooks) {
|
||||
this.set("args", { id: 1, canCreatePost: true, canRemoveReply: false });
|
||||
|
||||
withPluginApi("0.14.0", (api) => {
|
||||
api.replacePostMenuButton("reply", {
|
||||
name: "post-menu-replacement",
|
||||
buildAttrs: (widget) => {
|
||||
return widget.attrs;
|
||||
},
|
||||
shouldRender: (widget) => widget.attrs.id === 102323948, // false!
|
||||
withSilencedDeprecations("discourse.post-menu-widget-overrides", () => {
|
||||
api.replacePostMenuButton("reply", {
|
||||
name: "post-menu-replacement",
|
||||
buildAttrs: (widget) => {
|
||||
return widget.attrs;
|
||||
},
|
||||
shouldRender: (widget) => widget.attrs.id === 102323948, // false!
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await render(hbs`<MountWidget @widget="post-menu" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post-menu" @args={{this.args}} />`);
|
||||
|
||||
assert
|
||||
.dom("h1.post-menu-replacement")
|
||||
|
||||
@@ -5,6 +5,7 @@ import { module, test } from "qunit";
|
||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { resetPostMenuExtraButtons } from "discourse/widgets/post-menu";
|
||||
import { withSilencedDeprecations } from "discourse-common/lib/deprecated";
|
||||
|
||||
function postStreamTest(name, attrs) {
|
||||
test(name, async function (assert) {
|
||||
@@ -30,15 +31,17 @@ module("Integration | Component | Widget | post-stream", function (hooks) {
|
||||
postStreamTest("extensibility", {
|
||||
posts() {
|
||||
withPluginApi("0.14.0", (api) => {
|
||||
api.addPostMenuButton("coffee", (transformedPost) => {
|
||||
lastTransformedPost = transformedPost;
|
||||
return {
|
||||
action: "drinkCoffee",
|
||||
icon: "mug-saucer",
|
||||
className: "hot-coffee",
|
||||
title: "coffee.title",
|
||||
position: "first",
|
||||
};
|
||||
withSilencedDeprecations("discourse.post-menu-widget-overrides", () => {
|
||||
api.addPostMenuButton("coffee", (transformedPost) => {
|
||||
lastTransformedPost = transformedPost;
|
||||
return {
|
||||
action: "drinkCoffee",
|
||||
icon: "mug-saucer",
|
||||
className: "hot-coffee",
|
||||
title: "coffee.title",
|
||||
position: "first",
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,809 @@
|
||||
import EmberObject from "@ember/object";
|
||||
import { getOwner } from "@ember/owner";
|
||||
import { click, render, triggerEvent } from "@ember/test-helpers";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
import { module, test } from "qunit";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import {
|
||||
count,
|
||||
exists,
|
||||
query,
|
||||
queryAll,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
module(
|
||||
"Integration | Component | Widget | post with glimmer-post-menu",
|
||||
function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.siteSettings.glimmer_post_menu_mode = "enabled";
|
||||
this.siteSettings.post_menu_hidden_items = "";
|
||||
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", { id: 123 });
|
||||
const post = store.createRecord("post", {
|
||||
id: 1,
|
||||
post_number: 1,
|
||||
topic,
|
||||
like_count: 3,
|
||||
actions_summary: [{ id: 2, count: 1, hidden: false, can_act: true }],
|
||||
});
|
||||
|
||||
this.set("post", post);
|
||||
this.set("args", {});
|
||||
});
|
||||
|
||||
test("basic elements", async function (assert) {
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", {
|
||||
id: 123,
|
||||
archetype: "regular",
|
||||
});
|
||||
|
||||
this.set("args", { shareUrl: "/example", post_number: 1, topic });
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post"
|
||||
@model={{this.post}}
|
||||
@args={{this.args}} />`);
|
||||
|
||||
assert.ok(exists(".names"), "includes poster name");
|
||||
assert.ok(exists("a.post-date"), "includes post date");
|
||||
});
|
||||
|
||||
test("post - links", async function (assert) {
|
||||
this.set("args", {
|
||||
cooked:
|
||||
"<a href='http://link1.example.com/'>first link</a> and <a href='http://link2.example.com/?some=query'>second link</a>",
|
||||
linkCounts: [
|
||||
{ url: "http://link1.example.com/", clicks: 1, internal: true },
|
||||
{ url: "http://link2.example.com/", clicks: 2, internal: true },
|
||||
],
|
||||
});
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post-contents" @model={{this.post}} @args={{this.args}} />`
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
queryAll("a[data-clicks='1']")[0].getAttribute("data-clicks"),
|
||||
"1"
|
||||
);
|
||||
assert.strictEqual(
|
||||
queryAll("a[data-clicks='2']")[0].getAttribute("data-clicks"),
|
||||
"2"
|
||||
);
|
||||
});
|
||||
|
||||
test("post - onebox links", async function (assert) {
|
||||
this.set("args", {
|
||||
cooked: `
|
||||
<p><a href="https://example.com">Other URL</a></p>
|
||||
|
||||
<aside class="onebox twitterstatus" data-onebox-src="https://twitter.com/codinghorror">
|
||||
<header class="source">
|
||||
<a href="https://twitter.com/codinghorror" target="_blank" rel="noopener">twitter.com</a>
|
||||
</header>
|
||||
<article class="onebox-body">
|
||||
<h4><a href="https://twitter.com/codinghorror" target="_blank" rel="noopener">Jeff Atwood</a></h4>
|
||||
<div class="twitter-screen-name"><a href="https://twitter.com/codinghorror" target="_blank" rel="noopener">@codinghorror</a></div>
|
||||
</article>
|
||||
</aside>`,
|
||||
linkCounts: [
|
||||
{ url: "https://example.com", clicks: 1 },
|
||||
{ url: "https://twitter.com/codinghorror", clicks: 2 },
|
||||
],
|
||||
});
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post-contents" @model={{this.post}} @args={{this.args}} />`
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
queryAll("a[data-clicks='1']")[0].getAttribute("data-clicks"),
|
||||
"1",
|
||||
"First link has correct data attribute and content"
|
||||
);
|
||||
assert.strictEqual(
|
||||
queryAll("a[data-clicks='2']")[0].getAttribute("data-clicks"),
|
||||
"2",
|
||||
"Second link has correct data attribute and content"
|
||||
);
|
||||
});
|
||||
|
||||
test("wiki", async function (assert) {
|
||||
this.set("args", { wiki: true, version: 2, canViewEditHistory: true });
|
||||
this.set("showHistory", () => (this.historyShown = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}}
|
||||
@showHistory={{this.showHistory}} />
|
||||
`);
|
||||
|
||||
await click(".post-info .wiki");
|
||||
assert.ok(
|
||||
this.historyShown,
|
||||
"clicking the wiki icon displays the post history"
|
||||
);
|
||||
});
|
||||
|
||||
test("wiki without revision", async function (assert) {
|
||||
this.set("args", { wiki: true, version: 1, canViewEditHistory: true });
|
||||
this.set("editPost", () => (this.editPostCalled = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}}
|
||||
@editPost={{this.editPost}} />
|
||||
`);
|
||||
|
||||
await click(".post-info .wiki");
|
||||
assert.ok(this.editPostCalled, "clicking wiki icon edits the post");
|
||||
});
|
||||
|
||||
test("via-email", async function (assert) {
|
||||
this.set("args", { via_email: true, canViewRawEmail: true });
|
||||
this.set("showRawEmail", () => (this.rawEmailShown = true));
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} @showRawEmail={{this.showRawEmail}} />`
|
||||
);
|
||||
|
||||
await click(".post-info.via-email");
|
||||
assert.ok(
|
||||
this.rawEmailShown,
|
||||
"clicking the envelope shows the raw email"
|
||||
);
|
||||
});
|
||||
|
||||
test("via-email without permission", async function (assert) {
|
||||
this.set("args", { via_email: true, canViewRawEmail: false });
|
||||
this.set("showRawEmail", () => (this.rawEmailShown = true));
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} @showRawEmail={{this.showRawEmail}} />`
|
||||
);
|
||||
|
||||
await click(".post-info.via-email");
|
||||
assert.ok(
|
||||
!this.rawEmailShown,
|
||||
"clicking the envelope doesn't show the raw email"
|
||||
);
|
||||
});
|
||||
|
||||
test("history", async function (assert) {
|
||||
this.set("args", { version: 3, canViewEditHistory: true });
|
||||
this.set("showHistory", () => (this.historyShown = true));
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} @showHistory={{this.showHistory}} />`
|
||||
);
|
||||
|
||||
await click(".post-info.edits button");
|
||||
assert.ok(this.historyShown, "clicking the pencil shows the history");
|
||||
});
|
||||
|
||||
test("history without view permission", async function (assert) {
|
||||
this.set("args", { version: 3, canViewEditHistory: false });
|
||||
this.set("showHistory", () => (this.historyShown = true));
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} @showHistory={{this.showHistory}} />`
|
||||
);
|
||||
|
||||
await click(".post-info.edits");
|
||||
assert.ok(
|
||||
!this.historyShown,
|
||||
`clicking the pencil doesn't show the history`
|
||||
);
|
||||
});
|
||||
|
||||
test("whisper", async function (assert) {
|
||||
this.set("args", { isWhisper: true });
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`);
|
||||
|
||||
assert.strictEqual(count(".topic-post.whisper"), 1);
|
||||
assert.strictEqual(count(".post-info.whisper"), 1);
|
||||
});
|
||||
|
||||
test(`read indicator`, async function (assert) {
|
||||
this.set("args", { read: true });
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`);
|
||||
|
||||
assert.ok(exists(".read-state.read"));
|
||||
});
|
||||
|
||||
test(`unread indicator`, async function (assert) {
|
||||
this.set("args", { read: false });
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`);
|
||||
|
||||
assert.ok(exists(".read-state"));
|
||||
});
|
||||
|
||||
test("reply directly above (suppressed)", async function (assert) {
|
||||
this.set("args", {
|
||||
replyToUsername: "eviltrout",
|
||||
replyToAvatarTemplate: "/images/avatar.png",
|
||||
replyDirectlyAbove: true,
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`);
|
||||
|
||||
assert.ok(!exists("a.reply-to-tab"), "hides the tab");
|
||||
assert.ok(!exists(".avoid-tab"), "doesn't have the avoid tab class");
|
||||
});
|
||||
|
||||
test("reply a few posts above (suppressed)", async function (assert) {
|
||||
this.set("args", {
|
||||
replyToUsername: "eviltrout",
|
||||
replyToAvatarTemplate: "/images/avatar.png",
|
||||
replyDirectlyAbove: false,
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`);
|
||||
|
||||
assert.ok(exists("a.reply-to-tab"), "shows the tab");
|
||||
assert.strictEqual(count(".avoid-tab"), 1, "has the avoid tab class");
|
||||
});
|
||||
|
||||
test("reply directly above", async function (assert) {
|
||||
this.set("args", {
|
||||
replyToUsername: "eviltrout",
|
||||
replyToAvatarTemplate: "/images/avatar.png",
|
||||
replyDirectlyAbove: true,
|
||||
});
|
||||
this.siteSettings.suppress_reply_directly_above = false;
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`);
|
||||
|
||||
assert.strictEqual(count(".avoid-tab"), 1, "has the avoid tab class");
|
||||
await click("a.reply-to-tab");
|
||||
assert.strictEqual(count("section.embedded-posts.top .cooked"), 1);
|
||||
assert.strictEqual(count("section.embedded-posts .d-icon-arrow-up"), 1);
|
||||
});
|
||||
|
||||
test("cooked content hidden", async function (assert) {
|
||||
this.set("args", { cooked_hidden: true, canSeeHiddenPost: true });
|
||||
this.set("expandHidden", () => (this.unhidden = true));
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} @expandHidden={{this.expandHidden}} />`
|
||||
);
|
||||
|
||||
await click(".topic-body .expand-hidden");
|
||||
assert.ok(this.unhidden, "triggers the action");
|
||||
});
|
||||
|
||||
test(`cooked content hidden - can't view hidden post`, async function (assert) {
|
||||
this.set("args", { cooked_hidden: true, canSeeHiddenPost: false });
|
||||
this.set("expandHidden", () => (this.unhidden = true));
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} @expandHidden={{this.expandHidden}} />`
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
!exists(".topic-body .expand-hidden"),
|
||||
"button is not displayed"
|
||||
);
|
||||
});
|
||||
|
||||
test("expand first post", async function (assert) {
|
||||
this.set("args", { expandablePost: true });
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`
|
||||
);
|
||||
|
||||
await click(".topic-body .expand-post");
|
||||
assert.ok(!exists(".expand-post"), "button is gone");
|
||||
});
|
||||
|
||||
test("can't show admin menu when you can't manage", async function (assert) {
|
||||
this.set("args", { canManage: false });
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`);
|
||||
|
||||
assert.ok(!exists(".post-menu-area .show-post-admin-menu"));
|
||||
});
|
||||
|
||||
test("show admin menu", async function (assert) {
|
||||
this.currentUser.admin = true;
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />
|
||||
<DMenus/>`
|
||||
);
|
||||
|
||||
assert
|
||||
.dom("[data-content][data-identifier='admin-post-menu']")
|
||||
.doesNotExist();
|
||||
|
||||
await click(".post-menu-area .show-post-admin-menu");
|
||||
assert.dom("[data-content][data-identifier='admin-post-menu']").exists();
|
||||
|
||||
await triggerEvent(".post-menu-area", "pointerdown");
|
||||
assert
|
||||
.dom("[data-content][data-identifier='admin-post-menu']")
|
||||
.doesNotExist("clicking outside clears the popup");
|
||||
});
|
||||
|
||||
test("permanently delete topic", async function (assert) {
|
||||
this.currentUser.set("admin", true);
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", {
|
||||
id: 123,
|
||||
details: { can_permanently_delete: true },
|
||||
});
|
||||
const post = store.createRecord("post", {
|
||||
id: 1,
|
||||
post_number: 1,
|
||||
deleted_at: new Date().toISOString(),
|
||||
topic,
|
||||
});
|
||||
|
||||
this.set("args", { canManage: true });
|
||||
this.set("post", post);
|
||||
this.set("permanentlyDeletePost", () => (this.deleted = true));
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}}
|
||||
@permanentlyDeletePost={{this.permanentlyDeletePost}} />
|
||||
<DMenus/>`
|
||||
);
|
||||
|
||||
await click(".post-menu-area .show-post-admin-menu");
|
||||
await click(
|
||||
"[data-content][data-identifier='admin-post-menu'] .permanently-delete"
|
||||
);
|
||||
assert.ok(this.deleted);
|
||||
assert
|
||||
.dom("[data-content][data-identifier='admin-post-menu']")
|
||||
.doesNotExist("also hides the menu");
|
||||
});
|
||||
|
||||
test("permanently delete post", async function (assert) {
|
||||
this.currentUser.set("admin", true);
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", {
|
||||
id: 123,
|
||||
});
|
||||
const post = store.createRecord("post", {
|
||||
id: 1,
|
||||
post_number: 2,
|
||||
deleted_at: new Date().toISOString(),
|
||||
can_permanently_delete: true,
|
||||
topic,
|
||||
});
|
||||
|
||||
this.set("args", { canManage: true });
|
||||
this.set("post", post);
|
||||
this.set("permanentlyDeletePost", () => (this.deleted = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}}
|
||||
@permanentlyDeletePost={{this.permanentlyDeletePost}} />
|
||||
<DMenus/>
|
||||
`);
|
||||
|
||||
await click(".post-menu-area .show-post-admin-menu");
|
||||
|
||||
await click(
|
||||
"[data-content][data-identifier='admin-post-menu'] .permanently-delete"
|
||||
);
|
||||
assert.ok(this.deleted);
|
||||
assert
|
||||
.dom("[data-content][data-identifier='admin-post-menu']")
|
||||
.doesNotExist("also hides the menu");
|
||||
});
|
||||
|
||||
test("toggle moderator post", async function (assert) {
|
||||
this.currentUser.set("moderator", true);
|
||||
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", {
|
||||
id: 123,
|
||||
});
|
||||
const post = store.createRecord("post", {
|
||||
id: 1,
|
||||
post_number: 2,
|
||||
deleted_at: new Date().toISOString(),
|
||||
can_permanently_delete: true,
|
||||
topic,
|
||||
});
|
||||
|
||||
this.set("args", { canManage: true });
|
||||
this.set("post", post);
|
||||
this.set("togglePostType", () => (this.toggled = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}}
|
||||
@togglePostType={{this.togglePostType}} />
|
||||
<DMenus/>`);
|
||||
|
||||
await click(".post-menu-area .show-post-admin-menu");
|
||||
await click(
|
||||
"[data-content][data-identifier='admin-post-menu'] .toggle-post-type"
|
||||
);
|
||||
|
||||
assert.ok(this.toggled);
|
||||
assert
|
||||
.dom("[data-content][data-identifier='admin-post-menu']")
|
||||
.doesNotExist("also hides the menu");
|
||||
});
|
||||
|
||||
test("rebake post", async function (assert) {
|
||||
this.currentUser.moderator = true;
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", { id: 123 });
|
||||
const post = store.createRecord("post", {
|
||||
id: 1,
|
||||
post_number: 1,
|
||||
topic,
|
||||
});
|
||||
|
||||
this.set("args", { canManage: true });
|
||||
this.set("post", post);
|
||||
this.set("rebakePost", () => (this.baked = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}}
|
||||
@rebakePost={{this.rebakePost}} />
|
||||
<DMenus/>`);
|
||||
|
||||
await click(".post-menu-area .show-post-admin-menu");
|
||||
await click(
|
||||
"[data-content][data-identifier='admin-post-menu'] .rebuild-html"
|
||||
);
|
||||
assert.ok(this.baked);
|
||||
assert
|
||||
.dom("[data-content][data-identifier='admin-post-menu']")
|
||||
.doesNotExist("also hides the menu");
|
||||
});
|
||||
|
||||
test("unhide post", async function (assert) {
|
||||
let unhidden;
|
||||
|
||||
this.currentUser.admin = true;
|
||||
this.post.hidden = true;
|
||||
this.set("args", { canManage: true });
|
||||
this.set("unhidePost", () => (unhidden = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}}
|
||||
@unhidePost={{this.unhidePost}} />
|
||||
<DMenus/>
|
||||
`);
|
||||
|
||||
await click(".post-menu-area .show-post-admin-menu");
|
||||
|
||||
await click(
|
||||
"[data-content][data-identifier='admin-post-menu'] .unhide-post"
|
||||
);
|
||||
|
||||
assert.ok(unhidden);
|
||||
|
||||
assert
|
||||
.dom("[data-content][data-identifier='admin-post-menu']")
|
||||
.doesNotExist("also hides the menu");
|
||||
});
|
||||
|
||||
test("change owner", async function (assert) {
|
||||
this.currentUser.admin = true;
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", { id: 123 });
|
||||
const post = store.createRecord("post", {
|
||||
id: 1,
|
||||
post_number: 1,
|
||||
hidden: true,
|
||||
topic,
|
||||
});
|
||||
|
||||
this.set("args", { canManage: true });
|
||||
this.set("post", post);
|
||||
this.set("changePostOwner", () => (this.owned = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}}
|
||||
@changePostOwner={{this.changePostOwner}} />
|
||||
<DMenus/>
|
||||
`);
|
||||
|
||||
await click(".post-menu-area .show-post-admin-menu");
|
||||
await click(
|
||||
"[data-content][data-identifier='admin-post-menu'] .change-owner"
|
||||
);
|
||||
assert.ok(this.owned);
|
||||
assert
|
||||
.dom("[data-content][data-identifier='admin-post-menu']")
|
||||
.doesNotExist("also hides the menu");
|
||||
});
|
||||
|
||||
test("shows the topic map when setting the 'topicMap' attribute", async function (assert) {
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", { id: 123 });
|
||||
this.set("args", { topic, post_number: 1, topicMap: true });
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".topic-map").exists();
|
||||
});
|
||||
|
||||
test("shows the topic map when no replies", async function (assert) {
|
||||
this.siteSettings.show_topic_map_in_topics_without_replies = true;
|
||||
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", {
|
||||
id: 123,
|
||||
archetype: "regular",
|
||||
});
|
||||
this.set("args", { topic, post_number: 1 });
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".topic-map").exists();
|
||||
});
|
||||
|
||||
test("topic map - few participants", async function (assert) {
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", {
|
||||
id: 123,
|
||||
posts_count: 10,
|
||||
participant_count: 2,
|
||||
archetype: "regular",
|
||||
});
|
||||
topic.details.set("participants", [
|
||||
{ username: "eviltrout" },
|
||||
{ username: "codinghorror" },
|
||||
]);
|
||||
this.set("args", {
|
||||
topic,
|
||||
post_number: 1,
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`);
|
||||
assert.dom(".topic-map__users-trigger").doesNotExist();
|
||||
assert.dom(".topic-map__users-list a.poster").exists({ count: 2 });
|
||||
});
|
||||
|
||||
test("topic map - participants", async function (assert) {
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", {
|
||||
id: 123,
|
||||
posts_count: 10,
|
||||
participant_count: 6,
|
||||
archetype: "regular",
|
||||
});
|
||||
topic.postStream.setProperties({ userFilters: ["sam", "codinghorror"] });
|
||||
topic.details.set("participants", [
|
||||
{ username: "eviltrout" },
|
||||
{ username: "codinghorror" },
|
||||
{ username: "sam" },
|
||||
{ username: "zogstrip" },
|
||||
{ username: "joffreyjaffeux" },
|
||||
{ username: "david" },
|
||||
]);
|
||||
|
||||
this.set("args", {
|
||||
topic,
|
||||
post_number: 1,
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`);
|
||||
assert.dom(".topic-map__users-list a.poster").exists({ count: 5 });
|
||||
|
||||
await click(".topic-map__users-trigger");
|
||||
assert
|
||||
.dom(".topic-map__users-content .topic-map__users-list a.poster")
|
||||
.exists({ count: 6 });
|
||||
});
|
||||
|
||||
test("topic map - links", async function (assert) {
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", {
|
||||
id: 123,
|
||||
posts_count: 2,
|
||||
archetype: "regular",
|
||||
});
|
||||
topic.details.set("links", [
|
||||
{ url: "http://link1.example.com", clicks: 0 },
|
||||
{ url: "http://link2.example.com", clicks: 0 },
|
||||
{ url: "http://link3.example.com", clicks: 0 },
|
||||
{ url: "http://link4.example.com", clicks: 0 },
|
||||
{ url: "http://link5.example.com", clicks: 0 },
|
||||
{ url: "http://link6.example.com", clicks: 0 },
|
||||
]);
|
||||
this.set("args", { topic, post_number: 1 });
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".topic-map").exists({ count: 1 });
|
||||
assert.dom(".topic-map__links-content").doesNotExist();
|
||||
await click(".topic-map__links-trigger");
|
||||
assert.dom(".topic-map__links-content").exists({ count: 1 });
|
||||
assert.dom(".topic-map__links-content .topic-link").exists({ count: 5 });
|
||||
await click(".link-summary");
|
||||
assert.dom(".topic-map__links-content .topic-link").exists({ count: 6 });
|
||||
});
|
||||
|
||||
test("topic map - no top reply summary", async function (assert) {
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", {
|
||||
id: 123,
|
||||
archetype: "regular",
|
||||
posts_count: 2,
|
||||
});
|
||||
this.set("args", { topic, post_number: 1 });
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".topic-map").exists();
|
||||
assert.dom(".summarization-button .top-replies").doesNotExist();
|
||||
});
|
||||
|
||||
test("topic map - has top replies summary", async function (assert) {
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", {
|
||||
id: 123,
|
||||
archetype: "regular",
|
||||
posts_count: 2,
|
||||
has_summary: true,
|
||||
});
|
||||
this.set("args", { topic, post_number: 1 });
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".summarization-button .top-replies").exists({ count: 1 });
|
||||
});
|
||||
|
||||
test("pm map", async function (assert) {
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", {
|
||||
id: 123,
|
||||
archetype: "private_message",
|
||||
});
|
||||
topic.details.set("allowed_users", [
|
||||
EmberObject.create({ username: "eviltrout" }),
|
||||
]);
|
||||
this.set("args", {
|
||||
topic,
|
||||
post_number: 1,
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".topic-map__private-message-map").exists({ count: 1 });
|
||||
assert.dom(".topic-map__private-message-map .user").exists({ count: 1 });
|
||||
});
|
||||
|
||||
test("post notice - with username", async function (assert) {
|
||||
const twoDaysAgo = new Date();
|
||||
twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);
|
||||
this.siteSettings.display_name_on_posts = false;
|
||||
this.siteSettings.prioritize_username_in_ux = true;
|
||||
this.siteSettings.old_post_notice_days = 14;
|
||||
this.set("args", {
|
||||
username: "codinghorror",
|
||||
name: "Jeff",
|
||||
created_at: new Date(),
|
||||
notice: {
|
||||
type: "returning_user",
|
||||
lastPostedAt: twoDaysAgo,
|
||||
},
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`);
|
||||
|
||||
assert.strictEqual(
|
||||
query(".post-notice.returning-user:not(.old)").innerText.trim(),
|
||||
I18n.t("post.notice.returning_user", {
|
||||
user: "codinghorror",
|
||||
time: "2 days ago",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("post notice - with name", async function (assert) {
|
||||
this.siteSettings.display_name_on_posts = true;
|
||||
this.siteSettings.prioritize_username_in_ux = false;
|
||||
this.siteSettings.old_post_notice_days = 14;
|
||||
this.set("args", {
|
||||
username: "codinghorror",
|
||||
name: "Jeff",
|
||||
created_at: new Date(2019, 0, 1),
|
||||
notice: { type: "new_user" },
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`);
|
||||
|
||||
assert.strictEqual(
|
||||
query(".post-notice.old.new-user").innerText.trim(),
|
||||
I18n.t("post.notice.new_user", { user: "Jeff", time: "Jan '10" })
|
||||
);
|
||||
});
|
||||
|
||||
test("show group request in post", async function (assert) {
|
||||
this.set("args", {
|
||||
username: "foo",
|
||||
requestedGroupName: "testGroup",
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`);
|
||||
|
||||
const link = query(".group-request a");
|
||||
assert.strictEqual(
|
||||
link.innerText.trim(),
|
||||
I18n.t("groups.requests.handle")
|
||||
);
|
||||
assert.strictEqual(
|
||||
link.getAttribute("href"),
|
||||
"/g/testGroup/requests?filter=foo"
|
||||
);
|
||||
});
|
||||
|
||||
test("shows user status if enabled in site settings", async function (assert) {
|
||||
this.siteSettings.enable_user_status = true;
|
||||
const status = {
|
||||
emoji: "tooth",
|
||||
description: "off to dentist",
|
||||
};
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const user = store.createRecord("user", { status });
|
||||
this.set("args", { user });
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`);
|
||||
|
||||
assert.ok(exists(".user-status-message"));
|
||||
});
|
||||
|
||||
test("doesn't show user status if disabled in site settings", async function (assert) {
|
||||
this.siteSettings.enable_user_status = false;
|
||||
const status = {
|
||||
emoji: "tooth",
|
||||
description: "off to dentist",
|
||||
};
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const user = store.createRecord("user", { status });
|
||||
this.set("args", { user });
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`);
|
||||
|
||||
assert.notOk(exists(".user-status-message"));
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,3 +1,5 @@
|
||||
// deprecated in favor of ./post-test-with-glimmer-post-menu.js
|
||||
|
||||
import EmberObject from "@ember/object";
|
||||
import { getOwner } from "@ember/owner";
|
||||
import { click, render, triggerEvent } from "@ember/test-helpers";
|
||||
@@ -10,6 +12,10 @@ import I18n from "discourse-i18n";
|
||||
module("Integration | Component | Widget | post", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.siteSettings.glimmer_post_menu_mode = "disabled";
|
||||
});
|
||||
|
||||
test("basic elements", async function (assert) {
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", {
|
||||
@@ -19,7 +25,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
|
||||
this.set("args", { shareUrl: "/example", post_number: 1, topic });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".names").exists("includes poster name");
|
||||
assert.dom("a.post-date").exists("includes post date");
|
||||
@@ -36,7 +43,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
});
|
||||
|
||||
await render(
|
||||
hbs`<MountWidget @widget="post-contents" @args={{this.args}} />`
|
||||
hbs`
|
||||
<MountWidget @widget="post-contents" @args={{this.args}} />`
|
||||
);
|
||||
|
||||
assert.dom("a[data-clicks='1']").hasAttribute("data-clicks", "1");
|
||||
@@ -64,7 +72,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
});
|
||||
|
||||
await render(
|
||||
hbs`<MountWidget @widget="post-contents" @args={{this.args}} />`
|
||||
hbs`
|
||||
<MountWidget @widget="post-contents" @args={{this.args}} />`
|
||||
);
|
||||
|
||||
assert
|
||||
@@ -114,9 +123,10 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
this.set("args", { via_email: true, canViewRawEmail: true });
|
||||
this.set("showRawEmail", () => (this.rawEmailShown = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @showRawEmail={{this.showRawEmail}} />
|
||||
`);
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @showRawEmail={{this.showRawEmail}} />`
|
||||
);
|
||||
|
||||
await click(".post-info.via-email");
|
||||
assert.ok(this.rawEmailShown, "clicking the envelope shows the raw email");
|
||||
@@ -126,9 +136,10 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
this.set("args", { via_email: true, canViewRawEmail: false });
|
||||
this.set("showRawEmail", () => (this.rawEmailShown = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @showRawEmail={{this.showRawEmail}} />
|
||||
`);
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @showRawEmail={{this.showRawEmail}} />`
|
||||
);
|
||||
|
||||
await click(".post-info.via-email");
|
||||
assert.ok(
|
||||
@@ -141,9 +152,10 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
this.set("args", { version: 3, canViewEditHistory: true });
|
||||
this.set("showHistory", () => (this.historyShown = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @showHistory={{this.showHistory}} />
|
||||
`);
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @showHistory={{this.showHistory}} />`
|
||||
);
|
||||
|
||||
await click(".post-info.edits button");
|
||||
assert.ok(this.historyShown, "clicking the pencil shows the history");
|
||||
@@ -153,9 +165,10 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
this.set("args", { version: 3, canViewEditHistory: false });
|
||||
this.set("showHistory", () => (this.historyShown = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @showHistory={{this.showHistory}} />
|
||||
`);
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @showHistory={{this.showHistory}} />`
|
||||
);
|
||||
|
||||
await click(".post-info.edits");
|
||||
assert.ok(
|
||||
@@ -167,12 +180,14 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
test("whisper", async function (assert) {
|
||||
this.set("args", { isWhisper: true });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".topic-post.whisper").exists();
|
||||
assert.dom(".post-info.whisper").exists();
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test("like count button", async function (assert) {
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", { id: 123 });
|
||||
@@ -187,7 +202,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
this.set("args", { likeCount: 1 });
|
||||
|
||||
await render(
|
||||
hbs`<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`
|
||||
hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`
|
||||
);
|
||||
|
||||
assert.dom("button.like-count").exists();
|
||||
@@ -204,35 +220,42 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
assert.dom(".who-liked a.trigger-user-card").doesNotExist();
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test("like count with no likes", async function (assert) {
|
||||
this.set("args", { likeCount: 0 });
|
||||
|
||||
await render(
|
||||
hbs`<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`
|
||||
hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`
|
||||
);
|
||||
|
||||
assert.dom("button.like-count").doesNotExist();
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test("share button", async function (assert) {
|
||||
this.siteSettings.post_menu += "|share";
|
||||
this.set("args", { shareUrl: "http://share-me.example.com" });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".actions button.share").exists("renders a share button");
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test("copy link button", async function (assert) {
|
||||
this.set("args", { shareUrl: "http://share-me.example.com" });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert
|
||||
.dom(".actions button.post-action-menu__copy-link")
|
||||
.exists("it renders a copy link button");
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test("liking", async function (assert) {
|
||||
const args = { showLike: true, canToggleLike: true, id: 5 };
|
||||
this.set("args", args);
|
||||
@@ -241,9 +264,10 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
args.likeCount = args.liked ? 1 : 0;
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post-menu" @args={{this.args}} @toggleLike={{this.toggleLike}} />
|
||||
`);
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post-menu" @args={{this.args}} @toggleLike={{this.toggleLike}} />`
|
||||
);
|
||||
|
||||
assert.dom(".actions button.like").exists();
|
||||
assert.dom(".actions button.like-count").doesNotExist();
|
||||
@@ -259,15 +283,17 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
assert.dom(".actions button.like-count").doesNotExist();
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test("anon liking", async function (assert) {
|
||||
this.owner.unregister("service:current-user");
|
||||
const args = { showLike: true };
|
||||
this.set("args", args);
|
||||
this.set("showLogin", () => (this.loginShown = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post-menu" @args={{this.args}} @showLogin={{this.showLogin}} />
|
||||
`);
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post-menu" @args={{this.args}} @showLogin={{this.showLogin}} />`
|
||||
);
|
||||
|
||||
assert.dom(".actions button.like").exists();
|
||||
assert.dom(".actions button.like-count").doesNotExist();
|
||||
@@ -284,58 +310,55 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
assert.ok(this.loginShown);
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test("edit button", async function (assert) {
|
||||
this.set("args", { canEdit: true });
|
||||
this.set("editPost", () => (this.editPostCalled = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @editPost={{this.editPost}} />
|
||||
`);
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @editPost={{this.editPost}} />`
|
||||
);
|
||||
|
||||
await click("button.edit");
|
||||
assert.ok(this.editPostCalled, "it triggered the edit action");
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test(`edit button - can't edit`, async function (assert) {
|
||||
this.set("args", { canEdit: false });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom("button.edit").doesNotExist("button is not displayed");
|
||||
});
|
||||
|
||||
test("recover button", async function (assert) {
|
||||
this.set("args", { canDelete: true });
|
||||
this.set("deletePost", () => (this.deletePostCalled = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @deletePost={{this.deletePost}} />
|
||||
`);
|
||||
|
||||
await click("button.delete");
|
||||
assert.ok(this.deletePostCalled, "it triggered the delete action");
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test("delete topic button", async function (assert) {
|
||||
this.set("args", { canDeleteTopic: true });
|
||||
this.set("deletePost", () => (this.deletePostCalled = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @deletePost={{this.deletePost}} />
|
||||
`);
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @deletePost={{this.deletePost}} />`
|
||||
);
|
||||
|
||||
await click("button.delete");
|
||||
assert.ok(this.deletePostCalled, "it triggered the delete action");
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test(`delete topic button - can't delete`, async function (assert) {
|
||||
this.set("args", { canDeleteTopic: false });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom("button.delete").doesNotExist("button is not displayed");
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test(`delete topic button - can't delete when topic author without permission`, async function (assert) {
|
||||
this.set("args", {
|
||||
canDeleteTopic: false,
|
||||
@@ -343,7 +366,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
canFlag: true,
|
||||
});
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
await click(".show-more-actions");
|
||||
|
||||
@@ -358,47 +382,56 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
);
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test("recover topic button", async function (assert) {
|
||||
this.set("args", { canRecoverTopic: true });
|
||||
this.set("recoverPost", () => (this.recovered = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @recoverPost={{this.recoverPost}} />
|
||||
`);
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @recoverPost={{this.recoverPost}} />`
|
||||
);
|
||||
|
||||
await click("button.recover");
|
||||
assert.ok(this.recovered);
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test(`recover topic button - can't recover`, async function (assert) {
|
||||
this.set("args", { canRecoverTopic: false });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom("button.recover").doesNotExist("button is not displayed");
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test("delete post button", async function (assert) {
|
||||
this.set("args", { canDelete: true, canFlag: true });
|
||||
this.set("deletePost", () => (this.deletePostCalled = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @deletePost={{this.deletePost}} />
|
||||
`);
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @deletePost={{this.deletePost}} />`
|
||||
);
|
||||
|
||||
await click(".show-more-actions");
|
||||
await click("button.delete");
|
||||
assert.ok(this.deletePostCalled, "it triggered the delete action");
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test(`delete post button - can't delete`, async function (assert) {
|
||||
this.set("args", { canDelete: false });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom("button.delete").doesNotExist("button is not displayed");
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test(`delete post button - can't delete, can't flag`, async function (assert) {
|
||||
this.set("args", {
|
||||
canDeleteTopic: false,
|
||||
@@ -406,7 +439,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
canFlag: false,
|
||||
});
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom("button.delete").doesNotExist("delete button is not displayed");
|
||||
assert
|
||||
@@ -414,22 +448,26 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
.doesNotExist("flag button is not displayed");
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test("recover post button", async function (assert) {
|
||||
this.set("args", { canRecover: true });
|
||||
this.set("recoverPost", () => (this.recovered = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @recoverPost={{this.recoverPost}} />
|
||||
`);
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @recoverPost={{this.recoverPost}} />`
|
||||
);
|
||||
|
||||
await click("button.recover");
|
||||
assert.ok(this.recovered);
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test(`recover post button - can't recover`, async function (assert) {
|
||||
this.set("args", { canRecover: false });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom("button.recover").doesNotExist("button is not displayed");
|
||||
});
|
||||
@@ -438,9 +476,10 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
this.set("args", { canFlag: true });
|
||||
this.set("showFlags", () => (this.flagsShown = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @showFlags={{this.showFlags}} />
|
||||
`);
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @showFlags={{this.showFlags}} />`
|
||||
);
|
||||
|
||||
assert.dom("button.create-flag").exists();
|
||||
|
||||
@@ -451,7 +490,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
test(`flagging: can't flag`, async function (assert) {
|
||||
this.set("args", { canFlag: false });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom("button.create-flag").doesNotExist();
|
||||
});
|
||||
@@ -459,7 +499,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
test(`flagging: can't flag when post is hidden`, async function (assert) {
|
||||
this.set("args", { canFlag: true, hidden: true });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom("button.create-flag").doesNotExist();
|
||||
});
|
||||
@@ -467,7 +508,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
test(`read indicator`, async function (assert) {
|
||||
this.set("args", { read: true });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".read-state.read").exists();
|
||||
});
|
||||
@@ -475,7 +517,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
test(`unread indicator`, async function (assert) {
|
||||
this.set("args", { read: false });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".read-state").exists();
|
||||
});
|
||||
@@ -487,7 +530,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
replyDirectlyAbove: true,
|
||||
});
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom("a.reply-to-tab").doesNotExist("hides the tab");
|
||||
assert.dom(".avoid-tab").doesNotExist("doesn't have the avoid tab class");
|
||||
@@ -500,7 +544,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
replyDirectlyAbove: false,
|
||||
});
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom("a.reply-to-tab").exists("shows the tab");
|
||||
assert.dom(".avoid-tab").exists("has the avoid tab class");
|
||||
@@ -514,7 +559,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
});
|
||||
this.siteSettings.suppress_reply_directly_above = false;
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".avoid-tab").exists("has the avoid tab class");
|
||||
await click("a.reply-to-tab");
|
||||
@@ -526,9 +572,10 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
this.set("args", { cooked_hidden: true, canSeeHiddenPost: true });
|
||||
this.set("expandHidden", () => (this.unhidden = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @expandHidden={{this.expandHidden}} />
|
||||
`);
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @expandHidden={{this.expandHidden}} />`
|
||||
);
|
||||
|
||||
await click(".topic-body .expand-hidden");
|
||||
assert.ok(this.unhidden, "triggers the action");
|
||||
@@ -538,9 +585,10 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
this.set("args", { cooked_hidden: true, canSeeHiddenPost: false });
|
||||
this.set("expandHidden", () => (this.unhidden = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @expandHidden={{this.expandHidden}} />
|
||||
`);
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @expandHidden={{this.expandHidden}} />`
|
||||
);
|
||||
|
||||
assert
|
||||
.dom(".topic-body .expand-hidden")
|
||||
@@ -553,31 +601,36 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
this.set("post", store.createRecord("post", { id: 1234 }));
|
||||
|
||||
await render(
|
||||
hbs`<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`
|
||||
hbs`
|
||||
<MountWidget @widget="post" @model={{this.post}} @args={{this.args}} />`
|
||||
);
|
||||
|
||||
await click(".topic-body .expand-post");
|
||||
assert.dom(".expand-post").doesNotExist("button is gone");
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test("can't bookmark", async function (assert) {
|
||||
this.set("args", { canBookmark: false });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom("button.bookmark").doesNotExist();
|
||||
assert.dom("button.bookmarked").doesNotExist();
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test("bookmark", async function (assert) {
|
||||
const args = { canBookmark: true };
|
||||
|
||||
this.set("args", args);
|
||||
this.set("toggleBookmark", () => (args.bookmarked = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @toggleBookmark={{this.toggleBookmark}} />
|
||||
`);
|
||||
await render(
|
||||
hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @toggleBookmark={{this.toggleBookmark}} />`
|
||||
);
|
||||
|
||||
assert.dom(".post-menu-area .bookmark").exists();
|
||||
assert.dom("button.bookmarked").doesNotExist();
|
||||
@@ -586,7 +639,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
test("can't show admin menu when you can't manage", async function (assert) {
|
||||
this.set("args", { canManage: false });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".post-menu-area .show-post-admin-menu").doesNotExist();
|
||||
});
|
||||
@@ -595,14 +649,15 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
this.set("args", { canManage: true });
|
||||
|
||||
await render(
|
||||
hbs`<MountWidget @widget="post" @args={{this.args}} /><DMenus />`
|
||||
hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />
|
||||
<DMenus/>`
|
||||
);
|
||||
|
||||
assert
|
||||
.dom("[data-content][data-identifier='admin-post-menu']")
|
||||
.doesNotExist();
|
||||
await click(".post-menu-area .show-post-admin-menu");
|
||||
|
||||
assert.dom("[data-content][data-identifier='admin-post-menu']").exists();
|
||||
|
||||
await triggerEvent(".post-menu-area", "pointerdown");
|
||||
@@ -612,11 +667,28 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
});
|
||||
|
||||
test("permanently delete topic", async function (assert) {
|
||||
this.set("args", { canManage: true, canPermanentlyDelete: true });
|
||||
this.currentUser.set("admin", true);
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", {
|
||||
id: 123,
|
||||
details: { can_permanently_delete: true },
|
||||
});
|
||||
const post = store.createRecord("post", {
|
||||
id: 1,
|
||||
post_number: 1,
|
||||
deleted_at: new Date().toISOString(),
|
||||
topic,
|
||||
});
|
||||
|
||||
this.set("args", { canManage: true });
|
||||
this.set("post", post);
|
||||
this.set("permanentlyDeletePost", () => (this.deleted = true));
|
||||
|
||||
await render(
|
||||
hbs`<MountWidget @widget="post" @args={{this.args}} @permanentlyDeletePost={{this.permanentlyDeletePost}} /><DMenus />`
|
||||
hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @model={{this.post}}
|
||||
@permanentlyDeletePost={{this.permanentlyDeletePost}} />
|
||||
<DMenus/>`
|
||||
);
|
||||
|
||||
await click(".post-menu-area .show-post-admin-menu");
|
||||
@@ -630,12 +702,27 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
});
|
||||
|
||||
test("permanently delete post", async function (assert) {
|
||||
this.set("args", { canManage: true, canPermanentlyDelete: true });
|
||||
this.currentUser.set("admin", true);
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", {
|
||||
id: 123,
|
||||
});
|
||||
const post = store.createRecord("post", {
|
||||
id: 1,
|
||||
post_number: 2,
|
||||
deleted_at: new Date().toISOString(),
|
||||
can_permanently_delete: true,
|
||||
topic,
|
||||
});
|
||||
|
||||
this.set("args", { canManage: true });
|
||||
this.set("post", post);
|
||||
this.set("permanentlyDeletePost", () => (this.deleted = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @permanentlyDeletePost={{this.permanentlyDeletePost}} />
|
||||
<DMenus />
|
||||
<MountWidget @widget="post" @args={{this.args}} @model={{this.post}}
|
||||
@permanentlyDeletePost={{this.permanentlyDeletePost}} />
|
||||
<DMenus/>
|
||||
`);
|
||||
|
||||
await click(".post-menu-area .show-post-admin-menu");
|
||||
@@ -651,13 +738,26 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
|
||||
test("toggle moderator post", async function (assert) {
|
||||
this.currentUser.set("moderator", true);
|
||||
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", {
|
||||
id: 123,
|
||||
});
|
||||
const post = store.createRecord("post", {
|
||||
id: 1,
|
||||
post_number: 2,
|
||||
deleted_at: new Date().toISOString(),
|
||||
can_permanently_delete: true,
|
||||
topic,
|
||||
});
|
||||
|
||||
this.set("args", { canManage: true });
|
||||
this.set("post", post);
|
||||
this.set("togglePostType", () => (this.toggled = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @togglePostType={{this.togglePostType}} />
|
||||
<DMenus />
|
||||
`);
|
||||
<MountWidget @widget="post" @args={{this.args}} @model={{this.post}} @togglePostType={{this.togglePostType}} />
|
||||
<DMenus/>`);
|
||||
|
||||
await click(".post-menu-area .show-post-admin-menu");
|
||||
await click(
|
||||
@@ -671,13 +771,22 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
});
|
||||
|
||||
test("rebake post", async function (assert) {
|
||||
this.currentUser.moderator = true;
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", { id: 123 });
|
||||
const post = store.createRecord("post", {
|
||||
id: 1,
|
||||
post_number: 1,
|
||||
topic,
|
||||
});
|
||||
|
||||
this.set("args", { canManage: true });
|
||||
this.set("post", post);
|
||||
this.set("rebakePost", () => (this.baked = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @rebakePost={{this.rebakePost}} />
|
||||
<DMenus />
|
||||
`);
|
||||
<MountWidget @widget="post" @args={{this.args}} @model={{this.post}} @rebakePost={{this.rebakePost}} />
|
||||
<DMenus/>`);
|
||||
|
||||
await click(".post-menu-area .show-post-admin-menu");
|
||||
await click(
|
||||
@@ -692,12 +801,22 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
test("unhide post", async function (assert) {
|
||||
let unhidden;
|
||||
this.currentUser.admin = true;
|
||||
this.set("args", { canManage: true, hidden: true });
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", { id: 123 });
|
||||
const post = store.createRecord("post", {
|
||||
id: 1,
|
||||
post_number: 1,
|
||||
hidden: true,
|
||||
topic,
|
||||
});
|
||||
|
||||
this.set("args", { canManage: true });
|
||||
this.set("post", post);
|
||||
this.set("unhidePost", () => (unhidden = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @unhidePost={{this.unhidePost}} />
|
||||
<DMenus />
|
||||
<MountWidget @widget="post" @args={{this.args}} @model={{this.post}} @unhidePost={{this.unhidePost}} />
|
||||
<DMenus/>
|
||||
`);
|
||||
|
||||
await click(".post-menu-area .show-post-admin-menu");
|
||||
@@ -715,12 +834,22 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
|
||||
test("change owner", async function (assert) {
|
||||
this.currentUser.admin = true;
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
const topic = store.createRecord("topic", { id: 123 });
|
||||
const post = store.createRecord("post", {
|
||||
id: 1,
|
||||
post_number: 1,
|
||||
hidden: true,
|
||||
topic,
|
||||
});
|
||||
|
||||
this.set("args", { canManage: true });
|
||||
this.set("post", post);
|
||||
this.set("changePostOwner", () => (this.owned = true));
|
||||
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} @changePostOwner={{this.changePostOwner}} />
|
||||
<DMenus />
|
||||
<MountWidget @widget="post" @args={{this.args}} @model={{this.post}} @changePostOwner={{this.changePostOwner}} />
|
||||
<DMenus/>
|
||||
`);
|
||||
|
||||
await click(".post-menu-area .show-post-admin-menu");
|
||||
@@ -733,6 +862,7 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
.doesNotExist("also hides the menu");
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test("reply", async function (assert) {
|
||||
this.set("args", { canCreatePost: true });
|
||||
this.set("replyToPost", () => (this.replied = true));
|
||||
@@ -745,45 +875,55 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
assert.ok(this.replied);
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test("reply - without permissions", async function (assert) {
|
||||
this.set("args", { canCreatePost: false });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".post-controls .create").doesNotExist();
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test("replies - no replies", async function (assert) {
|
||||
this.set("args", { replyCount: 0 });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom("button.show-replies").doesNotExist();
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test("replies - multiple replies", async function (assert) {
|
||||
this.siteSettings.suppress_reply_directly_below = true;
|
||||
this.set("args", { replyCount: 2, replyDirectlyBelow: true });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom("button.show-replies").exists();
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test("replies - one below, suppressed", async function (assert) {
|
||||
this.siteSettings.suppress_reply_directly_below = true;
|
||||
this.set("args", { replyCount: 1, replyDirectlyBelow: true });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom("button.show-replies").doesNotExist();
|
||||
});
|
||||
|
||||
// glimmer-post-menu: deprecated in favor of spec/system/post_menu_spec.rb
|
||||
test("replies - one below, not suppressed", async function (assert) {
|
||||
this.siteSettings.suppress_reply_directly_below = false;
|
||||
this.set("args", { id: 6654, replyCount: 1, replyDirectlyBelow: true });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
await click("button.show-replies");
|
||||
assert.dom("section.embedded-posts.bottom .cooked").exists();
|
||||
@@ -795,7 +935,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
const topic = store.createRecord("topic", { id: 123 });
|
||||
this.set("args", { topic, post_number: 1, topicMap: true });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".topic-map").exists();
|
||||
});
|
||||
@@ -810,7 +951,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
});
|
||||
this.set("args", { topic, post_number: 1 });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".topic-map").exists();
|
||||
});
|
||||
@@ -832,7 +974,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
post_number: 1,
|
||||
});
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
assert.dom(".topic-map__users-trigger").doesNotExist();
|
||||
assert.dom(".topic-map__users-list a.poster").exists({ count: 2 });
|
||||
});
|
||||
@@ -860,7 +1003,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
post_number: 1,
|
||||
});
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
assert.dom(".topic-map__users-list a.poster").exists({ count: 5 });
|
||||
|
||||
await click(".topic-map__users-trigger");
|
||||
@@ -886,7 +1030,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
]);
|
||||
this.set("args", { topic, post_number: 1 });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".topic-map").exists({ count: 1 });
|
||||
assert.dom(".topic-map__links-content").doesNotExist();
|
||||
@@ -906,7 +1051,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
});
|
||||
this.set("args", { topic, post_number: 1 });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".topic-map").exists();
|
||||
assert.dom(".summarization-button .top-replies").doesNotExist();
|
||||
@@ -922,7 +1068,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
});
|
||||
this.set("args", { topic, post_number: 1 });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".summarization-button .top-replies").exists({ count: 1 });
|
||||
});
|
||||
@@ -941,7 +1088,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
post_number: 1,
|
||||
});
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".topic-map__private-message-map").exists({ count: 1 });
|
||||
assert.dom(".topic-map__private-message-map .user").exists({ count: 1 });
|
||||
@@ -963,7 +1111,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
},
|
||||
});
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".post-notice.returning-user:not(.old)").hasText(
|
||||
I18n.t("post.notice.returning_user", {
|
||||
@@ -984,7 +1133,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
notice: { type: "new_user" },
|
||||
});
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert
|
||||
.dom(".post-notice.old.new-user")
|
||||
@@ -999,7 +1149,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
requestedGroupName: "testGroup",
|
||||
});
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
const link = query(".group-request a");
|
||||
assert.strictEqual(link.innerText.trim(), I18n.t("groups.requests.handle"));
|
||||
@@ -1018,7 +1169,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
const user = store.createRecord("user", { status });
|
||||
this.set("args", { user });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".user-status-message").exists();
|
||||
});
|
||||
@@ -1033,7 +1185,8 @@ module("Integration | Component | Widget | post", function (hooks) {
|
||||
const user = store.createRecord("user", { status });
|
||||
this.set("args", { user });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
await render(hbs`
|
||||
<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.dom(".user-status-message").doesNotExist();
|
||||
});
|
||||
|
||||
@@ -7,6 +7,23 @@ module("Unit | Lib | DAG", function (hooks) {
|
||||
|
||||
let dag;
|
||||
|
||||
test("DAG.from should create a DAG instance from the provided entries", function (assert) {
|
||||
dag = DAG.from([
|
||||
["key1", "value1", { after: "key2" }],
|
||||
["key2", "value2", { before: "key3" }],
|
||||
["key3", "value3", { before: "key1" }],
|
||||
]);
|
||||
|
||||
assert.ok(dag.has("key1"));
|
||||
assert.ok(dag.has("key2"));
|
||||
assert.ok(dag.has("key3"));
|
||||
|
||||
const resolved = dag.resolve();
|
||||
const keys = resolved.map((entry) => entry.key);
|
||||
|
||||
assert.deepEqual(keys, ["key2", "key3", "key1"]);
|
||||
});
|
||||
|
||||
test("should add items to the map", function (assert) {
|
||||
dag = new DAG();
|
||||
dag.add("key1", "value1");
|
||||
@@ -16,6 +33,58 @@ module("Unit | Lib | DAG", function (hooks) {
|
||||
assert.ok(dag.has("key1"));
|
||||
assert.ok(dag.has("key2"));
|
||||
assert.ok(dag.has("key3"));
|
||||
|
||||
// adding a new item
|
||||
assert.ok(
|
||||
dag.add("key4", "value4"),
|
||||
"adding an item returns true when the item is added"
|
||||
);
|
||||
assert.ok(dag.has("key4"));
|
||||
|
||||
// adding an item that already exists
|
||||
assert.notOk(
|
||||
dag.add("key1", "value1"),
|
||||
"adding an item returns false when the item already exists"
|
||||
);
|
||||
});
|
||||
|
||||
test("should not throw an error when throwErrorOnCycle is false when adding an item creates a cycle", function (assert) {
|
||||
dag = new DAG({
|
||||
throwErrorOnCycle: false,
|
||||
defaultPosition: { before: "key3" },
|
||||
});
|
||||
|
||||
dag.add("key1", "value1", { after: "key2" });
|
||||
dag.add("key2", "value2", { after: "key3" });
|
||||
|
||||
// This would normally cause a cycle if throwErrorOnCycle was true
|
||||
dag.add("key3", "value3", { after: "key1" });
|
||||
|
||||
assert.ok(dag.has("key1"));
|
||||
assert.ok(dag.has("key2"));
|
||||
assert.ok(dag.has("key3"));
|
||||
|
||||
const resolved = dag.resolve();
|
||||
const keys = resolved.map((entry) => entry.key);
|
||||
|
||||
// Check that the default position was used to avoid the cycle
|
||||
assert.deepEqual(keys, ["key3", "key2", "key1"]);
|
||||
});
|
||||
|
||||
test("should call the method specified for onAddItem callback when an item is added", function (assert) {
|
||||
let called = 0;
|
||||
|
||||
dag = new DAG({
|
||||
onAddItem: () => {
|
||||
called++;
|
||||
},
|
||||
});
|
||||
dag.add("key1", "value1");
|
||||
assert.equal(called, 1, "the callback was called");
|
||||
|
||||
// it doesn't call the callback when the item already exists
|
||||
dag.add("key1", "value1");
|
||||
assert.equal(called, 1, "the callback was not called");
|
||||
});
|
||||
|
||||
test("should remove an item from the map", function (assert) {
|
||||
@@ -24,11 +93,80 @@ module("Unit | Lib | DAG", function (hooks) {
|
||||
dag.add("key2", "value2");
|
||||
dag.add("key3", "value3");
|
||||
|
||||
dag.delete("key2");
|
||||
let removed = dag.delete("key2");
|
||||
|
||||
assert.ok(dag.has("key1"));
|
||||
assert.false(dag.has("key2"));
|
||||
assert.ok(dag.has("key3"));
|
||||
|
||||
assert.ok(removed, "delete returns true when the item is removed");
|
||||
|
||||
removed = dag.delete("key2");
|
||||
assert.notOk(removed, "delete returns false when the item doesn't exist");
|
||||
});
|
||||
|
||||
test("should call the method specified for onDeleteItem callback when an item is removed", function (assert) {
|
||||
let called = 0;
|
||||
|
||||
dag = new DAG({
|
||||
onDeleteItem: () => {
|
||||
called++;
|
||||
},
|
||||
});
|
||||
dag.add("key1", "value1");
|
||||
dag.delete("key1");
|
||||
assert.equal(called, 1, "the callback was called");
|
||||
|
||||
// it doesn't call the callback when the item doesn't exist
|
||||
dag.delete("key1");
|
||||
assert.equal(called, 1, "the callback was not called");
|
||||
});
|
||||
|
||||
test("should replace the value from an item in the map", function (assert) {
|
||||
dag = new DAG();
|
||||
dag.add("key1", "value1");
|
||||
dag.add("key2", "value2");
|
||||
dag.add("key3", "value3");
|
||||
|
||||
// simply replacing the value
|
||||
let replaced = dag.replace("key2", "replaced-value2");
|
||||
|
||||
assert.deepEqual(
|
||||
dag.resolve().map((entry) => entry.value),
|
||||
["value1", "replaced-value2", "value3"],
|
||||
"replace allows simply replacing the value"
|
||||
);
|
||||
assert.ok(replaced, "replace returns true when the item is replaced");
|
||||
|
||||
// also changing the position
|
||||
dag.replace("key2", "replaced-value2-again", { before: "key1" });
|
||||
|
||||
assert.deepEqual(
|
||||
dag.resolve().map((entry) => entry.value),
|
||||
["replaced-value2-again", "value1", "value3"],
|
||||
"replace also allows changing the position"
|
||||
);
|
||||
|
||||
// replacing an item that doesn't exist
|
||||
replaced = dag.replace("key4", "replaced-value4");
|
||||
assert.notOk(replaced, "replace returns false when the item doesn't exist");
|
||||
});
|
||||
|
||||
test("should call the method specified for onReplaceItem callback when an item is replaced", function (assert) {
|
||||
let called = 0;
|
||||
|
||||
dag = new DAG({
|
||||
onReplaceItem: () => {
|
||||
called++;
|
||||
},
|
||||
});
|
||||
dag.add("key1", "value1");
|
||||
dag.replace("key1", "replaced-value1");
|
||||
assert.equal(called, 1, "the callback was called");
|
||||
|
||||
// it doesn't call the callback when the item doesn't exist
|
||||
dag.replace("key2", "replaced-value2");
|
||||
assert.equal(called, 1, "the callback was not called");
|
||||
});
|
||||
|
||||
test("should reposition an item in the map", function (assert) {
|
||||
@@ -37,12 +175,66 @@ module("Unit | Lib | DAG", function (hooks) {
|
||||
dag.add("key2", "value2");
|
||||
dag.add("key3", "value3");
|
||||
|
||||
dag.reposition("key3", { before: "key1" });
|
||||
let repositioned = dag.reposition("key3", { before: "key1" });
|
||||
assert.ok(
|
||||
repositioned,
|
||||
"reposition returns true when the item is repositioned"
|
||||
);
|
||||
|
||||
const resolved = dag.resolve();
|
||||
const keys = resolved.map((pair) => pair.key);
|
||||
const keys = resolved.map((entry) => entry.key);
|
||||
|
||||
assert.deepEqual(keys, ["key3", "key1", "key2"]);
|
||||
|
||||
// repositioning an item that doesn't exist
|
||||
repositioned = dag.reposition("key4", { before: "key1" });
|
||||
assert.notOk(
|
||||
repositioned,
|
||||
"reposition returns false when the item doesn't exist"
|
||||
);
|
||||
});
|
||||
|
||||
test("should call the method specified for onRepositionItem callback when an item is repositioned", function (assert) {
|
||||
let called = 0;
|
||||
|
||||
dag = new DAG({
|
||||
onRepositionItem: () => {
|
||||
called++;
|
||||
},
|
||||
});
|
||||
dag.add("key1", "value1");
|
||||
dag.reposition("key1", { before: "key2" });
|
||||
assert.equal(called, 1, "the callback was called");
|
||||
|
||||
// it doesn't call the callback when the item doesn't exist
|
||||
dag.reposition("key2", { before: "key1" });
|
||||
assert.equal(called, 1, "the callback was not called");
|
||||
});
|
||||
|
||||
test("should return the entries in the map", function (assert) {
|
||||
const entries = [
|
||||
["key1", "value1", { after: "key2" }],
|
||||
["key2", "value2", { before: "key3" }],
|
||||
["key3", "value3", { before: "key1" }],
|
||||
];
|
||||
|
||||
dag = DAG.from(entries);
|
||||
const dagEntries = dag.entries();
|
||||
|
||||
entries.forEach((entry, index) => {
|
||||
assert.equal(dagEntries[index][0], entry[0], "the key is correct");
|
||||
assert.equal(dagEntries[index][1], entry[1], "the value is correct");
|
||||
assert.equal(
|
||||
dagEntries[index][2]["before"],
|
||||
entry[2]["before"],
|
||||
"the before position is correct"
|
||||
);
|
||||
assert.equal(
|
||||
dagEntries[index][2]["after"],
|
||||
entry[2]["after"],
|
||||
"the after position is correct"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("should resolve the map in the correct order", function (assert) {
|
||||
@@ -52,7 +244,7 @@ module("Unit | Lib | DAG", function (hooks) {
|
||||
dag.add("key3", "value3");
|
||||
|
||||
const resolved = dag.resolve();
|
||||
const keys = resolved.map((pair) => pair.key);
|
||||
const keys = resolved.map((entry) => entry.key);
|
||||
|
||||
assert.deepEqual(keys, ["key1", "key2", "key3"]);
|
||||
});
|
||||
@@ -65,9 +257,20 @@ module("Unit | Lib | DAG", function (hooks) {
|
||||
dag.add("key4", "value4");
|
||||
|
||||
const resolved = dag.resolve();
|
||||
const keys = resolved.map((pair) => pair.key);
|
||||
const keys = resolved.map((entry) => entry.key);
|
||||
|
||||
assert.deepEqual(keys, ["key1", "key2", "key4", "key3"]);
|
||||
|
||||
// it also returns the positioning data for each entry
|
||||
assert.deepEqual(
|
||||
resolved.map((entry) => entry.position),
|
||||
[
|
||||
{ before: undefined, after: undefined }, // {} from key1
|
||||
{ before: undefined, after: "key1" }, // from key2
|
||||
{ before: "key3", after: "key2" }, // from the defaultPosition applied to key4
|
||||
{ before: undefined, after: "key2" }, // from key3
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test("should resolve only existing keys", function (assert) {
|
||||
@@ -79,7 +282,7 @@ module("Unit | Lib | DAG", function (hooks) {
|
||||
dag.delete("key1");
|
||||
|
||||
const resolved = dag.resolve();
|
||||
const keys = resolved.map((pair) => pair.key);
|
||||
const keys = resolved.map((entry) => entry.key);
|
||||
|
||||
assert.deepEqual(keys, ["key2", "key3"]);
|
||||
});
|
||||
|
||||
@@ -122,7 +122,11 @@ module("Unit | Utility | transformers", function (hooks) {
|
||||
|
||||
test("warns if transformer is unknown", function (assert) {
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
api.registerValueTransformer("whatever", () => "foo");
|
||||
const result = api.registerValueTransformer("whatever", () => "foo");
|
||||
assert.notOk(
|
||||
result,
|
||||
"registerValueTransformer returns false if the transformer name does not exist"
|
||||
);
|
||||
|
||||
// testing warning about core transformers
|
||||
assert.strictEqual(
|
||||
@@ -138,7 +142,7 @@ module("Unit | Utility | transformers", function (hooks) {
|
||||
assert.throws(
|
||||
() =>
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
api.registerValueTransformer("whatever", "foo");
|
||||
api.registerValueTransformer("home-logo-href", "foo");
|
||||
}),
|
||||
/api.registerValueTransformer requires the callback argument to be a function/
|
||||
);
|
||||
@@ -160,7 +164,14 @@ module("Unit | Utility | transformers", function (hooks) {
|
||||
"value did not change. transformer is not registered yet"
|
||||
);
|
||||
|
||||
api.registerValueTransformer("test-transformer", () => true);
|
||||
const result = api.registerValueTransformer(
|
||||
"test-transformer",
|
||||
() => true
|
||||
);
|
||||
assert.ok(
|
||||
result,
|
||||
"registerValueTransformer returns true if the transformer was registered"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
transformerWasRegistered("test-transformer"),
|
||||
@@ -656,9 +667,13 @@ module("Unit | Utility | transformers", function (hooks) {
|
||||
);
|
||||
});
|
||||
|
||||
test("warns if transformer is unknown", function (assert) {
|
||||
test("warns if transformer is unknown ans returns false", function (assert) {
|
||||
withPluginApi("1.35.0", (api) => {
|
||||
api.registerBehaviorTransformer("whatever", () => "foo");
|
||||
const result = api.registerBehaviorTransformer("whatever", () => "foo");
|
||||
assert.notOk(
|
||||
result,
|
||||
"registerBehaviorTransformer returns false if the transformer name does not exist"
|
||||
);
|
||||
|
||||
// testing warning about core transformers
|
||||
assert.strictEqual(
|
||||
@@ -674,7 +689,10 @@ module("Unit | Utility | transformers", function (hooks) {
|
||||
assert.throws(
|
||||
() =>
|
||||
withPluginApi("1.35.0", (api) => {
|
||||
api.registerBehaviorTransformer("whatever", "foo");
|
||||
api.registerBehaviorTransformer(
|
||||
"discovery-topic-list-load-more",
|
||||
"foo"
|
||||
);
|
||||
}),
|
||||
/api.registerBehaviorTransformer requires the callback argument to be a function/
|
||||
);
|
||||
@@ -707,8 +725,13 @@ module("Unit | Utility | transformers", function (hooks) {
|
||||
"value was set by the default callback. transformer is not registered yet"
|
||||
);
|
||||
|
||||
api.registerBehaviorTransformer("test-transformer", ({ context }) =>
|
||||
context.setValue("TRANSFORMED_CALLBACK")
|
||||
const result = api.registerBehaviorTransformer(
|
||||
"test-transformer",
|
||||
({ context }) => context.setValue("TRANSFORMED_CALLBACK")
|
||||
);
|
||||
assert.ok(
|
||||
result,
|
||||
"registerBehaviorTransformer returns true if the transformer was registered"
|
||||
);
|
||||
|
||||
transformerWasRegistered("test-transformer");
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:before {
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
animation: placeHolderShimmer 4s linear infinite forwards;
|
||||
@@ -53,6 +54,7 @@
|
||||
.names {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
|
||||
span.first {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
@@ -65,19 +67,23 @@
|
||||
display: inline-block;
|
||||
@include ellipsis;
|
||||
vertical-align: middle;
|
||||
|
||||
a {
|
||||
color: var(--primary-high-or-secondary-low);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.fa {
|
||||
font-size: var(--font-down-1);
|
||||
color: var(--primary-med-or-secondary-med);
|
||||
}
|
||||
|
||||
.svg-icon-title {
|
||||
margin-left: 3px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.new_user a,
|
||||
.user-title,
|
||||
.user-title a {
|
||||
@@ -103,9 +109,11 @@
|
||||
h6 {
|
||||
margin: 2rem 0 0.67rem;
|
||||
line-height: var(--line-height-medium);
|
||||
|
||||
a.anchor {
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s;
|
||||
|
||||
&:before {
|
||||
content: svg-uri(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 512 512" fill="#{$primary-medium}"><path d="M326.612 185.391c59.747 59.809 58.927 155.698.36 214.59-.11.12-.24.25-.36.37l-67.2 67.2c-59.27 59.27-155.699 59.262-214.96 0-59.27-59.26-59.27-155.7 0-214.96l37.106-37.106c9.84-9.84 26.786-3.3 27.294 10.606.648 17.722 3.826 35.527 9.69 52.721 1.986 5.822.567 12.262-3.783 16.612l-13.087 13.087c-28.026 28.026-28.905 73.66-1.155 101.96 28.024 28.579 74.086 28.749 102.325.51l67.2-67.19c28.191-28.191 28.073-73.757 0-101.83-3.701-3.694-7.429-6.564-10.341-8.569a16.037 16.037 0 0 1-6.947-12.606c-.396-10.567 3.348-21.456 11.698-29.806l21.054-21.055c5.521-5.521 14.182-6.199 20.584-1.731a152.482 152.482 0 0 1 20.522 17.197zM467.547 44.449c-59.261-59.262-155.69-59.27-214.96 0l-67.2 67.2c-.12.12-.25.25-.36.37-58.566 58.892-59.387 154.781.36 214.59a152.454 152.454 0 0 0 20.521 17.196c6.402 4.468 15.064 3.789 20.584-1.731l21.054-21.055c8.35-8.35 12.094-19.239 11.698-29.806a16.037 16.037 0 0 0-6.947-12.606c-2.912-2.005-6.64-4.875-10.341-8.569-28.073-28.073-28.191-73.639 0-101.83l67.2-67.19c28.239-28.239 74.3-28.069 102.325.51 27.75 28.3 26.872 73.934-1.155 101.96l-13.087 13.087c-4.35 4.35-5.769 10.79-3.783 16.612 5.864 17.194 9.042 34.999 9.69 52.721.509 13.906 17.454 20.446 27.294 10.606l37.106-37.106c59.271-59.259 59.271-155.699.001-214.959z"></path></svg>'
|
||||
@@ -162,29 +170,37 @@
|
||||
background-color: var(--success-low);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
del {
|
||||
background-color: var(--danger-low);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
mark {
|
||||
background-color: var(--highlight);
|
||||
}
|
||||
|
||||
// Prevents users from breaking posts with tag nesting
|
||||
big {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
small small {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
big big {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
sub sub sub {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
sup sup sup {
|
||||
top: 0;
|
||||
}
|
||||
@@ -195,6 +211,7 @@
|
||||
video {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
sup sup {
|
||||
top: 0;
|
||||
}
|
||||
@@ -220,9 +237,11 @@
|
||||
.regular > .cooked {
|
||||
background-color: var(--highlight-bg);
|
||||
}
|
||||
|
||||
.clearfix > .topic-meta-data > .names {
|
||||
span.user-title {
|
||||
color: var(--primary-high-or-secondary-low);
|
||||
|
||||
a {
|
||||
background-color: var(--highlight-bg);
|
||||
padding-left: 4px;
|
||||
@@ -248,6 +267,7 @@
|
||||
|
||||
.user-status-message {
|
||||
display: flex;
|
||||
|
||||
img.emoji {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
@@ -260,6 +280,7 @@ nav.post-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
color: var(--primary-low-mid);
|
||||
|
||||
.actions {
|
||||
@@ -273,11 +294,13 @@ nav.post-controls {
|
||||
flex: 0 1 auto;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
|
||||
button {
|
||||
// It looks really confusing when one half a double button has an inner shadow on click.
|
||||
&:active {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.my-likes,
|
||||
&.read-indicator,
|
||||
&.regular-likes {
|
||||
@@ -287,22 +310,26 @@ nav.post-controls {
|
||||
padding-left: 0.45em;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-like {
|
||||
// Like button after I've liked
|
||||
.d-icon {
|
||||
color: var(--love);
|
||||
}
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
// Disabled like button
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.button-count {
|
||||
// Like count button
|
||||
height: 100%; // sometimes the font might be shorter than the icon
|
||||
&:not(.my-likes) {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
+ .toggle-like {
|
||||
// Like button when like count is present
|
||||
padding-left: 0.45em;
|
||||
@@ -310,12 +337,15 @@ nav.post-controls {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a,
|
||||
button {
|
||||
color: var(--primary-low-mid-or-secondary-high);
|
||||
|
||||
.d-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
@@ -326,39 +356,51 @@ nav.post-controls {
|
||||
vertical-align: top;
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
||||
.d-icon {
|
||||
// this avoids an issue where hovering off the icon
|
||||
// removes the .d-hover class from the button prematurely
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.d-hover,
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
outline: none;
|
||||
background: var(--primary-low);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.admin {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// TODO (glimmer-post-menu): Go over the the d-hover style and remove the unnecessary ones when glimmer-post-menu replaces the widget version
|
||||
&.delete.d-hover,
|
||||
&.delete:hover,
|
||||
&.delete:active,
|
||||
&.delete:focus {
|
||||
background: var(--danger);
|
||||
color: var(--secondary);
|
||||
|
||||
.d-icon {
|
||||
color: var(--secondary);
|
||||
}
|
||||
}
|
||||
|
||||
&.bookmarked .d-icon {
|
||||
color: var(--tertiary);
|
||||
}
|
||||
|
||||
&.create {
|
||||
margin-right: 0;
|
||||
color: var(--primary-high-or-secondary-low);
|
||||
|
||||
.d-icon {
|
||||
color: var(--primary-high-or-secondary-low);
|
||||
}
|
||||
@@ -371,15 +413,18 @@ nav.post-controls {
|
||||
font-size: inherit;
|
||||
padding: 10px;
|
||||
color: var(--primary-medium);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
outline: none;
|
||||
color: var(--primary);
|
||||
background: var(--primary-low);
|
||||
}
|
||||
|
||||
.d-icon {
|
||||
font-size: var(--font-down-1);
|
||||
}
|
||||
|
||||
.d-button-label + .d-icon {
|
||||
margin-left: 0.45em;
|
||||
margin-right: 0;
|
||||
@@ -391,6 +436,7 @@ nav.post-controls {
|
||||
.regular > .cooked {
|
||||
background-color: var(--danger-low-mid);
|
||||
}
|
||||
|
||||
.topic-meta-data:not(.embedded-reply) {
|
||||
color: var(--danger);
|
||||
|
||||
@@ -403,36 +449,44 @@ nav.post-controls {
|
||||
color: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
nav.post-controls {
|
||||
color: var(--danger);
|
||||
|
||||
.show-replies,
|
||||
button.reply.create {
|
||||
color: var(--danger);
|
||||
|
||||
.d-icon {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
|
||||
.widget-button {
|
||||
&:hover {
|
||||
color: currentColor;
|
||||
background: var(--danger-low);
|
||||
|
||||
.d-icon {
|
||||
color: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
&[disabled]:hover {
|
||||
background-color: transparent;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.fade-out {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.d-icon {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
|
||||
.post-action {
|
||||
color: var(--danger);
|
||||
}
|
||||
@@ -450,7 +504,9 @@ nav.post-controls {
|
||||
}
|
||||
}
|
||||
|
||||
.has-like .d-icon.heart-animation {
|
||||
// .d-icon.heart-animation is the widget animation while .toggle-like.heart-animation is the glimmer-post-menu's
|
||||
.has-like .d-icon.heart-animation,
|
||||
.toggle-like.heart-animation .d-icon {
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
animation: heartBump 0.4s;
|
||||
}
|
||||
@@ -480,20 +536,24 @@ aside.quote {
|
||||
.avatar {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-top: -0.26em;
|
||||
}
|
||||
|
||||
@include unselectable;
|
||||
}
|
||||
|
||||
// blockquote is docked within aside for content
|
||||
blockquote {
|
||||
margin-top: 0;
|
||||
|
||||
.expanded-quote {
|
||||
overflow: hidden;
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
animation: slideout 1s ease-in-out;
|
||||
}
|
||||
|
||||
&.icon-only {
|
||||
text-align: center;
|
||||
font-size: var(--font-up-4);
|
||||
@@ -528,6 +588,7 @@ aside.quote {
|
||||
|
||||
.post-action {
|
||||
color: var(--primary-medium);
|
||||
|
||||
.undo-action,
|
||||
.act-action {
|
||||
margin-left: 5px;
|
||||
@@ -570,6 +631,7 @@ aside.quote {
|
||||
float: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
a {
|
||||
margin-left: 0.3em;
|
||||
}
|
||||
@@ -660,11 +722,13 @@ aside.quote {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quote-share-label + .quote-share-buttons {
|
||||
max-width: 10em;
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
// this pseudo element creates a transition buffer zone
|
||||
// without it, the width change on hover can cause transition jitter
|
||||
// the width is roughly wide enough to cover long translations of "share"
|
||||
@@ -712,6 +776,7 @@ aside.quote {
|
||||
right: -6px;
|
||||
}
|
||||
}
|
||||
|
||||
.topic-avatar .avatar-flair,
|
||||
.avatar-flair-preview .avatar-flair,
|
||||
.collapsed-info .user-profile-avatar .avatar-flair,
|
||||
@@ -720,6 +785,7 @@ aside.quote {
|
||||
background-size: 20px 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
&.rounded {
|
||||
background-size: 18px 18px;
|
||||
border-radius: 12px;
|
||||
@@ -729,11 +795,13 @@ aside.quote {
|
||||
right: -8px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-card-avatar .avatar-flair,
|
||||
.user-profile-avatar .avatar-flair {
|
||||
background-size: 40px 40px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
&.rounded {
|
||||
background-size: 30px 30px;
|
||||
border-radius: 24px;
|
||||
@@ -742,6 +810,7 @@ aside.quote {
|
||||
bottom: -2px;
|
||||
right: -4px;
|
||||
}
|
||||
|
||||
.fa {
|
||||
font-size: var(--font-up-4);
|
||||
}
|
||||
@@ -758,6 +827,7 @@ aside.quote {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.group-request {
|
||||
border-top: 1px solid var(--primary-low);
|
||||
padding-top: 0.5em;
|
||||
@@ -789,6 +859,7 @@ aside.quote {
|
||||
&.post-date {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&.via-email,
|
||||
&.whisper,
|
||||
&.post-locked {
|
||||
@@ -805,32 +876,39 @@ aside.quote {
|
||||
&.via-email {
|
||||
color: var(--primary-low-mid-or-secondary-high);
|
||||
}
|
||||
|
||||
&.raw-email {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.edits {
|
||||
.widget-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.d-button-label {
|
||||
order: 0;
|
||||
padding-right: 0.25em;
|
||||
color: var(--primary-med-or-secondary-med);
|
||||
}
|
||||
|
||||
.d-icon {
|
||||
order: 1;
|
||||
color: var(--primary-med-or-secondary-med);
|
||||
}
|
||||
|
||||
.discourse-no-touch & {
|
||||
&:hover {
|
||||
.d-button-label {
|
||||
color: var(--primary-high);
|
||||
}
|
||||
|
||||
.d-icon {
|
||||
color: var(--primary-high);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@include default-focus;
|
||||
background: transparent;
|
||||
@@ -841,6 +919,7 @@ aside.quote {
|
||||
|
||||
pre {
|
||||
max-height: 2000px;
|
||||
|
||||
code {
|
||||
word-wrap: normal;
|
||||
display: block;
|
||||
@@ -882,6 +961,7 @@ pre {
|
||||
|
||||
&.action-complete {
|
||||
cursor: auto;
|
||||
|
||||
.d-icon {
|
||||
color: var(--tertiary);
|
||||
}
|
||||
@@ -924,6 +1004,7 @@ kbd {
|
||||
blockquote > *:first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
blockquote > *:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
@@ -941,6 +1022,7 @@ blockquote > *:last-child {
|
||||
.who-liked,
|
||||
.who-read {
|
||||
transition: height 0.5s;
|
||||
|
||||
a {
|
||||
margin: 0 0.25em 0.5em 0;
|
||||
display: inline-block;
|
||||
@@ -993,6 +1075,7 @@ blockquote > *:last-child {
|
||||
width: var(--topic-avatar-width);
|
||||
justify-content: center;
|
||||
height: auto;
|
||||
|
||||
.d-icon {
|
||||
font-size: var(--font-up-3);
|
||||
width: var(--topic-avatar-width);
|
||||
@@ -1037,6 +1120,7 @@ blockquote > *:last-child {
|
||||
word-break: break-word;
|
||||
min-width: 0; // Allows content like oneboxes to shrink
|
||||
color: var(--primary);
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -1045,6 +1129,7 @@ blockquote > *:last-child {
|
||||
button {
|
||||
background: transparent;
|
||||
font-size: var(--font-down-1);
|
||||
|
||||
.d-icon {
|
||||
color: var(--primary-500);
|
||||
}
|
||||
@@ -1053,6 +1138,7 @@ blockquote > *:last-child {
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: var(--primary-200);
|
||||
|
||||
.d-icon {
|
||||
color: var(--primary);
|
||||
}
|
||||
@@ -1074,6 +1160,7 @@ blockquote > *:last-child {
|
||||
var(--topic-body-width) + var(--topic-avatar-width) +
|
||||
(var(--topic-body-width-padding) * 2)
|
||||
);
|
||||
|
||||
.topic-post-visited-message {
|
||||
background-color: var(--secondary);
|
||||
padding: 0 0.5em;
|
||||
@@ -1086,6 +1173,7 @@ blockquote > *:last-child {
|
||||
.post-info.whisper {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.topic-body {
|
||||
.cooked {
|
||||
font-style: italic;
|
||||
@@ -1210,6 +1298,7 @@ span.mention {
|
||||
border-color: var(--tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.topic-body {
|
||||
.contents:after {
|
||||
display: none;
|
||||
@@ -1217,14 +1306,17 @@ span.mention {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
article.boxed {
|
||||
position: relative;
|
||||
|
||||
.select-posts {
|
||||
position: absolute;
|
||||
right: 7em;
|
||||
z-index: z("dropdown");
|
||||
top: 0.5em;
|
||||
height: 100px;
|
||||
|
||||
button {
|
||||
margin-left: 8px;
|
||||
background-color: var(--primary-low);
|
||||
@@ -1255,20 +1347,25 @@ span.mention {
|
||||
@include breakpoint(mobile-extra-large) {
|
||||
padding: 1.5em 1.5em 0.25em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin: 1.5em 0 1em;
|
||||
|
||||
> * {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-right: 0.5em;
|
||||
white-space: nowrap;
|
||||
@@ -1284,6 +1381,7 @@ span.mention {
|
||||
background: var(--primary-medium);
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint(mobile-extra-large) {
|
||||
&.btn-primary {
|
||||
margin-right: 100%;
|
||||
@@ -1306,6 +1404,7 @@ span.mention {
|
||||
);
|
||||
padding: var(--topic-body-width-padding);
|
||||
padding-left: 0;
|
||||
|
||||
&.old {
|
||||
background-color: unset;
|
||||
color: var(--primary-medium);
|
||||
@@ -1344,11 +1443,14 @@ html.anon #topic-footer-buttons {
|
||||
details {
|
||||
margin-right: 0.54em;
|
||||
}
|
||||
|
||||
> * {
|
||||
margin-bottom: 0.5em; // all immediate children should have a bottom margin in case of wrapping
|
||||
}
|
||||
|
||||
.topic-admin-menu-button-container {
|
||||
display: inline-flex;
|
||||
|
||||
.topic-admin-menu-button {
|
||||
display: flex; // to make this button match siblings behavior, all of its parents need to be flex
|
||||
}
|
||||
@@ -1374,6 +1476,7 @@ html.anon #topic-footer-buttons {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.name {
|
||||
@include ellipsis;
|
||||
}
|
||||
@@ -1423,6 +1526,7 @@ iframe {
|
||||
|
||||
.filtered-avatar {
|
||||
margin: 0 0.5em;
|
||||
|
||||
+ .names {
|
||||
flex: inherit;
|
||||
}
|
||||
@@ -1458,6 +1562,7 @@ iframe {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
|
||||
> .d-icon {
|
||||
cursor: pointer;
|
||||
padding: 0.5em;
|
||||
@@ -1523,8 +1628,10 @@ iframe {
|
||||
.d-modal__container {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.d-modal__body {
|
||||
padding-top: 0;
|
||||
|
||||
thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@@ -1542,6 +1649,7 @@ html.discourse-no-touch .fullscreen-table-wrapper:hover {
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 5px 0 rgba(var(--always-black-rgb), 0.1),
|
||||
0 2px 10px 0 rgba(var(--always-black-rgb), 0.1);
|
||||
|
||||
.open-popup-link {
|
||||
opacity: 100%;
|
||||
}
|
||||
@@ -1554,6 +1662,7 @@ html.discourse-no-touch .fullscreen-table-wrapper:hover {
|
||||
color: var(--tertiary-medium);
|
||||
right: 0;
|
||||
font-size: 0.571em;
|
||||
|
||||
&.read {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
|
||||
@@ -12,23 +12,28 @@
|
||||
|
||||
.topic-body {
|
||||
padding: 0;
|
||||
|
||||
&:first-of-type {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.reply-to-tab {
|
||||
z-index: z("base") + 1;
|
||||
color: var(--primary-med-or-secondary-med);
|
||||
}
|
||||
|
||||
.actions .fade-out {
|
||||
.discourse-no-touch & {
|
||||
opacity: 0.7;
|
||||
transition: background 0.25s, opacity 0.7s ease-in-out;
|
||||
animation: none; // replaces jsuites animation on .fade-out
|
||||
}
|
||||
|
||||
.discourse-touch & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .actions .fade-out,
|
||||
.selected .actions .fade-out {
|
||||
opacity: 1;
|
||||
@@ -48,54 +53,71 @@ nav.post-controls {
|
||||
// for consistency, try to control spacing by editing these variables
|
||||
--control-margin: 0.33em;
|
||||
--control-icon-space: 0.33em;
|
||||
|
||||
.actions {
|
||||
button {
|
||||
margin-left: var(--control-margin);
|
||||
|
||||
&.btn-icon-text,
|
||||
&.create {
|
||||
margin-left: calc(var(--control-margin) * 1.52);
|
||||
|
||||
.d-icon {
|
||||
margin-right: var(--control-icon-space);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Some buttons can be doubled up, like likes or flags
|
||||
.double-button {
|
||||
margin-left: var(--control-margin);
|
||||
|
||||
&:hover {
|
||||
button {
|
||||
background: var(--primary-low);
|
||||
color: var(--primary-medium);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
|
||||
&.like {
|
||||
// Like button with 0 likes
|
||||
&.d-hover {
|
||||
&.d-hover,
|
||||
&:hover {
|
||||
background: var(--love-low);
|
||||
|
||||
.d-icon {
|
||||
color: var(--love);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.has-like {
|
||||
// Like button after I've liked
|
||||
&.d-hover {
|
||||
&.d-hover,
|
||||
&:hover {
|
||||
background: var(--primary-low);
|
||||
|
||||
.d-icon {
|
||||
color: var(--primary-medium);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.button-count {
|
||||
// Like count button
|
||||
&.d-hover {
|
||||
&.d-hover,
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
+ .toggle-like {
|
||||
// Like button when like count is present
|
||||
&.d-hover {
|
||||
&.d-hover,
|
||||
&:hover {
|
||||
background: var(--primary-low);
|
||||
}
|
||||
}
|
||||
@@ -103,27 +125,34 @@ nav.post-controls {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.show-replies {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 0;
|
||||
border-radius: var(--d-button-border-radius);
|
||||
|
||||
.topic-post & {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
.d-icon {
|
||||
margin-inline: var(--control-icon-space);
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&[aria-expanded="true"] {
|
||||
background: var(--primary-low);
|
||||
color: var(--primary-high);
|
||||
box-shadow: 0px 0px 0px 1px var(--primary-300);
|
||||
z-index: 1;
|
||||
|
||||
.d-icon {
|
||||
color: var(--primary-high);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: var(--primary-300);
|
||||
@@ -147,6 +176,7 @@ pre.codeblock-buttons:hover {
|
||||
.fullscreen-cmd {
|
||||
opacity: 0.7;
|
||||
visibility: visible;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -159,22 +189,28 @@ pre.codeblock-buttons:hover {
|
||||
h3 {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
border: 1px solid var(--primary-low);
|
||||
|
||||
.topic-body {
|
||||
box-sizing: border-box;
|
||||
width: calc(100% - 70px); // [100% - .topic-avatar width]
|
||||
// WARNING: overflow hide is required for quoted / embedded images
|
||||
// which expect "normal" post width, but expansions are narrower
|
||||
overflow: hidden;
|
||||
} // this is covered by .topic-body .regular on a normal post
|
||||
}
|
||||
|
||||
// this is covered by .topic-body .regular on a normal post
|
||||
// but no such class structure exists for an embedded, expanded post
|
||||
.cooked {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.topic-avatar {
|
||||
padding-left: 25px;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.collapse-down,
|
||||
.collapse-up {
|
||||
position: absolute;
|
||||
@@ -186,10 +222,12 @@ pre.codeblock-buttons:hover {
|
||||
.d-icon {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.discourse-no-touch & {
|
||||
&:hover {
|
||||
background: var(--primary-low);
|
||||
color: var(--primary-high);
|
||||
|
||||
.d-icon {
|
||||
color: currentColor;
|
||||
}
|
||||
@@ -203,10 +241,13 @@ pre.codeblock-buttons:hover {
|
||||
max-width: calc(100% - 66px);
|
||||
margin-bottom: 30px;
|
||||
border: none;
|
||||
|
||||
> div {
|
||||
position: relative;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
|
||||
.row {
|
||||
// Main reply line
|
||||
&:before {
|
||||
@@ -220,6 +261,7 @@ pre.codeblock-buttons:hover {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
padding-bottom: 0.5em;
|
||||
// Main reply line
|
||||
@@ -232,28 +274,36 @@ pre.codeblock-buttons:hover {
|
||||
background: var(--primary-300);
|
||||
left: 32px;
|
||||
}
|
||||
|
||||
.topic-avatar {
|
||||
border-top: none;
|
||||
padding-left: 9px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.topic-body {
|
||||
border-top: none;
|
||||
padding-bottom: 2.5em;
|
||||
|
||||
.topic-meta-data {
|
||||
position: unset;
|
||||
|
||||
.post-link-arrow {
|
||||
position: absolute;
|
||||
bottom: 0.75em;
|
||||
|
||||
.archetype-private_message & {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.post-info.arrow {
|
||||
display: block;
|
||||
margin-right: 0;
|
||||
|
||||
.d-icon {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--primary-high);
|
||||
@@ -261,17 +311,20 @@ pre.codeblock-buttons:hover {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cooked {
|
||||
margin-top: 0.25em;
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-up {
|
||||
transform: translate(-50%, -164%);
|
||||
background: var(--primary-low);
|
||||
@@ -281,12 +334,15 @@ pre.codeblock-buttons:hover {
|
||||
left: 32px;
|
||||
bottom: -3em;
|
||||
z-index: 1;
|
||||
|
||||
.archetype-private_message & {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.d-icon {
|
||||
transform: scale(0.871);
|
||||
}
|
||||
|
||||
.discourse-no-touch & {
|
||||
&:hover,
|
||||
&:focus {
|
||||
@@ -295,6 +351,7 @@ pre.codeblock-buttons:hover {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.load-more-replies {
|
||||
font-size: var(--font-down-1);
|
||||
position: absolute;
|
||||
@@ -312,10 +369,12 @@ pre.codeblock-buttons:hover {
|
||||
&.top {
|
||||
margin-left: 0px;
|
||||
border: none;
|
||||
|
||||
.collapse-down {
|
||||
transform: translate(17%, 230%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
width: calc(
|
||||
var(--topic-body-width) + (var(--topic-body-width-padding) * 2) +
|
||||
var(--topic-avatar-width) - (var(--topic-avatar-width) + 2px)
|
||||
@@ -325,11 +384,14 @@ pre.codeblock-buttons:hover {
|
||||
.topic-avatar {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.topic-avatar {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.topic-body {
|
||||
overflow: visible;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@@ -342,31 +404,39 @@ pre.codeblock-buttons:hover {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.top.topic-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.post-date {
|
||||
color: var(--primary-med-or-secondary-high);
|
||||
}
|
||||
|
||||
.d-icon-arrow-up,
|
||||
.d-icon-arrow-down {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.reply:first-of-type .row {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.topic-meta-data {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.topic-meta-data h5 {
|
||||
position: absolute;
|
||||
z-index: z("base");
|
||||
font-size: var(--font-down-1);
|
||||
|
||||
a {
|
||||
font-weight: bold;
|
||||
color: var(--primary-low-mid-or-secondary-high);
|
||||
}
|
||||
}
|
||||
|
||||
.arrow {
|
||||
color: var(--primary-med-or-secondary-high);
|
||||
}
|
||||
@@ -376,6 +446,7 @@ pre.codeblock-buttons:hover {
|
||||
.relative-date {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-right: 2px;
|
||||
}
|
||||
@@ -403,6 +474,7 @@ pre.codeblock-buttons:hover {
|
||||
.bookmark.bookmarked .d-icon-discourse-bookmark-clock {
|
||||
color: var(--tertiary);
|
||||
}
|
||||
|
||||
.feature-on-profile.featured-on-profile .d-icon-id-card {
|
||||
color: var(--tertiary);
|
||||
}
|
||||
@@ -426,6 +498,7 @@ video {
|
||||
.video {
|
||||
// Height determined by aspect-ratio
|
||||
max-height: 500px;
|
||||
|
||||
> video {
|
||||
max-height: unset;
|
||||
}
|
||||
@@ -481,6 +554,7 @@ blockquote {
|
||||
background-color: var(--primary-very-low);
|
||||
}
|
||||
}
|
||||
|
||||
aside {
|
||||
.quote,
|
||||
.title,
|
||||
@@ -490,6 +564,7 @@ blockquote {
|
||||
background: var(--primary-very-low);
|
||||
border-left: 5px solid var(--primary-low);
|
||||
}
|
||||
|
||||
aside.quote > blockquote,
|
||||
aside.quote > .title {
|
||||
border-left: 0;
|
||||
@@ -514,18 +589,22 @@ blockquote {
|
||||
position: relative;
|
||||
border-top: 1px solid var(--primary-low);
|
||||
padding: 0.8em 0 0 0;
|
||||
|
||||
.topic-meta-data {
|
||||
padding: 0 var(--topic-body-width-padding) 0.25em
|
||||
var(--topic-body-width-padding);
|
||||
}
|
||||
|
||||
.cooked {
|
||||
padding: 1em var(--topic-body-width-padding) 0.25em
|
||||
var(--topic-body-width-padding);
|
||||
}
|
||||
|
||||
.group-request {
|
||||
padding: 0.5em var(--topic-body-width-padding) 0
|
||||
var(--topic-body-width-padding);
|
||||
}
|
||||
|
||||
a.expand-hidden {
|
||||
padding-left: var(--topic-body-width-padding);
|
||||
}
|
||||
@@ -599,29 +678,36 @@ blockquote {
|
||||
margin-left: 330px;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
margin: 4px auto;
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-down-1);
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
p.cancel {
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-up-4);
|
||||
color: var(--primary);
|
||||
margin-bottom: 5px;
|
||||
|
||||
.d-icon {
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,8 @@ html.discourse-no-touch {
|
||||
color: var(--secondary);
|
||||
}
|
||||
}
|
||||
.btn-flat.delete.d-hover {
|
||||
.btn-flat.delete.d-hover,
|
||||
.btn-flat.delete:hover {
|
||||
background: var(--danger);
|
||||
}
|
||||
|
||||
@@ -218,6 +219,7 @@ html {
|
||||
}
|
||||
button {
|
||||
&.d-hover,
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
background: var(--primary-medium);
|
||||
@@ -241,7 +243,8 @@ html {
|
||||
.d-icon {
|
||||
color: var(--tertiary);
|
||||
}
|
||||
&.d-hover {
|
||||
&.d-hover,
|
||||
&:hover {
|
||||
.d-icon {
|
||||
color: var(--tertiary-medium);
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ class CurrentUserSerializer < BasicUserSerializer
|
||||
:use_admin_sidebar,
|
||||
:can_view_raw_email,
|
||||
:use_glimmer_topic_list?,
|
||||
:use_auto_glimmer_post_menu?,
|
||||
:login_method,
|
||||
:has_unseen_features
|
||||
|
||||
@@ -326,6 +327,10 @@ class CurrentUserSerializer < BasicUserSerializer
|
||||
scope.user.in_any_groups?(SiteSetting.experimental_glimmer_topic_list_groups_map)
|
||||
end
|
||||
|
||||
def use_auto_glimmer_post_menu?
|
||||
scope.user.in_any_groups?(SiteSetting.glimmer_post_menu_groups_map)
|
||||
end
|
||||
|
||||
def do_not_disturb_channel_position
|
||||
MessageBus.last_id("/do-not-disturb/#{object.id}")
|
||||
end
|
||||
|
||||
@@ -3846,17 +3846,24 @@ en:
|
||||
|
||||
controls:
|
||||
reply: "begin composing a reply to this post"
|
||||
like_action: "Like"
|
||||
like: "like this post"
|
||||
has_liked: "you've liked this post"
|
||||
read_indicator: "members who read this post"
|
||||
undo_like_action: "Undo like"
|
||||
undo_like: "undo like"
|
||||
edit: "edit this post"
|
||||
edit_action: "Edit"
|
||||
edit_anonymous: "Sorry, but you need to be logged in to edit this post."
|
||||
flag_action: "Flag"
|
||||
flag: "privately flag this post for attention or send a private notification about it"
|
||||
delete_action: "Delete post"
|
||||
delete: "delete this post"
|
||||
undelete_action: "Undelete post"
|
||||
undelete: "undelete this post"
|
||||
share_action: "Share"
|
||||
share: "share a link to this post"
|
||||
copy_action: "Copy link"
|
||||
copy_title: "copy a link to this post to clipboard"
|
||||
link_copied: "Link copied!"
|
||||
more: "More"
|
||||
@@ -3870,6 +3877,7 @@ en:
|
||||
other: "Yes, and all %{count} replies"
|
||||
just_the_post: "No, just this post"
|
||||
admin: "post admin actions"
|
||||
admin_action: "Admin"
|
||||
permanently_delete: "Permanently Delete"
|
||||
permanently_delete_confirmation: "Are you sure you permanently want to delete this post? You will not be able to recover it."
|
||||
wiki: "Make Wiki"
|
||||
|
||||
@@ -1877,7 +1877,7 @@ en:
|
||||
markdown_linkify_tlds: "List of top level domains that get automatically treated as links"
|
||||
markdown_typographer_quotation_marks: "List of double and single quotes replacement pairs"
|
||||
post_undo_action_window_mins: "Number of minutes users are allowed to undo recent actions on a post (like, flag, etc)."
|
||||
must_approve_users: "All new users must wait for moderator or admin approval before being allowed to log in. (Note: enabling this setting also removes the \"arrive at topic\" invite option)"
|
||||
must_approve_users: 'All new users must wait for moderator or admin approval before being allowed to log in. (Note: enabling this setting also removes the "arrive at topic" invite option)'
|
||||
invite_code: "User must type this code to be allowed account registration, ignored when empty (case-insensitive)"
|
||||
approve_suspect_users: "Add suspicious users to the review queue. Suspicious users have entered a bio/website but have no reading activity."
|
||||
review_every_post: "Send every new post to the review queue for moderation. Posts are still published immediately and are visible to all users. WARNING! Not recommended for high-traffic sites due to the potential volume of posts needing review."
|
||||
@@ -2725,6 +2725,8 @@ en:
|
||||
experimental_topics_filter: "Enables the experimental topics filter route at /filter"
|
||||
enable_experimental_lightbox: "Replace the default image lightbox with the revamped design."
|
||||
experimental_glimmer_topic_list_groups: "Enable the new 'glimmer' topic list implementation. This implementation is under active development, and is not intended for production use. Do not develop themes/plugins against it until the implementation is finalized and announced."
|
||||
glimmer_post_menu_mode: "Control whether the new 'glimmer' post menu implementation is used. 'auto' will enable automatically once all your themes and plugins are ready. This implementation is under active development, and is not intended for production use. Do not develop themes/plugins against it until the implementation is finalized and announced."
|
||||
glimmer_post_menu_groups: "Enable the new 'glimmer' post menu implementation in 'auto' mode for the specified user groups. This implementation is under active development, and is not intended for production use. Do not develop themes/plugins against it until the implementation is finalized and announced."
|
||||
experimental_form_templates: "Enable the form templates feature. <b>After enabled,</b> manage the templates at <a href='%{base_path}/admin/customize/form-templates'>Customize / Templates</a>."
|
||||
admin_sidebar_enabled_groups: "Enable sidebar navigation for the admin UI for the specified groups, which replaces the top-level admin navigation buttons."
|
||||
lazy_load_categories_groups: "Lazy load category information only for users of these groups. This improves performance on sites with many categories."
|
||||
|
||||
@@ -3304,6 +3304,22 @@ experimental:
|
||||
default: ""
|
||||
allow_any: false
|
||||
refresh: true
|
||||
glimmer_post_menu_mode:
|
||||
client: true
|
||||
hidden: true
|
||||
type: enum
|
||||
choices:
|
||||
- disabled
|
||||
- auto
|
||||
- enabled
|
||||
default: disabled
|
||||
glimmer_post_menu_groups:
|
||||
client: true
|
||||
type: group_list
|
||||
list_type: compact
|
||||
default: ""
|
||||
allow_any: false
|
||||
refresh: true
|
||||
enable_experimental_lightbox:
|
||||
default: false
|
||||
client: true
|
||||
|
||||
@@ -7,6 +7,7 @@ module PageObjects
|
||||
include RSpec::Matchers
|
||||
|
||||
BODY_SELECTOR = ""
|
||||
MODAL_SELECTOR = ""
|
||||
|
||||
def header
|
||||
find(".d-modal__header")
|
||||
@@ -49,11 +50,11 @@ module PageObjects
|
||||
end
|
||||
|
||||
def open?
|
||||
has_css?(".modal.d-modal")
|
||||
has_css?(".modal.d-modal#{MODAL_SELECTOR}")
|
||||
end
|
||||
|
||||
def closed?
|
||||
has_no_css?(".modal.d-modal")
|
||||
has_no_css?(".modal.d-modal#{MODAL_SELECTOR}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -49,10 +49,26 @@ module PageObjects
|
||||
post_by_number(post).has_content? post.raw
|
||||
end
|
||||
|
||||
def has_deleted_post?(post)
|
||||
has_css?(".topic-post.deleted:has(#post_#{post.post_number})")
|
||||
end
|
||||
|
||||
def has_no_deleted_post?(post)
|
||||
has_no_css?(".topic-post.deleted:has(#post_#{post.post_number})")
|
||||
end
|
||||
|
||||
def has_post_number?(number)
|
||||
has_css?("#post_#{number}")
|
||||
end
|
||||
|
||||
def has_replies_expanded?(post)
|
||||
within_post(post) { has_css?(".embedded-posts") }
|
||||
end
|
||||
|
||||
def has_replies_collapsed?(post)
|
||||
within_post(post) { has_no_css?(".embedded-posts") }
|
||||
end
|
||||
|
||||
def post_by_number(post_or_number, wait: Capybara.default_max_wait_time)
|
||||
post_or_number = post_or_number.is_a?(Post) ? post_or_number.post_number : post_or_number
|
||||
find(".topic-post:not(.staged) #post_#{post_or_number}", wait: wait)
|
||||
@@ -79,22 +95,62 @@ module PageObjects
|
||||
end
|
||||
|
||||
def click_post_action_button(post, button)
|
||||
case button
|
||||
when :bookmark
|
||||
within_post(post) { find(".bookmark").click }
|
||||
when :reply
|
||||
within_post(post) { find(".post-controls .reply").click }
|
||||
when :flag
|
||||
within_post(post) { find(".post-controls .create-flag").click }
|
||||
when :copy_link
|
||||
within_post(post) { find(".post-controls .post-action-menu__copy-link").click }
|
||||
when :edit
|
||||
within_post(post) { find(".post-controls .edit").click }
|
||||
find_post_action_button(post, button).click
|
||||
end
|
||||
|
||||
def find_post_action_buttons(post)
|
||||
within_post(post) { find(".post-controls .actions") }
|
||||
end
|
||||
|
||||
def find_post_action_button(post, button)
|
||||
button_selector = selector_for_post_action_button(button)
|
||||
within_post(post) { find(button_selector) }
|
||||
end
|
||||
|
||||
def has_post_action_button?(post, button)
|
||||
button_selector = selector_for_post_action_button(button)
|
||||
within_post(post) { has_css?(button_selector) }
|
||||
end
|
||||
|
||||
def has_no_post_action_button?(post, button)
|
||||
button_selector = selector_for_post_action_button(button)
|
||||
within_post(post) { has_no_css?(button_selector) }
|
||||
end
|
||||
|
||||
def has_who_liked_on_post?(post, count: nil)
|
||||
if count
|
||||
return within_post(post) { has_css?(".who-liked a.trigger-user-card", count: count) }
|
||||
end
|
||||
|
||||
within_post(post) { has_css?(".who-liked") }
|
||||
end
|
||||
|
||||
def has_no_who_liked_on_post?(post)
|
||||
within_post(post) { has_no_css?(".who-liked") }
|
||||
end
|
||||
|
||||
def has_who_read_on_post?(post, count: nil)
|
||||
if count
|
||||
return within_post(post) { has_css?(".who-read a.trigger-user-card", count: count) }
|
||||
end
|
||||
|
||||
within_post(post) { has_css?(".who-read") }
|
||||
end
|
||||
|
||||
def has_no_who_read_on_post?(post)
|
||||
within_post(post) { has_no_css?(".who-read") }
|
||||
end
|
||||
|
||||
def expand_post_admin_actions(post)
|
||||
within_post(post) { find(".show-post-admin-menu").click }
|
||||
click_post_action_button(post, :admin)
|
||||
end
|
||||
|
||||
def has_post_admin_menu?()
|
||||
has_css?("[data-content][data-identifier='admin-post-menu']")
|
||||
end
|
||||
|
||||
def has_no_post_admin_menu?()
|
||||
has_no_css?("[data-content][data-identifier='admin-post-menu']")
|
||||
end
|
||||
|
||||
def click_post_admin_action_button(post, button)
|
||||
@@ -258,6 +314,42 @@ module PageObjects
|
||||
within("#topic-footer-buttons") { yield }
|
||||
end
|
||||
|
||||
def selector_for_post_action_button(button)
|
||||
# TODO (glimmer-post-menu): Replace the selector with the BEM format ones once the glimmer-post-menu replaces the widget post menu
|
||||
case button
|
||||
when :admin
|
||||
".post-controls .show-post-admin-menu"
|
||||
when :bookmark
|
||||
".post-controls .bookmark"
|
||||
when :copy_link, :copyLink
|
||||
".post-controls .post-action-menu__copy-link"
|
||||
when :delete
|
||||
".post-controls .delete"
|
||||
when :edit
|
||||
".post-controls .edit"
|
||||
when :flag
|
||||
".post-controls .create-flag"
|
||||
when :like
|
||||
".post-controls .toggle-like"
|
||||
when :like_count
|
||||
".post-controls .like-count"
|
||||
when :read
|
||||
".post-controls .read-indicator"
|
||||
when :recover
|
||||
".post-controls .recover"
|
||||
when :replies
|
||||
".post-controls .show-replies"
|
||||
when :reply
|
||||
".post-controls .reply"
|
||||
when :share
|
||||
".post-controls .share"
|
||||
when :show_more
|
||||
".post-controls .show-more-actions"
|
||||
else
|
||||
raise "Unknown post menu button type: #{button}"
|
||||
end
|
||||
end
|
||||
|
||||
def is_post_bookmarked(post, bookmarked:, with_reminder: false)
|
||||
within_post(post) do
|
||||
css_class = ".bookmark.bookmarked#{with_reminder ? ".with-reminder" : ""}"
|
||||
|
||||
@@ -1,23 +1,728 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe "Post menu", type: :system do
|
||||
fab!(:current_user) { Fabricate(:user) }
|
||||
fab!(:topic)
|
||||
fab!(:post) { Fabricate(:post, topic: topic) }
|
||||
fab!(:admin) { Fabricate(:admin, refresh_auto_groups: true) }
|
||||
fab!(:user) { Fabricate(:user, refresh_auto_groups: true) }
|
||||
fab!(:topic, reload: true)
|
||||
fab!(:post, reload: true) { Fabricate(:post, topic: topic, reads: 5, like_count: 6) }
|
||||
fab!(:post2, reload: true) { Fabricate(:post, user: user, topic: topic, like_count: 0) }
|
||||
|
||||
let(:composer) { PageObjects::Components::Composer.new }
|
||||
let(:topic_page) { PageObjects::Pages::Topic.new }
|
||||
let(:flag_modal) { PageObjects::Modals::Flag.new }
|
||||
let(:login_modal) { PageObjects::Modals::Login.new }
|
||||
let(:modal) { PageObjects::Modals::Base.new }
|
||||
|
||||
before { sign_in(current_user) }
|
||||
%w[enabled disabled].each do |value|
|
||||
before do
|
||||
SiteSetting.glimmer_post_menu_mode = value
|
||||
SiteSetting.post_menu = "like|copyLink|share|flag|edit|bookmark|delete|admin|reply"
|
||||
SiteSetting.post_menu_hidden_items = "flag|bookmark|edit|delete|admin"
|
||||
end
|
||||
|
||||
describe "copy link" do
|
||||
let(:cdp) { PageObjects::CDP.new }
|
||||
context "when the Glimmer post menu is #{value}" do
|
||||
describe "general rendering" do
|
||||
before { sign_in(admin) }
|
||||
|
||||
before { cdp.allow_clipboard }
|
||||
it "renders the expected buttons according to what is specified in the `post_menu`/`post_menu_hidden_items` settings" do
|
||||
[
|
||||
{
|
||||
# skip the read button because we're not viewing a PM
|
||||
post_menu: "like|copyLink|share|flag|edit|bookmark|delete|admin|reply",
|
||||
post_menu_hidden_items: "flag|bookmark|edit|delete|admin",
|
||||
},
|
||||
{
|
||||
post_menu: "like|copyLink|edit|bookmark|delete|admin|reply",
|
||||
post_menu_hidden_items: "flag|admin|edit",
|
||||
},
|
||||
].each do |scenario|
|
||||
scenario => { post_menu:, post_menu_hidden_items: }
|
||||
|
||||
it "copies the absolute link to the post when clicked" do
|
||||
topic_page.visit_topic(post.topic)
|
||||
topic_page.click_post_action_button(post, :copy_link)
|
||||
cdp.clipboard_has_text?(post.full_url(share_url: true) + "?u=#{current_user.username}")
|
||||
SiteSetting.post_menu = post_menu
|
||||
SiteSetting.post_menu_hidden_items = post_menu_hidden_items
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
available_buttons = %w[admin bookmark copyLink delete edit flag like read reply share]
|
||||
expected_buttons = SiteSetting.post_menu.split("|")
|
||||
hidden_buttons = SiteSetting.post_menu_hidden_items.split("|")
|
||||
visible_buttons = expected_buttons - hidden_buttons
|
||||
|
||||
visible_buttons.each do |button|
|
||||
expect(topic_page).to have_post_action_button(post, button.to_sym)
|
||||
end
|
||||
hidden_buttons.each do |button|
|
||||
expect(topic_page).to have_no_post_action_button(post, button.to_sym)
|
||||
end
|
||||
|
||||
# expand the items and check again if all expected buttons are visible now
|
||||
topic_page.expand_post_actions(post)
|
||||
expected_buttons.each do |button|
|
||||
expect(topic_page).to have_post_action_button(post, button.to_sym)
|
||||
end
|
||||
|
||||
# check if the buttons are in the correct order
|
||||
node_elements = topic_page.find_post_action_buttons(post).all("*")
|
||||
button_node_positions =
|
||||
expected_buttons.map do |button|
|
||||
node_elements.find_index(topic_page.find_post_action_button(post, button.to_sym))
|
||||
end
|
||||
|
||||
expect(button_node_positions).to eq(button_node_positions.compact.sort)
|
||||
|
||||
# verify that buttons that weren't specified int the post_menu setting weren't rendered
|
||||
(available_buttons - expected_buttons).each do |button|
|
||||
expect(topic_page).to have_no_post_action_button(post, button.to_sym)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "admin" do
|
||||
before do
|
||||
SiteSetting.edit_wiki_post_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
||||
SiteSetting.post_menu_hidden_items = ""
|
||||
end
|
||||
|
||||
it "displays the admin button when the user can manage the post" do
|
||||
# do not display the edit button when unlogged
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_no_post_action_button(post, :admin)
|
||||
expect(topic_page).to have_no_post_action_button(post2, :admin)
|
||||
|
||||
# display the admin button for all the posts when a moderator is logged
|
||||
sign_in(Fabricate(:moderator))
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_post_action_button(post, :admin)
|
||||
expect(topic_page).to have_post_action_button(post2, :admin)
|
||||
|
||||
# display the admin button for the all the posts when an admin is logged
|
||||
sign_in(admin)
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_post_action_button(post, :admin)
|
||||
expect(topic_page).to have_post_action_button(post2, :admin)
|
||||
end
|
||||
|
||||
it "displays the admin button when the user can wiki the post / edit staff notices" do
|
||||
# display the admin button when the user can wiki
|
||||
sign_in(Fabricate(:trust_level_4))
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_post_action_button(post, :admin)
|
||||
expect(topic_page).to have_post_action_button(post2, :admin)
|
||||
|
||||
# display the admin button when the user can wiki
|
||||
SiteSetting.self_wiki_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
||||
|
||||
sign_in(user)
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_no_post_action_button(post, :admin)
|
||||
expect(topic_page).to have_post_action_button(post2, :admin)
|
||||
end
|
||||
|
||||
it "works as expected" do
|
||||
sign_in(admin)
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_no_post_admin_menu
|
||||
topic_page.click_post_action_button(post, :admin)
|
||||
expect(topic_page).to have_post_admin_menu
|
||||
end
|
||||
end
|
||||
|
||||
describe "bookmark" do
|
||||
before { SiteSetting.post_menu_hidden_items = "" }
|
||||
|
||||
it "does not display the bookmark button when the user is anonymous" do
|
||||
topic_page.visit_topic(post.topic)
|
||||
expect(topic_page).to have_no_post_action_button(post, :bookmark)
|
||||
end
|
||||
|
||||
it "works as expected" do
|
||||
sign_in(user)
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_post_action_button(post, :bookmark)
|
||||
|
||||
bookmark_button = topic_page.find_post_action_button(post, :bookmark)
|
||||
expect(bookmark_button[:class].split("\s")).not_to include("bookmarked")
|
||||
|
||||
topic_page.click_post_action_button(post, :bookmark)
|
||||
expect(topic_page).to have_post_bookmarked(post)
|
||||
end
|
||||
end
|
||||
|
||||
describe "copy link" do
|
||||
let(:cdp) { PageObjects::CDP.new }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
cdp.allow_clipboard
|
||||
end
|
||||
|
||||
it "copies the absolute link to the post when clicked" do
|
||||
topic_page.visit_topic(post.topic)
|
||||
topic_page.click_post_action_button(post, :copy_link)
|
||||
cdp.clipboard_has_text?(post.full_url(share_url: true) + "?u=#{user.username}")
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete / recover" do
|
||||
before { SiteSetting.post_menu_hidden_items = "" }
|
||||
|
||||
it "displays the delete button only when the user can delete the post" do
|
||||
# do not display the edit button when unlogged
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_no_post_action_button(post, :delete)
|
||||
expect(topic_page).to have_no_post_action_button(post2, :delete)
|
||||
|
||||
# display the delete button only for the post that `user` can delete
|
||||
sign_in(user)
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_no_post_action_button(post, :delete)
|
||||
expect(topic_page).to have_post_action_button(post2, :delete)
|
||||
|
||||
# display the delete button for the all the posts because an admin is logged
|
||||
sign_in(admin)
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_post_action_button(post, :delete)
|
||||
expect(topic_page).to have_post_action_button(post2, :delete)
|
||||
end
|
||||
|
||||
it "displays the recover button only when the user can recover the post" do
|
||||
PostDestroyer.new(user, post2).destroy
|
||||
|
||||
# do not display the edit button when unlogged
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_no_post_action_button(post, :recover)
|
||||
expect(topic_page).to have_no_post_action_button(post2, :recover)
|
||||
|
||||
# do not display the recover button because when `user` deletes teh post. it's just marked for deletion
|
||||
# `user` cannot recover it.
|
||||
sign_in(user)
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_no_post_action_button(post, :recover)
|
||||
expect(topic_page).to have_no_post_action_button(post2, :recover)
|
||||
|
||||
# display the recover button for the deleted post because an admin is logged
|
||||
PostDestroyer.new(admin, post).destroy
|
||||
|
||||
sign_in(admin)
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_post_action_button(post, :recover)
|
||||
end
|
||||
|
||||
it "deletes a topic" do
|
||||
sign_in(admin)
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_no_deleted_post(post)
|
||||
topic_page.click_post_action_button(post, :delete)
|
||||
expect(topic_page).to have_deleted_post(post)
|
||||
end
|
||||
|
||||
it "deletes a post" do
|
||||
sign_in(admin)
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_no_deleted_post(post2)
|
||||
topic_page.click_post_action_button(post2, :delete)
|
||||
expect(topic_page).to have_deleted_post(post2)
|
||||
end
|
||||
|
||||
it "shows a flag to delete message when the user is the author but can't delete it without permission" do
|
||||
other_topic = Fabricate(:topic)
|
||||
other_p1 = Fabricate(:post, topic: other_topic, user: user)
|
||||
other_p2 = Fabricate(:post, topic: other_topic, user: Fabricate(:user))
|
||||
|
||||
sign_in(user)
|
||||
|
||||
topic_page.visit_topic(other_topic)
|
||||
|
||||
expect(topic_page).to have_post_action_button(other_p1, :delete)
|
||||
expect(topic_page).to have_post_action_button(other_p1, :flag)
|
||||
topic_page.click_post_action_button(other_p1, :delete)
|
||||
|
||||
expect(topic_page).to have_no_deleted_post(other_p1)
|
||||
expect(modal).to be_open
|
||||
expect(modal).to have_content(I18n.t("js.post.controls.delete_topic_disallowed_modal"))
|
||||
end
|
||||
|
||||
it "recovers a topic" do
|
||||
PostDestroyer.new(admin, post).destroy
|
||||
|
||||
sign_in(admin)
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_deleted_post(post)
|
||||
topic_page.click_post_action_button(post, :recover)
|
||||
expect(topic_page).to have_no_deleted_post(post)
|
||||
end
|
||||
|
||||
it "recovers a post" do
|
||||
sign_in(admin)
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_no_deleted_post(post2)
|
||||
topic_page.click_post_action_button(post2, :delete)
|
||||
expect(topic_page).to have_deleted_post(post2)
|
||||
|
||||
topic_page.click_post_action_button(post2, :recover)
|
||||
expect(topic_page).to have_no_deleted_post(post2)
|
||||
end
|
||||
end
|
||||
|
||||
describe "edit" do
|
||||
before do
|
||||
SiteSetting.edit_post_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
||||
SiteSetting.edit_wiki_post_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
||||
SiteSetting.post_menu_hidden_items = ""
|
||||
end
|
||||
|
||||
it "displays the edit button only when the user can edit the post" do
|
||||
# do not display the edit button when unlogged
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_no_post_action_button(post, :edit)
|
||||
expect(topic_page).to have_no_post_action_button(post2, :edit)
|
||||
|
||||
# display the edit button only for the post that `user` can edit
|
||||
sign_in(user)
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_no_post_action_button(post, :edit)
|
||||
expect(topic_page).to have_post_action_button(post2, :edit)
|
||||
|
||||
# display the edit button for the all the posts because an admin is logged
|
||||
sign_in(admin)
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_post_action_button(post, :edit)
|
||||
expect(topic_page).to have_post_action_button(post2, :edit)
|
||||
end
|
||||
|
||||
it "displays the edit button properly when the topic is a wiki" do
|
||||
wiki_topic = Fabricate(:topic)
|
||||
wiki_post = Fabricate(:post, topic: wiki_topic, user: Fabricate(:user), wiki: true)
|
||||
non_wiki_post = Fabricate(:post, topic: wiki_topic, user: Fabricate(:user), wiki: false)
|
||||
|
||||
# do not display the edit button when unlogged
|
||||
topic_page.visit_topic(wiki_post.topic)
|
||||
|
||||
expect(topic_page).to have_no_post_action_button(wiki_post, :edit)
|
||||
expect(topic_page).to have_no_post_action_button(non_wiki_post, :edit)
|
||||
|
||||
# display the edit button only for the post that `user` can edit
|
||||
sign_in(user)
|
||||
|
||||
topic_page.visit_topic(wiki_post.topic)
|
||||
|
||||
expect(topic_page).to have_post_action_button(wiki_post, :edit)
|
||||
expect(topic_page).to have_no_post_action_button(non_wiki_post, :edit)
|
||||
end
|
||||
|
||||
it "works as expected" do
|
||||
sign_in(user)
|
||||
|
||||
topic_page.visit_topic(post2.topic)
|
||||
|
||||
expect(composer).to be_closed
|
||||
topic_page.click_post_action_button(post2, :edit)
|
||||
|
||||
expect(composer).to be_opened
|
||||
expect(composer).to have_content(post2.raw)
|
||||
end
|
||||
end
|
||||
|
||||
describe "flag" do
|
||||
before { SiteSetting.post_menu_hidden_items = "" }
|
||||
|
||||
it "displays the flag button only when the user can flag the post" do
|
||||
# do not display the edit button when unlogged
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_no_post_action_button(post, :flag)
|
||||
expect(topic_page).to have_no_post_action_button(post2, :flag)
|
||||
|
||||
# display the flag button only for the post that `user` can flag
|
||||
sign_in(user)
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_post_action_button(post, :flag)
|
||||
expect(topic_page).to have_post_action_button(post2, :flag)
|
||||
|
||||
# display the flag button for the all the posts because an admin is logged
|
||||
sign_in(admin)
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_post_action_button(post, :flag)
|
||||
expect(topic_page).to have_post_action_button(post2, :flag)
|
||||
end
|
||||
|
||||
it "works as expected" do
|
||||
sign_in(user)
|
||||
|
||||
topic_page.visit_topic(post2.topic)
|
||||
|
||||
expect(flag_modal).to be_closed
|
||||
topic_page.click_post_action_button(post2, :flag)
|
||||
expect(flag_modal).to be_open
|
||||
end
|
||||
end
|
||||
|
||||
describe "like" do
|
||||
it "toggles liking a post" do
|
||||
unliked_post = Fabricate(:post, topic: topic, like_count: 0)
|
||||
|
||||
sign_in(user)
|
||||
|
||||
topic_page.visit_topic(unliked_post.topic)
|
||||
|
||||
expect(topic_page).to have_post_action_button(unliked_post, :like)
|
||||
like_button = topic_page.find_post_action_button(unliked_post, :like)
|
||||
expect(like_button[:class].split("\s")).to include("like")
|
||||
|
||||
expect(topic_page).to have_no_post_action_button(unliked_post, :like_count)
|
||||
|
||||
# toggle the like on
|
||||
topic_page.click_post_action_button(unliked_post, :like)
|
||||
|
||||
# we need this because the find_post_action_button will target the like button on or off
|
||||
try_until_success do
|
||||
like_button = topic_page.find_post_action_button(unliked_post, :like)
|
||||
expect(like_button[:class].split("\s")).to include("has-like")
|
||||
end
|
||||
|
||||
like_count_button = topic_page.find_post_action_button(unliked_post, :like_count)
|
||||
expect(like_count_button).to have_content(1)
|
||||
|
||||
# toggle the like off
|
||||
topic_page.click_post_action_button(unliked_post, :like)
|
||||
|
||||
# we need this because the find_post_action_button will target the like button on or off
|
||||
try_until_success do
|
||||
like_button = topic_page.find_post_action_button(unliked_post, :like)
|
||||
expect(like_button[:class].split("\s")).to include("like")
|
||||
end
|
||||
|
||||
expect(topic_page).to have_no_post_action_button(unliked_post, :like_count)
|
||||
end
|
||||
|
||||
it "displays the login dialog when the user is anonymous" do
|
||||
topic_page.visit_topic(post2.topic)
|
||||
|
||||
expect(topic_page).to have_post_action_button(post2, :like)
|
||||
like_button = topic_page.find_post_action_button(post2, :like)
|
||||
expect(like_button[:title]).to eq(I18n.t("js.post.controls.like"))
|
||||
|
||||
expect(topic_page).to have_no_post_action_button(post2, :like_count)
|
||||
|
||||
# clicking on the like button should display the login modal
|
||||
topic_page.click_post_action_button(post2, :like)
|
||||
|
||||
expect(login_modal).to be_open
|
||||
end
|
||||
|
||||
it "renders the like count as expected" do
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
# renders the like count when the it's not zero
|
||||
like_count_button = topic_page.find_post_action_button(post, :like_count)
|
||||
expect(like_count_button).to have_content(post.like_count)
|
||||
|
||||
# does not render the like count when it's zero
|
||||
expect(topic_page).to have_no_post_action_button(post2, :like_count)
|
||||
end
|
||||
|
||||
it "toggles the users who liked when clicking on the like count" do
|
||||
PostActionCreator.like(user, post)
|
||||
PostActionCreator.like(admin, post)
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_no_who_liked_on_post(post)
|
||||
|
||||
# toggle users who liked on
|
||||
topic_page.click_post_action_button(post, :like_count)
|
||||
expect(topic_page).to have_who_liked_on_post(post, count: 2)
|
||||
|
||||
# toggle users who liked off
|
||||
topic_page.click_post_action_button(post, :like_count)
|
||||
expect(topic_page).to have_no_who_liked_on_post(post)
|
||||
end
|
||||
end
|
||||
|
||||
describe "read" do
|
||||
fab!(:group) { Fabricate(:group, publish_read_state: true) }
|
||||
fab!(:group_user) { Fabricate(:group_user, group: group, user: user) }
|
||||
fab!(:group_user2) { Fabricate(:group_user, group: group, user: Fabricate(:user)) }
|
||||
fab!(:pm) { Fabricate(:private_message_topic, allowed_groups: [group]) }
|
||||
fab!(:pm_post1) { Fabricate(:post, topic: pm, user: user, reads: 2, created_at: 1.day.ago) }
|
||||
fab!(:pm_post2) { Fabricate(:post, topic: pm, user: group_user2.user, reads: 0) }
|
||||
|
||||
before do
|
||||
SiteSetting.post_menu = "read|like|copyLink|share|flag|edit|bookmark|delete|admin|reply"
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it "shows the read indicator when expected" do
|
||||
topic_page.visit_topic(pm)
|
||||
# it shows the read indicator on group pms where publish_read_state = true
|
||||
# when the post has reads > 0
|
||||
expect(topic_page).to have_post_action_button(pm_post1, :read)
|
||||
read_button = topic_page.find_post_action_button(pm_post1, :read)
|
||||
expect(read_button).to have_content(1)
|
||||
# don't show when the post has reads = 0
|
||||
expect(topic_page).to have_no_post_action_button(pm_post2, :read)
|
||||
|
||||
# dont't show on regular posts
|
||||
topic_page.visit_topic(post.topic)
|
||||
expect(topic_page).to have_no_post_action_button(post, :read)
|
||||
expect(topic_page).to have_no_post_action_button(post2, :read)
|
||||
end
|
||||
|
||||
it "toggles the users who read when clicking on the read button" do
|
||||
TopicUser.update_last_read(user, pm.id, 1, 1, 1)
|
||||
TopicUser.update_last_read(admin, pm.id, 2, 1, 1)
|
||||
|
||||
topic_page.visit_topic(pm)
|
||||
|
||||
expect(topic_page).to have_no_who_read_on_post(pm_post1)
|
||||
|
||||
# toggle users who read on
|
||||
topic_page.click_post_action_button(pm_post1, :read)
|
||||
expect(topic_page).to have_who_read_on_post(pm_post1, count: 1)
|
||||
|
||||
# toggle users who read off
|
||||
topic_page.click_post_action_button(pm_post1, :read)
|
||||
expect(topic_page).to have_no_who_read_on_post(pm_post1)
|
||||
end
|
||||
end
|
||||
|
||||
describe "replies" do
|
||||
fab!(:reply_to_post) do
|
||||
PostCreator.new(
|
||||
Fabricate(:user),
|
||||
raw: "Just a reply to the OP",
|
||||
topic_id: topic.id,
|
||||
reply_to_post_number: post.post_number,
|
||||
).create
|
||||
end
|
||||
fab!(:post_with_reply, reload: true) do
|
||||
PostCreator.new(topic.user, raw: Fabricate.build(:post).raw, topic_id: topic.id).create
|
||||
end
|
||||
fab!(:reply_to_post_with_reply) do
|
||||
PostCreator.new(
|
||||
Fabricate(:user),
|
||||
raw: "A reply directly below the post",
|
||||
topic_id: topic.id,
|
||||
reply_to_post_number: post_with_reply.post_number,
|
||||
).create
|
||||
end
|
||||
|
||||
it "doesn't display the replies button when there are no replies" do
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_no_post_action_button(post2, :replies)
|
||||
end
|
||||
|
||||
it "is disabled when the post is deleted" do
|
||||
PostDestroyer.new(admin, post).destroy
|
||||
|
||||
sign_in(admin)
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_post_action_button(post, :replies)
|
||||
|
||||
replies_button = topic_page.find_post_action_button(post, :replies)
|
||||
expect(replies_button[:disabled]).to eq("true")
|
||||
end
|
||||
|
||||
it "displays the replies when clicked" do
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_replies_collapsed(post)
|
||||
topic_page.click_post_action_button(post, :replies)
|
||||
expect(topic_page).to have_replies_expanded(post)
|
||||
|
||||
replies_button = topic_page.find_post_action_button(post, :replies)
|
||||
expect(replies_button[:ariaExpanded]).to eq("true")
|
||||
expect(replies_button[:ariaPressed]).to eq("true")
|
||||
end
|
||||
|
||||
it "is displayed correctly when the reply is directly below" do
|
||||
# it is displayed when there is only one reply directly below and the setting `suppress_reply_directly_below`
|
||||
# is disabled
|
||||
SiteSetting.suppress_reply_directly_below = false
|
||||
|
||||
topic_page.visit_topic(post_with_reply.topic)
|
||||
expect(topic_page).to have_post_action_button(post_with_reply, :replies)
|
||||
|
||||
# it is not displayed when there is only one reply directly below and the setting
|
||||
# `suppress_reply_directly_below` is enabled
|
||||
SiteSetting.suppress_reply_directly_below = true
|
||||
|
||||
topic_page.visit_topic(post_with_reply.topic)
|
||||
expect(topic_page).to have_no_post_action_button(post_with_reply, :replies)
|
||||
|
||||
# it is displayed when there is more than one reply directly below
|
||||
PostCreator.new(
|
||||
Fabricate(:user),
|
||||
raw: "A reply directly below the post",
|
||||
topic_id: topic.id,
|
||||
reply_to_post_number: post_with_reply.post_number,
|
||||
).create
|
||||
|
||||
topic_page.visit_topic(post_with_reply.topic)
|
||||
expect(topic_page).to have_post_action_button(post_with_reply, :replies)
|
||||
end
|
||||
end
|
||||
|
||||
describe "reply" do
|
||||
before { SiteSetting.post_menu_hidden_items = "" }
|
||||
|
||||
it "displays the reply button only when the user can reply the post" do
|
||||
# do not display the reply button when unlogged
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_no_post_action_button(post, :reply)
|
||||
expect(topic_page).to have_no_post_action_button(post2, :reply)
|
||||
|
||||
# display the reply button when the user can reply to the post
|
||||
sign_in(user)
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_post_action_button(post, :reply)
|
||||
expect(topic_page).to have_post_action_button(post2, :reply)
|
||||
end
|
||||
|
||||
it "displays the reply button less prominently when the topic is a wiki" do
|
||||
wiki_topic = Fabricate(:topic)
|
||||
wiki_post = Fabricate(:post, topic: wiki_topic, user: Fabricate(:user), wiki: true)
|
||||
non_wiki_post = Fabricate(:post, topic: wiki_topic, user: Fabricate(:user), wiki: false)
|
||||
|
||||
# do not display the reply button when unlogged
|
||||
topic_page.visit_topic(wiki_post.topic)
|
||||
|
||||
expect(topic_page).to have_no_post_action_button(wiki_post, :reply)
|
||||
expect(topic_page).to have_no_post_action_button(non_wiki_post, :reply)
|
||||
|
||||
# display the reply button only for the post that `user` can reply
|
||||
sign_in(user)
|
||||
|
||||
topic_page.visit_topic(wiki_post.topic)
|
||||
|
||||
expect(topic_page).to have_post_action_button(wiki_post, :reply)
|
||||
expect(topic_page).to have_post_action_button(non_wiki_post, :reply)
|
||||
|
||||
# display the reply button less prominently for the wiki post, i.e. it's not a create button
|
||||
# and the label is not displayed
|
||||
wiki_post_reply_button = topic_page.find_post_action_button(wiki_post, :reply)
|
||||
expect(wiki_post_reply_button[:class].split("\s")).not_to include("create")
|
||||
expect(wiki_post_reply_button).to have_no_content(I18n.t("js.topic.reply.title"))
|
||||
|
||||
# display the reply as a create button for the non-wiki post and the label should be displayed
|
||||
non_wiki_post_reply_button = topic_page.find_post_action_button(non_wiki_post, :reply)
|
||||
expect(non_wiki_post_reply_button[:class].split("\s")).to include("create")
|
||||
expect(non_wiki_post_reply_button).to have_content(I18n.t("js.topic.reply.title"))
|
||||
end
|
||||
|
||||
it "works as expected" do
|
||||
sign_in(user)
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(composer).to be_closed
|
||||
topic_page.click_post_action_button(post, :reply)
|
||||
|
||||
expect(composer).to be_opened
|
||||
expect(composer).to have_content("")
|
||||
end
|
||||
end
|
||||
|
||||
describe "share" do
|
||||
it "works as expected" do
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_post_action_button(post, :share)
|
||||
topic_page.click_post_action_button(post, :share)
|
||||
|
||||
expect(page).to have_css(".d-modal.share-topic-modal")
|
||||
end
|
||||
end
|
||||
|
||||
describe "show more" do
|
||||
before do
|
||||
sign_in(admin)
|
||||
|
||||
SiteSetting.post_menu = "like|copyLink|share|flag|edit|bookmark|delete|admin|reply"
|
||||
SiteSetting.post_menu_hidden_items = "flag|bookmark|edit|delete|admin"
|
||||
end
|
||||
|
||||
it "is not displayed when `post_menu_hidden_items` is empty" do
|
||||
SiteSetting.post_menu_hidden_items = ""
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_no_post_action_button(post, :show_more)
|
||||
end
|
||||
|
||||
it "is not displayed when there is only one hidden button" do
|
||||
SiteSetting.post_menu_hidden_items = "admin"
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_no_post_action_button(post, :show_more)
|
||||
end
|
||||
|
||||
it "works as expected" do
|
||||
topic_page.visit_topic(post.topic)
|
||||
|
||||
expect(topic_page).to have_no_post_action_button(post, :flag)
|
||||
expect(topic_page).to have_no_post_action_button(post, :bookmark)
|
||||
expect(topic_page).to have_no_post_action_button(post, :edit)
|
||||
expect(topic_page).to have_no_post_action_button(post, :delete)
|
||||
expect(topic_page).to have_no_post_action_button(post, :admin)
|
||||
|
||||
expect(topic_page).to have_post_action_button(post, :show_more)
|
||||
topic_page.click_post_action_button(post, :show_more)
|
||||
|
||||
expect(topic_page).to have_post_action_button(post, :flag)
|
||||
expect(topic_page).to have_post_action_button(post, :bookmark)
|
||||
expect(topic_page).to have_post_action_button(post, :edit)
|
||||
expect(topic_page).to have_post_action_button(post, :delete)
|
||||
expect(topic_page).to have_post_action_button(post, :admin)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user