FIX: allows selection of messages in threads (#22119)
This commit fixes the selection of message in threads and also applies various refactorings - improves specs and especially page objects/components - makes the channel/thread panes responsible of the state - adds an animationend modifier - continues to follow the logic of "state" should be displayed as data attributes on component by having a new `data-selected` attribute on chat messages
This commit is contained in:
parent
f3afc8bf85
commit
79a260a6bb
|
@ -68,14 +68,12 @@
|
|||
/>
|
||||
|
||||
{{#if this.pane.selectingMessages}}
|
||||
<ChatSelectionManager
|
||||
@selectedMessageIds={{this.pane.selectedMessageIds}}
|
||||
@chatChannel={{@channel}}
|
||||
@cancelSelecting={{action
|
||||
this.pane.cancelSelecting
|
||||
@channel.selectedMessages
|
||||
<Chat::SelectionManager
|
||||
@enableMove={{and
|
||||
(not @channel.isDirectMessageChannel)
|
||||
@channel.canModerate
|
||||
}}
|
||||
@context="channel"
|
||||
@pane={{this.pane}}
|
||||
/>
|
||||
{{else}}
|
||||
{{#if (or @channel.isDraft @channel.isFollowing)}}
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
}}
|
||||
data-id={{@message.id}}
|
||||
data-thread-id={{@message.thread.id}}
|
||||
data-selected={{@message.selected}}
|
||||
{{chat/track-message
|
||||
(hash
|
||||
didEnterViewport=(fn @messageDidEnterViewport @message)
|
||||
|
|
|
@ -1,133 +0,0 @@
|
|||
import Component from "@ember/component";
|
||||
import { action, computed } from "@ember/object";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import { clipboardCopyAsync } from "discourse/lib/utilities";
|
||||
import { getOwner } from "discourse-common/lib/get-owner";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { isTesting } from "discourse-common/config/environment";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { schedule } from "@ember/runloop";
|
||||
import { inject as service } from "@ember/service";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import { MESSAGE_CONTEXT_THREAD } from "discourse/plugins/chat/discourse/components/chat-message";
|
||||
|
||||
export default class ChatSelectionManager extends Component {
|
||||
@service router;
|
||||
tagName = "";
|
||||
chatChannel = null;
|
||||
context = null;
|
||||
selectedMessageIds = null;
|
||||
chatCopySuccess = false;
|
||||
showChatCopySuccess = false;
|
||||
cancelSelecting = null;
|
||||
|
||||
@computed("selectedMessageIds.length")
|
||||
get anyMessagesSelected() {
|
||||
return this.selectedMessageIds.length > 0;
|
||||
}
|
||||
|
||||
@computed("chatChannel.isDirectMessageChannel", "chatChannel.canModerate")
|
||||
get showMoveMessageButton() {
|
||||
return (
|
||||
this.context !== MESSAGE_CONTEXT_THREAD &&
|
||||
!this.chatChannel.isDirectMessageChannel &&
|
||||
this.chatChannel.canModerate
|
||||
);
|
||||
}
|
||||
|
||||
@bind
|
||||
async generateQuote() {
|
||||
const response = await ajax(
|
||||
getURL(`/chat/${this.chatChannel.id}/quote.json`),
|
||||
{
|
||||
data: { message_ids: this.selectedMessageIds },
|
||||
type: "POST",
|
||||
}
|
||||
);
|
||||
|
||||
return new Blob([response.markdown], {
|
||||
type: "text/plain",
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
openMoveMessageModal() {
|
||||
showModal("chat-message-move-to-channel-modal").setProperties({
|
||||
sourceChannel: this.chatChannel,
|
||||
selectedMessageIds: this.selectedMessageIds,
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async quoteMessages() {
|
||||
let quoteMarkdown;
|
||||
|
||||
try {
|
||||
const quoteMarkdownBlob = await this.generateQuote();
|
||||
quoteMarkdown = await quoteMarkdownBlob.text();
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
|
||||
const container = getOwner(this);
|
||||
const composer = container.lookup("controller:composer");
|
||||
const openOpts = {};
|
||||
|
||||
if (this.chatChannel.isCategoryChannel) {
|
||||
openOpts.categoryId = this.chatChannel.chatableId;
|
||||
}
|
||||
|
||||
if (this.site.mobileView) {
|
||||
// go to the relevant chatable (e.g. category) and open the
|
||||
// composer to insert text
|
||||
if (this.chatChannel.chatableUrl) {
|
||||
this.router.transitionTo(this.chatChannel.chatableUrl);
|
||||
}
|
||||
|
||||
await composer.focusComposer({
|
||||
fallbackToNewTopic: true,
|
||||
insertText: quoteMarkdown,
|
||||
openOpts,
|
||||
});
|
||||
} else {
|
||||
// open the composer and insert text, reply to the current
|
||||
// topic if there is one, use the active draft if there is one
|
||||
const topic = container.lookup("controller:topic");
|
||||
await composer.focusComposer({
|
||||
fallbackToNewTopic: true,
|
||||
topic: topic?.model,
|
||||
insertText: quoteMarkdown,
|
||||
openOpts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async copyMessages() {
|
||||
try {
|
||||
this.set("chatCopySuccess", false);
|
||||
|
||||
if (!isTesting()) {
|
||||
// clipboard API throws errors in tests
|
||||
await clipboardCopyAsync(this.generateQuote);
|
||||
this.set("chatCopySuccess", true);
|
||||
}
|
||||
|
||||
this.set("showChatCopySuccess", true);
|
||||
|
||||
schedule("afterRender", () => {
|
||||
const element = document.querySelector(".chat-selection-message");
|
||||
element?.addEventListener("animationend", () => {
|
||||
if (this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.set("showChatCopySuccess", false);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -39,15 +39,7 @@
|
|||
</div>
|
||||
|
||||
{{#if this.chatThreadPane.selectingMessages}}
|
||||
<ChatSelectionManager
|
||||
@selectedMessageIds={{this.chatThreadPane.selectedMessageIds}}
|
||||
@chatChannel={{@channel}}
|
||||
@cancelSelecting={{action
|
||||
this.chatThreadPane.cancelSelecting
|
||||
@channel.selectedMessages
|
||||
}}
|
||||
@context="thread"
|
||||
/>
|
||||
<Chat::SelectionManager @pane={{this.chatThreadPane}} />
|
||||
{{else}}
|
||||
<Chat::Composer::Thread
|
||||
@channel={{@channel}}
|
||||
|
|
|
@ -1,41 +1,30 @@
|
|||
<div
|
||||
class={{concat-class
|
||||
"chat-selection-management"
|
||||
(if this.chatCopySuccess "chat-copy-success")
|
||||
}}
|
||||
>
|
||||
<div class="chat-selection-management-buttons">
|
||||
<div class="chat-selection-management">
|
||||
<div class="chat-selection-management__buttons">
|
||||
<DButton
|
||||
@id="chat-quote-btn"
|
||||
@class="btn-secondary"
|
||||
@icon="quote-left"
|
||||
@label="chat.selection.quote_selection"
|
||||
@title="chat.selection.quote_selection"
|
||||
@disabled={{not this.anyMessagesSelected}}
|
||||
@action={{action "quoteMessages"}}
|
||||
@action={{this.quoteMessages}}
|
||||
/>
|
||||
|
||||
{{#if this.site.desktopView}}
|
||||
<DButton
|
||||
@id="chat-copy-btn"
|
||||
@class="btn-secondary"
|
||||
@icon="copy"
|
||||
@label="chat.selection.copy"
|
||||
@title="chat.selection.copy"
|
||||
@disabled={{not this.anyMessagesSelected}}
|
||||
@action={{action "copyMessages"}}
|
||||
@action={{this.copyMessages}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showMoveMessageButton}}
|
||||
{{#if this.enableMove}}
|
||||
<DButton
|
||||
@id="chat-move-to-channel-btn"
|
||||
@class="btn-secondary"
|
||||
@icon="sign-out-alt"
|
||||
@label="chat.selection.move_selection_to_channel"
|
||||
@title="chat.selection.move_selection_to_channel"
|
||||
@disabled={{not this.anyMessagesSelected}}
|
||||
@action={{action "openMoveMessageModal"}}
|
||||
@action={{this.openMoveMessageModal}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
|
@ -44,14 +33,16 @@
|
|||
@icon="times"
|
||||
@class="btn-secondary cancel-btn"
|
||||
@label="chat.selection.cancel"
|
||||
@title="chat.selection.cancel"
|
||||
@action={{this.cancelSelecting}}
|
||||
@action={{@pane.cancelSelecting}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{{#if this.showChatCopySuccess}}
|
||||
<div class="chat-selection-message">
|
||||
{{#if this.showCopySuccess}}
|
||||
<span
|
||||
class="chat-selection-management__copy-success"
|
||||
{{chat/on-animation-end (fn (mut this.showCopySuccess) false)}}
|
||||
>
|
||||
{{i18n "chat.quote.copy_success"}}
|
||||
</div>
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
|
@ -0,0 +1,103 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import { clipboardCopyAsync } from "discourse/lib/utilities";
|
||||
import { getOwner } from "discourse-common/lib/get-owner";
|
||||
import { isTesting } from "discourse-common/config/environment";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
|
||||
export default class ChatSelectionManager extends Component {
|
||||
@service("composer") topicComposer;
|
||||
@service router;
|
||||
@service site;
|
||||
@service("chat-api") api;
|
||||
|
||||
@tracked showCopySuccess = false;
|
||||
|
||||
get enableMove() {
|
||||
return this.args.enableMove ?? false;
|
||||
}
|
||||
|
||||
get anyMessagesSelected() {
|
||||
return this.args.pane.selectedMessageIds.length > 0;
|
||||
}
|
||||
|
||||
@bind
|
||||
async generateQuote() {
|
||||
const { markdown } = await this.api.generateQuote(
|
||||
this.args.pane.channel.id,
|
||||
this.args.pane.selectedMessageIds
|
||||
);
|
||||
|
||||
return new Blob([markdown], { type: "text/plain" });
|
||||
}
|
||||
|
||||
@action
|
||||
openMoveMessageModal() {
|
||||
showModal("chat-message-move-to-channel-modal").setProperties({
|
||||
sourceChannel: this.args.pane.channel,
|
||||
selectedMessageIds: this.args.pane.selectedMessageIds,
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async quoteMessages() {
|
||||
let quoteMarkdown;
|
||||
|
||||
try {
|
||||
const quoteMarkdownBlob = await this.generateQuote();
|
||||
quoteMarkdown = await quoteMarkdownBlob.text();
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
|
||||
const openOpts = {};
|
||||
if (this.args.pane.channel.isCategoryChannel) {
|
||||
openOpts.categoryId = this.args.pane.channel.chatableId;
|
||||
}
|
||||
|
||||
if (this.site.mobileView) {
|
||||
// go to the relevant chatable (e.g. category) and open the
|
||||
// composer to insert text
|
||||
if (this.args.pane.channel.chatableUrl) {
|
||||
this.router.transitionTo(this.args.pane.channel.chatableUrl);
|
||||
}
|
||||
|
||||
await this.topicComposer.focusComposer({
|
||||
fallbackToNewTopic: true,
|
||||
insertText: quoteMarkdown,
|
||||
openOpts,
|
||||
});
|
||||
} else {
|
||||
// open the composer and insert text, reply to the current
|
||||
// topic if there is one, use the active draft if there is one
|
||||
const container = getOwner(this);
|
||||
const topic = container.lookup("controller:topic");
|
||||
await this.topicComposer.focusComposer({
|
||||
fallbackToNewTopic: true,
|
||||
topic: topic?.model,
|
||||
insertText: quoteMarkdown,
|
||||
openOpts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async copyMessages() {
|
||||
try {
|
||||
this.showCopySuccess = false;
|
||||
|
||||
if (!isTesting()) {
|
||||
// clipboard API throws errors in tests
|
||||
await clipboardCopyAsync(this.generateQuote);
|
||||
}
|
||||
|
||||
this.showCopySuccess = true;
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -336,4 +336,8 @@ export default class ChatChannel {
|
|||
method: "PUT",
|
||||
});
|
||||
}
|
||||
|
||||
clearSelectedMessages() {
|
||||
this.selectedMessages.forEach((message) => (message.selected = false));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,6 +79,10 @@ export default class ChatThread {
|
|||
return this.messagesManager.findLastUserMessage(user);
|
||||
}
|
||||
|
||||
clearSelectedMessages() {
|
||||
this.selectedMessages.forEach((message) => (message.selected = false));
|
||||
}
|
||||
|
||||
get routeModels() {
|
||||
return [...this.channel.routeModels, this.id];
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import Modifier from "ember-modifier";
|
||||
import { registerDestructor } from "@ember/destroyable";
|
||||
import { cancel, schedule } from "@ember/runloop";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
|
||||
export default class ChatOnAnimationEnd extends Modifier {
|
||||
constructor(owner, args) {
|
||||
super(owner, args);
|
||||
registerDestructor(this, (instance) => instance.cleanup());
|
||||
}
|
||||
|
||||
modify(element, [fn]) {
|
||||
this.element = element;
|
||||
this.fn = fn;
|
||||
|
||||
this.handler = schedule("afterRender", () => {
|
||||
this.element.addEventListener("animationend", this.handleAnimationEnd);
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
handleAnimationEnd() {
|
||||
if (this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.fn?.(this.element);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
cancel(this.handler);
|
||||
this.element?.removeEventListener("animationend", this.handleAnimationEnd);
|
||||
}
|
||||
}
|
|
@ -431,6 +431,19 @@ export default class ChatApi extends Service {
|
|||
return this.#putRequest(`/channels/${channelId}/threads/${threadId}`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a quote for a list of messages.
|
||||
*
|
||||
* @param {number} channelId - The ID of the channel containing the messages.
|
||||
* @param {Array<number>} messageIds - The IDs of the messages to quote.
|
||||
*/
|
||||
generateQuote(channelId, messageIds) {
|
||||
return ajax(`/chat/${channelId}/quote`, {
|
||||
type: "POST",
|
||||
data: { message_ids: messageIds },
|
||||
});
|
||||
}
|
||||
|
||||
get #basePath() {
|
||||
return "/chat/api";
|
||||
}
|
||||
|
|
|
@ -10,21 +10,22 @@ export default class ChatChannelPane extends Service {
|
|||
@tracked lastSelectedMessage = null;
|
||||
@tracked sending = false;
|
||||
|
||||
get selectedMessageIds() {
|
||||
return this.chat.activeChannel?.selectedMessages?.mapBy("id") || [];
|
||||
}
|
||||
|
||||
get channel() {
|
||||
return this.chat.activeChannel;
|
||||
}
|
||||
|
||||
@action
|
||||
cancelSelecting(selectedMessages) {
|
||||
this.selectingMessages = false;
|
||||
get selectedMessages() {
|
||||
return this.channel?.selectedMessages;
|
||||
}
|
||||
|
||||
selectedMessages.forEach((message) => {
|
||||
message.selected = false;
|
||||
});
|
||||
get selectedMessageIds() {
|
||||
return this.selectedMessages.mapBy("id");
|
||||
}
|
||||
|
||||
@action
|
||||
cancelSelecting() {
|
||||
this.selectingMessages = false;
|
||||
this.channel.clearSelectedMessages();
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -32,8 +33,4 @@ export default class ChatChannelPane extends Service {
|
|||
this.lastSelectedMessage = message;
|
||||
this.selectingMessages = true;
|
||||
}
|
||||
|
||||
get lastMessage() {
|
||||
return this.chat.activeChannel.messages.lastObject;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,15 +5,20 @@ export default class ChatThreadPane extends ChatChannelPane {
|
|||
@service chat;
|
||||
@service router;
|
||||
|
||||
get thread() {
|
||||
return this.channel?.activeThread;
|
||||
}
|
||||
|
||||
get isOpened() {
|
||||
return this.router.currentRoute.name === "chat.channel.thread";
|
||||
}
|
||||
|
||||
get selectedMessages() {
|
||||
return this.thread?.selectedMessages;
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.router.transitionTo(
|
||||
"chat.channel",
|
||||
...this.chat.activeChannel.routeModels
|
||||
);
|
||||
await this.router.transitionTo("chat.channel", ...this.channel.routeModels);
|
||||
}
|
||||
|
||||
async open(thread) {
|
||||
|
@ -22,8 +27,4 @@ export default class ChatThreadPane extends ChatChannelPane {
|
|||
...thread.routeModels
|
||||
);
|
||||
}
|
||||
|
||||
get selectedMessageIds() {
|
||||
return this.chat.activeChannel.activeThread.selectedMessages.mapBy("id");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-selection-management-buttons {
|
||||
&__buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
||||
|
@ -18,7 +18,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.chat-selection-message {
|
||||
&__copy-success {
|
||||
animation: chat-quote-message-background-fade-highlight 2s ease-out 3s;
|
||||
animation-fill-mode: forwards;
|
||||
background-color: var(--success-low);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.chat-selection-management {
|
||||
.chat-selection-management-buttons {
|
||||
&__buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
|
|
@ -1,112 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe "Channel message selection", type: :system do
|
||||
fab!(:current_user) { Fabricate(:user) }
|
||||
fab!(:channel_1) { Fabricate(:chat_channel) }
|
||||
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel_1) }
|
||||
fab!(:message_2) { Fabricate(:chat_message, chat_channel: channel_1) }
|
||||
fab!(:message_3) { Fabricate(:chat_message, chat_channel: channel_1) }
|
||||
|
||||
let(:chat_page) { PageObjects::Pages::Chat.new }
|
||||
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||
let(:thread_page) { PageObjects::Pages::ChatThread.new }
|
||||
|
||||
before do
|
||||
chat_system_bootstrap
|
||||
channel_1.add(current_user)
|
||||
sign_in(current_user)
|
||||
end
|
||||
|
||||
it "can select multiple messages" do
|
||||
chat_page.visit_channel(channel_1)
|
||||
channel_page.select_message(message_1)
|
||||
|
||||
expect(page).to have_css(".chat-selection-management")
|
||||
|
||||
channel_page.message_by_id(message_2.id).find(".chat-message-selector").click
|
||||
expect(page).to have_css(".chat-message-selector:checked", count: 2)
|
||||
end
|
||||
|
||||
it "can shift + click to select messages between the first and last" do
|
||||
chat_page.visit_channel(channel_1)
|
||||
channel_page.select_message(message_1)
|
||||
|
||||
expect(page).to have_css(".chat-selection-management")
|
||||
|
||||
channel_page.message_by_id(message_3.id).find(".chat-message-selector").click(:shift)
|
||||
expect(page).to have_css(".chat-message-selector:checked", count: 3)
|
||||
end
|
||||
|
||||
context "when visiting another channel" do
|
||||
fab!(:channel_2) { Fabricate(:chat_channel) }
|
||||
|
||||
before { channel_2.add(current_user) }
|
||||
|
||||
it "resets message selection" do
|
||||
chat_page.visit_channel(channel_1)
|
||||
channel_page.select_message(message_1)
|
||||
|
||||
expect(page).to have_css(".chat-selection-management")
|
||||
|
||||
chat_page.visit_channel(channel_2)
|
||||
|
||||
expect(page).to have_no_css(".chat-selection-management")
|
||||
end
|
||||
end
|
||||
|
||||
context "when in a thread" do
|
||||
fab!(:thread_message_1) do
|
||||
Chat::MessageCreator.create(
|
||||
chat_channel: channel_1,
|
||||
in_reply_to_id: message_1.id,
|
||||
user: Fabricate(:user),
|
||||
content: Faker::Lorem.paragraph,
|
||||
).chat_message
|
||||
end
|
||||
|
||||
fab!(:thread_message_2) do
|
||||
Chat::MessageCreator.create(
|
||||
chat_channel: channel_1,
|
||||
in_reply_to_id: message_1.id,
|
||||
user: Fabricate(:user),
|
||||
content: Faker::Lorem.paragraph,
|
||||
).chat_message
|
||||
end
|
||||
|
||||
fab!(:thread_message_3) do
|
||||
Chat::MessageCreator.create(
|
||||
chat_channel: channel_1,
|
||||
in_reply_to_id: message_1.id,
|
||||
user: Fabricate(:user),
|
||||
content: Faker::Lorem.paragraph,
|
||||
).chat_message
|
||||
end
|
||||
|
||||
before do
|
||||
SiteSetting.enable_experimental_chat_threaded_discussions = true
|
||||
channel_1.update!(threading_enabled: true)
|
||||
end
|
||||
|
||||
it "can select multiple messages" do
|
||||
chat_page.visit_thread(thread_message_1.thread)
|
||||
thread_page.select_message(thread_message_1)
|
||||
|
||||
expect(thread_page).to have_css(".chat-selection-management")
|
||||
|
||||
thread_page.message_by_id(thread_message_2.id).find(".chat-message-selector").click
|
||||
|
||||
expect(thread_page).to have_css(".chat-message-selector:checked", count: 2)
|
||||
end
|
||||
|
||||
it "can shift + click to select messages between the first and last" do
|
||||
chat_page.visit_thread(thread_message_1.thread)
|
||||
thread_page.select_message(thread_message_1)
|
||||
|
||||
expect(thread_page).to have_css(".chat-selection-management")
|
||||
|
||||
thread_page.message_by_id(thread_message_3.id).find(".chat-message-selector").click(:shift)
|
||||
|
||||
expect(page).to have_css(".chat-message-selector:checked", count: 3)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -18,9 +18,9 @@ RSpec.describe "Move message to channel", type: :system do
|
|||
|
||||
it "is not available" do
|
||||
chat_page.visit_channel(channel_1)
|
||||
channel_page.select_message(message_1)
|
||||
channel_page.messages.select(message_1)
|
||||
|
||||
expect(page).to have_no_content(I18n.t("js.chat.selection.move_selection_to_channel"))
|
||||
expect(channel_page.selection_management).to have_no_move_action
|
||||
end
|
||||
|
||||
context "when can moderate channel" do
|
||||
|
@ -37,9 +37,9 @@ RSpec.describe "Move message to channel", type: :system do
|
|||
|
||||
it "is available" do
|
||||
chat_page.visit_channel(channel_1)
|
||||
channel_page.select_message(message_1)
|
||||
channel_page.messages.select(message_1)
|
||||
|
||||
expect(page).to have_content(I18n.t("js.chat.selection.move_selection_to_channel"))
|
||||
expect(channel_page.selection_management).to have_move_action
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -57,9 +57,9 @@ RSpec.describe "Move message to channel", type: :system do
|
|||
|
||||
it "is not available" do
|
||||
chat_page.visit_channel(dm_channel_1)
|
||||
channel_page.select_message(message_1)
|
||||
channel_page.messages.select(message_1)
|
||||
|
||||
expect(page).to have_no_content(I18n.t("js.chat.selection.move_selection_to_channel"))
|
||||
expect(channel_page.selection_management).to have_no_move_action
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -77,8 +77,8 @@ RSpec.describe "Move message to channel", type: :system do
|
|||
|
||||
it "moves the message" do
|
||||
chat_page.visit_channel(channel_1)
|
||||
channel_page.select_message(message_1)
|
||||
click_button(I18n.t("js.chat.selection.move_selection_to_channel"))
|
||||
channel_page.messages.select(message_1)
|
||||
channel_page.selection_management.move
|
||||
find(".chat-move-message-channel-chooser").click
|
||||
find("[data-value='#{channel_2.id}']").click
|
||||
click_button(I18n.t("js.chat.move_to_channel.confirm_move"))
|
||||
|
|
|
@ -11,6 +11,15 @@ module PageObjects
|
|||
@messages ||= PageObjects::Components::Chat::Messages.new(".chat-channel")
|
||||
end
|
||||
|
||||
def selection_management
|
||||
@selection_management ||=
|
||||
PageObjects::Components::Chat::SelectionManagement.new(".chat-channel")
|
||||
end
|
||||
|
||||
def has_selected_messages?(*messages)
|
||||
self.messages.has_selected_messages?(*messages)
|
||||
end
|
||||
|
||||
def replying_to?(message)
|
||||
find(".chat-channel .chat-reply", text: message.message)
|
||||
end
|
||||
|
@ -76,16 +85,6 @@ module PageObjects
|
|||
end
|
||||
end
|
||||
|
||||
def select_message(message)
|
||||
if page.has_css?("html.mobile-view", wait: 0)
|
||||
click_message_action_mobile(message, "select")
|
||||
else
|
||||
hover_message(message)
|
||||
click_more_button
|
||||
find("[data-value='select']").click
|
||||
end
|
||||
end
|
||||
|
||||
def click_more_button
|
||||
find(".more-buttons").click
|
||||
end
|
||||
|
|
|
@ -20,6 +20,15 @@ module PageObjects
|
|||
@header ||= PageObjects::Components::Chat::ThreadHeader.new(".chat-thread")
|
||||
end
|
||||
|
||||
def selection_management
|
||||
@selection_management ||=
|
||||
PageObjects::Components::Chat::SelectionManagement.new(".chat-channel")
|
||||
end
|
||||
|
||||
def has_selected_messages?(*messages)
|
||||
self.messages.has_selected_messages?(*messages)
|
||||
end
|
||||
|
||||
def close
|
||||
header.find(".chat-thread__close").click
|
||||
end
|
||||
|
@ -110,12 +119,6 @@ module PageObjects
|
|||
".chat-thread .chat-messages-container .chat-message-container[data-id=\"#{id}\"]"
|
||||
end
|
||||
|
||||
def select_message(message)
|
||||
hover_message(message)
|
||||
click_more_button
|
||||
find("[data-value='select']").click
|
||||
end
|
||||
|
||||
def has_deleted_message?(message, count: 1)
|
||||
has_css?(
|
||||
".chat-thread .chat-message-container[data-id=\"#{message.id}\"] .chat-message-deleted",
|
||||
|
|
|
@ -5,6 +5,7 @@ module PageObjects
|
|||
module Chat
|
||||
class Message < PageObjects::Components::Base
|
||||
attr_reader :context
|
||||
attr_reader :component
|
||||
|
||||
SELECTOR = ".chat-message-container"
|
||||
|
||||
|
@ -16,31 +17,80 @@ module PageObjects
|
|||
exists?(**args, does_not_exist: true)
|
||||
end
|
||||
|
||||
def exists?(**args)
|
||||
text = args[:text]
|
||||
def hover
|
||||
message_by_id(message.id).hover
|
||||
end
|
||||
|
||||
selectors = SELECTOR
|
||||
selectors += "[data-id=\"#{args[:id]}\"]" if args[:id]
|
||||
selectors += ".is-persisted" if args[:persisted]
|
||||
selectors += ".is-staged" if args[:staged]
|
||||
def select(shift: false)
|
||||
if component[:class].include?("selecting-message")
|
||||
message_selector = component.find(".chat-message-selector")
|
||||
if shift
|
||||
message_selector.click(:shift)
|
||||
else
|
||||
message_selector.click
|
||||
end
|
||||
|
||||
if args[:deleted]
|
||||
selectors += ".is-deleted"
|
||||
text = I18n.t("js.chat.deleted", count: args[:deleted])
|
||||
return
|
||||
end
|
||||
|
||||
if page.has_css?("html.mobile-view", wait: 0)
|
||||
component.click(delay: 0.6)
|
||||
page.find(".chat-message-actions [data-id=\"select\"]").click
|
||||
else
|
||||
component.hover
|
||||
click_more_button
|
||||
page.find("[data-value='select']").click
|
||||
end
|
||||
end
|
||||
|
||||
def find(**args)
|
||||
selector = build_selector(**args)
|
||||
text = args[:text]
|
||||
text = I18n.t("js.chat.deleted", count: args[:deleted]) if args[:deleted]
|
||||
|
||||
if text
|
||||
@component =
|
||||
find(context).find("#{selector} .chat-message-text", text: /#{Regexp.escape(text)}/)
|
||||
else
|
||||
@component = page.find(context).find(selector)
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def exists?(**args)
|
||||
selector = build_selector(**args)
|
||||
text = args[:text]
|
||||
text = I18n.t("js.chat.deleted", count: args[:deleted]) if args[:deleted]
|
||||
|
||||
selector_method = args[:does_not_exist] ? :has_no_selector? : :has_selector?
|
||||
|
||||
if text
|
||||
find(context).send(
|
||||
page.find(context).send(
|
||||
selector_method,
|
||||
selectors + " " + ".chat-message-text",
|
||||
selector + " " + ".chat-message-text",
|
||||
text: /#{Regexp.escape(text)}/,
|
||||
)
|
||||
else
|
||||
find(context).send(selector_method, selectors)
|
||||
page.find(context).send(selector_method, selector)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def click_more_button
|
||||
page.find(".more-buttons").click
|
||||
end
|
||||
|
||||
def build_selector(**args)
|
||||
selector = SELECTOR
|
||||
selector += "[data-id=\"#{args[:id]}\"]" if args[:id]
|
||||
selector += "[data-selected]" if args[:selected]
|
||||
selector += ".is-persisted" if args[:persisted]
|
||||
selector += ".is-staged" if args[:staged]
|
||||
selector += ".is-deleted" if args[:deleted]
|
||||
selector
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,11 +13,23 @@ module PageObjects
|
|||
end
|
||||
|
||||
def component
|
||||
find(context)
|
||||
page.find(context)
|
||||
end
|
||||
|
||||
def message
|
||||
PageObjects::Components::Chat::Message.new(context + " " + SELECTOR)
|
||||
def select(args)
|
||||
find(args).select
|
||||
end
|
||||
|
||||
def shift_select(args)
|
||||
find(args).select(shift: true)
|
||||
end
|
||||
|
||||
def find(args)
|
||||
if args.is_a?(Hash)
|
||||
message.find(**args)
|
||||
else
|
||||
message.find(id: args.id)
|
||||
end
|
||||
end
|
||||
|
||||
def has_message?(**args)
|
||||
|
@ -27,6 +39,16 @@ module PageObjects
|
|||
def has_no_message?(**args)
|
||||
message.does_not_exist?(**args)
|
||||
end
|
||||
|
||||
def has_selected_messages?(*messages)
|
||||
messages.all? { |message| has_message?(id: message.id, selected: true) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def message
|
||||
PageObjects::Components::Chat::Message.new("#{context} #{SELECTOR}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module PageObjects
|
||||
module Components
|
||||
module Chat
|
||||
class SelectionManagement < PageObjects::Components::Base
|
||||
attr_reader :context
|
||||
|
||||
SELECTOR = ".chat-selection-management"
|
||||
|
||||
def initialize(context)
|
||||
@context = context
|
||||
end
|
||||
|
||||
def visible?
|
||||
find(context).has_css?(SELECTOR)
|
||||
end
|
||||
|
||||
def not_visible?
|
||||
find(context).has_no_css?(SELECTOR)
|
||||
end
|
||||
|
||||
def has_no_move_action?
|
||||
has_no_button?(selector_for("move"))
|
||||
end
|
||||
|
||||
def has_move_action?
|
||||
has_button?(selector_for("move"))
|
||||
end
|
||||
|
||||
def component
|
||||
find(context).find(SELECTOR)
|
||||
end
|
||||
|
||||
def cancel
|
||||
click_button("cancel")
|
||||
end
|
||||
|
||||
def quote
|
||||
click_button("quote")
|
||||
end
|
||||
|
||||
def copy
|
||||
click_button("copy")
|
||||
end
|
||||
|
||||
def move
|
||||
click_button("move")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def selector_for(action)
|
||||
case action
|
||||
when "quote"
|
||||
"chat-quote-btn"
|
||||
when "copy"
|
||||
"chat-copy-btn"
|
||||
when "cancel"
|
||||
"chat-cancel-selection-btn"
|
||||
when "move"
|
||||
"chat-move-to-channel-btn"
|
||||
end
|
||||
end
|
||||
|
||||
def click_button(action)
|
||||
find_button(selector_for(action), disabled: false).click
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe "Chat | Select message | channel", type: :system do
|
||||
fab!(:current_user) { Fabricate(:user) }
|
||||
fab!(:channel_1) { Fabricate(:chat_channel) }
|
||||
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel_1) }
|
||||
fab!(:message_2) { Fabricate(:chat_message, chat_channel: channel_1) }
|
||||
fab!(:message_3) { Fabricate(:chat_message, chat_channel: channel_1) }
|
||||
|
||||
let(:chat_page) { PageObjects::Pages::Chat.new }
|
||||
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||
|
||||
before do
|
||||
chat_system_bootstrap
|
||||
channel_1.add(current_user)
|
||||
sign_in(current_user)
|
||||
end
|
||||
|
||||
it "can select multiple messages" do
|
||||
chat_page.visit_channel(channel_1)
|
||||
|
||||
channel_page.messages.select(message_1)
|
||||
channel_page.messages.select(message_2)
|
||||
|
||||
expect(channel_page).to have_selected_messages(message_1, message_2)
|
||||
end
|
||||
|
||||
it "can shift + click to select messages between the first and last" do
|
||||
chat_page.visit_channel(channel_1)
|
||||
channel_page.messages.select(message_1)
|
||||
channel_page.messages.shift_select(message_3)
|
||||
|
||||
expect(channel_page).to have_selected_messages(message_1, message_2, message_3)
|
||||
end
|
||||
|
||||
context "when visiting another channel" do
|
||||
fab!(:channel_2) { Fabricate(:chat_channel) }
|
||||
|
||||
before { channel_2.add(current_user) }
|
||||
|
||||
it "resets message selection" do
|
||||
chat_page.visit_channel(channel_1)
|
||||
channel_page.messages.select(message_1)
|
||||
|
||||
expect(channel_page.selection_management).to be_visible
|
||||
|
||||
chat_page.visit_channel(channel_2)
|
||||
|
||||
expect(channel_page.selection_management).to be_not_visible
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,53 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe "Chat | Select message | thread", type: :system do
|
||||
fab!(:current_user) { Fabricate(:user) }
|
||||
fab!(:channel_1) { Fabricate(:chat_channel, threading_enabled: true) }
|
||||
fab!(:original_message) { Fabricate(:chat_message, chat_channel: channel_1) }
|
||||
|
||||
let(:chat_page) { PageObjects::Pages::Chat.new }
|
||||
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||
let(:thread_page) { PageObjects::Pages::ChatThread.new }
|
||||
|
||||
before do
|
||||
SiteSetting.enable_experimental_chat_threaded_discussions = true
|
||||
chat_system_bootstrap
|
||||
channel_1.add(current_user)
|
||||
sign_in(current_user)
|
||||
end
|
||||
|
||||
fab!(:thread_message_1) do
|
||||
Fabricate(:chat_message, chat_channel: channel_1, in_reply_to: original_message)
|
||||
end
|
||||
fab!(:thread_message_2) do
|
||||
Fabricate(:chat_message, chat_channel: channel_1, in_reply_to: original_message)
|
||||
end
|
||||
fab!(:thread_message_3) do
|
||||
Fabricate(:chat_message, chat_channel: channel_1, in_reply_to: original_message)
|
||||
end
|
||||
|
||||
before do
|
||||
SiteSetting.enable_experimental_chat_threaded_discussions = true
|
||||
channel_1.update!(threading_enabled: true)
|
||||
end
|
||||
|
||||
it "can select multiple messages" do
|
||||
chat_page.visit_thread(thread_message_1.thread)
|
||||
thread_page.messages.select(thread_message_1)
|
||||
thread_page.messages.select(thread_message_2)
|
||||
|
||||
expect(thread_page).to have_selected_messages(thread_message_1, thread_message_2)
|
||||
end
|
||||
|
||||
it "can shift + click to select messages between the first and last" do
|
||||
chat_page.visit_thread(thread_message_1.thread)
|
||||
thread_page.messages.select(thread_message_1)
|
||||
thread_page.messages.shift_select(thread_message_3)
|
||||
|
||||
expect(thread_page).to have_selected_messages(
|
||||
thread_message_1,
|
||||
thread_message_2,
|
||||
thread_message_3,
|
||||
)
|
||||
end
|
||||
end
|
|
@ -7,7 +7,7 @@ RSpec.describe "Quoting chat message transcripts", type: :system do
|
|||
|
||||
let(:cdp) { PageObjects::CDP.new }
|
||||
let(:chat_page) { PageObjects::Pages::Chat.new }
|
||||
let(:chat_channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||
let(:topic_page) { PageObjects::Pages::Topic.new }
|
||||
|
||||
before do
|
||||
|
@ -16,38 +16,11 @@ RSpec.describe "Quoting chat message transcripts", type: :system do
|
|||
sign_in(current_user)
|
||||
end
|
||||
|
||||
def select_message_desktop(message)
|
||||
if page.has_css?(".chat-message-container.selecting-messages", wait: 0)
|
||||
chat_channel_page.message_by_id(message.id).find(".chat-message-selector").click
|
||||
else
|
||||
chat_channel_page.message_by_id(message.id).hover
|
||||
expect(page).to have_css(".chat-message-actions .more-buttons")
|
||||
find(".chat-message-actions .more-buttons").click
|
||||
find(".select-kit-row[data-value=\"select\"]").click
|
||||
end
|
||||
end
|
||||
|
||||
def click_selection_button(button)
|
||||
selector =
|
||||
case button
|
||||
when "quote"
|
||||
"chat-quote-btn"
|
||||
when "copy"
|
||||
"chat-copy-btn"
|
||||
when "cancel"
|
||||
"chat-cancel-selection-btn"
|
||||
when "move"
|
||||
"chat-move-to-channel-btn"
|
||||
end
|
||||
find_button(selector, disabled: false, wait: 5).click
|
||||
end
|
||||
|
||||
def copy_messages_to_clipboard(messages)
|
||||
messages = Array.wrap(messages)
|
||||
messages.each { |message| select_message_desktop(message) }
|
||||
expect(chat_channel_page).to have_selection_management
|
||||
click_selection_button("copy")
|
||||
expect(page).to have_selector(".chat-copy-success")
|
||||
messages.each { |message| channel_page.messages.select(message) }
|
||||
channel_page.selection_management.copy
|
||||
expect(page).to have_selector(".chat-selection-management__copy-success")
|
||||
clip_text = cdp.read_clipboard
|
||||
expect(clip_text.chomp).to eq(generate_transcript(messages, current_user))
|
||||
clip_text
|
||||
|
@ -140,8 +113,8 @@ RSpec.describe "Quoting chat message transcripts", type: :system do
|
|||
chat_page.visit_channel(chat_channel_1)
|
||||
|
||||
clip_text = copy_messages_to_clipboard(message_1)
|
||||
click_selection_button("cancel")
|
||||
chat_channel_page.send_message(clip_text)
|
||||
channel_page.selection_management.cancel
|
||||
channel_page.send_message(clip_text)
|
||||
|
||||
expect(page).to have_selector(".chat-message", count: 2)
|
||||
expect(page).to have_css(".chat-transcript")
|
||||
|
@ -155,9 +128,8 @@ RSpec.describe "Quoting chat message transcripts", type: :system do
|
|||
|
||||
it "opens the topic composer with correct state" do
|
||||
chat_page.visit_channel(chat_channel_1)
|
||||
|
||||
select_message_desktop(message_1)
|
||||
click_selection_button("quote")
|
||||
channel_page.messages.select(message_1)
|
||||
channel_page.selection_management.quote
|
||||
|
||||
expect(topic_page).to have_expanded_composer
|
||||
expect(topic_page).to have_composer_content(generate_transcript(message_1, current_user))
|
||||
|
@ -181,8 +153,8 @@ RSpec.describe "Quoting chat message transcripts", type: :system do
|
|||
it "first navigates to the channel's category before opening the topic composer with the quote prefilled",
|
||||
mobile: true do
|
||||
chat_page.visit_channel(chat_channel_1)
|
||||
chat_channel_page.select_message(message_1)
|
||||
click_selection_button("quote")
|
||||
channel_page.messages.select(message_1)
|
||||
channel_page.selection_management.quote
|
||||
|
||||
expect(topic_page).to have_expanded_composer
|
||||
expect(topic_page).to have_composer_content(generate_transcript(message_1, current_user))
|
||||
|
|
Loading…
Reference in New Issue