mirror of
https://github.com/discourse/discourse.git
synced 2025-06-01 09:08:10 +08:00
UX: Simplify invite modal (#28974)
This commit simplifies the initial state of the invite modal when it's opened to make it one click away from creating an invite link. The existing options/fields within the invite modal are still available, but are now hidden behind an advanced mode which can be enabled. On the technical front, this PR also switches the invite modal to use our FormKit library. Internal topic: t/134023.
This commit is contained in:
297
spec/system/create_invite_spec.rb
Normal file
297
spec/system/create_invite_spec.rb
Normal file
@ -0,0 +1,297 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe "Creating Invites", type: :system do
|
||||
fab!(:group)
|
||||
fab!(:user) { Fabricate(:user, groups: [group]) }
|
||||
fab!(:topic) { Fabricate(:post).topic }
|
||||
let(:user_invited_pending_page) { PageObjects::Pages::UserInvitedPending.new }
|
||||
let(:create_invite_modal) { PageObjects::Modals::CreateInvite.new }
|
||||
let(:cdp) { PageObjects::CDP.new }
|
||||
|
||||
def open_invite_modal
|
||||
find(".user-invite-buttons .btn", match: :first).click
|
||||
end
|
||||
|
||||
def display_advanced_options
|
||||
create_invite_modal.edit_options_link.click
|
||||
end
|
||||
|
||||
before do
|
||||
SiteSetting.invite_allowed_groups = "#{group.id}"
|
||||
SiteSetting.invite_link_max_redemptions_limit_users = 7
|
||||
SiteSetting.invite_link_max_redemptions_limit = 63
|
||||
SiteSetting.invite_expiry_days = 3
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
before do
|
||||
user_invited_pending_page.visit(user)
|
||||
open_invite_modal
|
||||
end
|
||||
|
||||
it "is possible to create an invite link without toggling the advanced options" do
|
||||
cdp.allow_clipboard
|
||||
|
||||
create_invite_modal.save_button.click
|
||||
create_invite_modal.copy_button.click
|
||||
|
||||
invite_link = create_invite_modal.invite_link_input.value
|
||||
invite_key = invite_link.split("/").last
|
||||
|
||||
cdp.clipboard_has_text?(invite_link)
|
||||
|
||||
expect(create_invite_modal.link_limits_info_paragraph).to have_text(
|
||||
"Link is valid for up to 7 users and expires in 3 days.",
|
||||
)
|
||||
|
||||
create_invite_modal.close
|
||||
|
||||
expect(user_invited_pending_page.invites_list.size).to eq(1)
|
||||
|
||||
expect(user_invited_pending_page.latest_invite).to be_link_type(
|
||||
key: invite_key,
|
||||
redemption_count: 0,
|
||||
max_redemption_count: 7,
|
||||
)
|
||||
expect(user_invited_pending_page.latest_invite.expiry_date).to be_within(2.minutes).of(
|
||||
Time.zone.now + 3.days,
|
||||
)
|
||||
end
|
||||
|
||||
it "has the correct modal title when creating a new invite" do
|
||||
expect(create_invite_modal.header).to have_text(I18n.t("js.user.invited.invite.new_title"))
|
||||
end
|
||||
|
||||
it "hides the modal footer after creating an invite via simple mode" do
|
||||
expect(create_invite_modal).to have_footer
|
||||
create_invite_modal.save_button.click
|
||||
expect(create_invite_modal).to have_no_footer
|
||||
end
|
||||
|
||||
context "when editing an invite" do
|
||||
before do
|
||||
create_invite_modal.save_button.click
|
||||
create_invite_modal.close
|
||||
|
||||
expect(user_invited_pending_page.invites_list.size).to eq(1)
|
||||
|
||||
user_invited_pending_page.latest_invite.edit_button.click
|
||||
end
|
||||
|
||||
it "has the correct modal title" do
|
||||
expect(create_invite_modal.header).to have_text(I18n.t("js.user.invited.invite.edit_title"))
|
||||
end
|
||||
|
||||
it "displays the invite link and a copy button" do
|
||||
expect(create_invite_modal).to have_copy_button
|
||||
expect(create_invite_modal).to have_invite_link_input
|
||||
end
|
||||
end
|
||||
|
||||
context "with the advanced options" do
|
||||
before { display_advanced_options }
|
||||
|
||||
it "is possible to populate all the fields" do
|
||||
user.update!(admin: true)
|
||||
page.refresh
|
||||
open_invite_modal
|
||||
display_advanced_options
|
||||
|
||||
create_invite_modal.form.field("restrictTo").fill_in("discourse.org")
|
||||
create_invite_modal.form.field("maxRedemptions").fill_in("53")
|
||||
create_invite_modal.form.field("expiresAfterDays").select(90)
|
||||
|
||||
create_invite_modal.choose_topic(topic)
|
||||
create_invite_modal.choose_groups([group])
|
||||
|
||||
create_invite_modal.save_button.click
|
||||
|
||||
expect(create_invite_modal).to have_copy_button
|
||||
|
||||
invite_link = create_invite_modal.invite_link_input.value
|
||||
invite_key = invite_link.split("/").last
|
||||
|
||||
create_invite_modal.close
|
||||
|
||||
expect(user_invited_pending_page.invites_list.size).to eq(1)
|
||||
|
||||
expect(user_invited_pending_page.latest_invite).to be_link_type(
|
||||
key: invite_key,
|
||||
redemption_count: 0,
|
||||
max_redemption_count: 53,
|
||||
)
|
||||
expect(user_invited_pending_page.latest_invite).to have_group(group)
|
||||
expect(user_invited_pending_page.latest_invite).to have_topic(topic)
|
||||
expect(user_invited_pending_page.latest_invite.expiry_date).to be_within(2.minutes).of(
|
||||
Time.zone.now + 90.days,
|
||||
)
|
||||
end
|
||||
|
||||
it "is possible to create an email invite" do
|
||||
another_group = Fabricate(:group)
|
||||
user.update!(admin: true)
|
||||
page.refresh
|
||||
open_invite_modal
|
||||
display_advanced_options
|
||||
|
||||
create_invite_modal.form.field("restrictTo").fill_in("someone@discourse.org")
|
||||
create_invite_modal.form.field("expiresAfterDays").select(1)
|
||||
|
||||
create_invite_modal.choose_topic(topic)
|
||||
create_invite_modal.choose_groups([group, another_group])
|
||||
|
||||
create_invite_modal
|
||||
.form
|
||||
.field("customMessage")
|
||||
.fill_in("Hello someone, this is a test invite")
|
||||
|
||||
create_invite_modal.save_button.click
|
||||
|
||||
expect(create_invite_modal).to have_copy_button
|
||||
|
||||
invite_link = create_invite_modal.invite_link_input.value
|
||||
invite_key = invite_link.split("/").last
|
||||
|
||||
create_invite_modal.close
|
||||
|
||||
expect(user_invited_pending_page.invites_list.size).to eq(1)
|
||||
|
||||
expect(user_invited_pending_page.latest_invite).to be_email_type("someone@discourse.org")
|
||||
expect(user_invited_pending_page.latest_invite).to have_group(group)
|
||||
expect(user_invited_pending_page.latest_invite).to have_group(another_group)
|
||||
expect(user_invited_pending_page.latest_invite).to have_topic(topic)
|
||||
expect(user_invited_pending_page.latest_invite.expiry_date).to be_within(2.minutes).of(
|
||||
Time.zone.now + 1.day,
|
||||
)
|
||||
end
|
||||
|
||||
it "adds the invite_expiry_days site setting to the list of options for the expiresAfterDays field" do
|
||||
options =
|
||||
create_invite_modal
|
||||
.form
|
||||
.field("expiresAfterDays")
|
||||
.component
|
||||
.all(".form-kit__control-option")
|
||||
.map(&:text)
|
||||
expect(options).to eq(["1 day", "3 days", "7 days", "30 days", "90 days", "Never"])
|
||||
|
||||
SiteSetting.invite_expiry_days = 90
|
||||
page.refresh
|
||||
open_invite_modal
|
||||
display_advanced_options
|
||||
|
||||
options =
|
||||
create_invite_modal
|
||||
.form
|
||||
.field("expiresAfterDays")
|
||||
.component
|
||||
.all(".form-kit__control-option")
|
||||
.map(&:text)
|
||||
expect(options).to eq(["1 day", "7 days", "30 days", "90 days", "Never"])
|
||||
end
|
||||
|
||||
it "uses the invite_link_max_redemptions_limit_users setting as the default value for the maxRedemptions field if the setting is lower than 10" do
|
||||
expect(create_invite_modal.form.field("maxRedemptions").value).to eq("7")
|
||||
|
||||
SiteSetting.invite_link_max_redemptions_limit_users = 11
|
||||
page.refresh
|
||||
open_invite_modal
|
||||
display_advanced_options
|
||||
|
||||
expect(create_invite_modal.form.field("maxRedemptions").value).to eq("10")
|
||||
end
|
||||
|
||||
it "uses the invite_link_max_redemptions_limit setting as the default value for the maxRedemptions field for staff users if the setting is lower than 100" do
|
||||
user.update!(admin: true)
|
||||
page.refresh
|
||||
open_invite_modal
|
||||
display_advanced_options
|
||||
|
||||
expect(create_invite_modal.form.field("maxRedemptions").value).to eq("63")
|
||||
|
||||
SiteSetting.invite_link_max_redemptions_limit = 108
|
||||
page.refresh
|
||||
open_invite_modal
|
||||
display_advanced_options
|
||||
|
||||
expect(create_invite_modal.form.field("maxRedemptions").value).to eq("100")
|
||||
end
|
||||
|
||||
it "shows the inviteToGroups field for a normal user if they're owner on at least 1 group" do
|
||||
expect(create_invite_modal.form).to have_no_field_with_name("inviteToGroups")
|
||||
|
||||
group.add_owner(user)
|
||||
page.refresh
|
||||
open_invite_modal
|
||||
display_advanced_options
|
||||
|
||||
expect(create_invite_modal.form).to have_field_with_name("inviteToGroups")
|
||||
end
|
||||
|
||||
it "shows the inviteToGroups field for admins" do
|
||||
user.update!(admin: true)
|
||||
page.refresh
|
||||
open_invite_modal
|
||||
display_advanced_options
|
||||
|
||||
expect(create_invite_modal.form).to have_field_with_name("inviteToGroups")
|
||||
end
|
||||
|
||||
it "doesn't show the inviteToTopic field to normal users" do
|
||||
SiteSetting.must_approve_users = false
|
||||
page.refresh
|
||||
open_invite_modal
|
||||
display_advanced_options
|
||||
|
||||
expect(create_invite_modal.form).to have_no_field_with_name("inviteToTopic")
|
||||
end
|
||||
|
||||
it "shows the inviteToTopic field to admins if the must_approve_users setting is false" do
|
||||
user.update!(admin: true)
|
||||
SiteSetting.must_approve_users = false
|
||||
page.refresh
|
||||
open_invite_modal
|
||||
display_advanced_options
|
||||
|
||||
expect(create_invite_modal.form).to have_field_with_name("inviteToTopic")
|
||||
end
|
||||
|
||||
it "doesn't show the inviteToTopic field to admins if the must_approve_users setting is true" do
|
||||
user.update!(admin: true)
|
||||
SiteSetting.must_approve_users = true
|
||||
page.refresh
|
||||
open_invite_modal
|
||||
display_advanced_options
|
||||
|
||||
expect(create_invite_modal.form).to have_no_field_with_name("inviteToTopic")
|
||||
end
|
||||
|
||||
it "replaces the expiresAfterDays field with expiresAt with date and time controls after creating the invite" do
|
||||
create_invite_modal.form.field("expiresAfterDays").select(1)
|
||||
create_invite_modal.save_button.click
|
||||
now = Time.zone.now
|
||||
|
||||
expect(create_invite_modal.form).to have_no_field_with_name("expiresAfterDays")
|
||||
expect(create_invite_modal.form).to have_field_with_name("expiresAt")
|
||||
|
||||
expires_at_field = create_invite_modal.form.field("expiresAt").component
|
||||
date = expires_at_field.find(".date-picker").value
|
||||
time = expires_at_field.find(".time-input").value
|
||||
|
||||
expire_date = Time.parse("#{date} #{time}:#{now.strftime("%S")}").utc
|
||||
expect(expire_date).to be_within_one_minute_of(now + 1.day)
|
||||
end
|
||||
|
||||
context "when an email is given to the restrictTo field" do
|
||||
it "shows the customMessage field and hides the maxRedemptions field" do
|
||||
expect(create_invite_modal.form).to have_no_field_with_name("customMessage")
|
||||
expect(create_invite_modal.form).to have_field_with_name("maxRedemptions")
|
||||
|
||||
create_invite_modal.form.field("restrictTo").fill_in("discourse@cdck.org")
|
||||
|
||||
expect(create_invite_modal.form).to have_field_with_name("customMessage")
|
||||
expect(create_invite_modal.form).to have_no_field_with_name("maxRedemptions")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -114,7 +114,12 @@ module PageObjects
|
||||
picker.search(value)
|
||||
picker.select_row_by_value(value)
|
||||
when "select"
|
||||
component.find(".form-kit__control-option[value='#{value}']").click
|
||||
selector = component.find(".form-kit__control-select")
|
||||
selector.find(".form-kit__control-option[value='#{value}']").select_option
|
||||
selector.execute_script(<<~JS, selector)
|
||||
var selector = arguments[0];
|
||||
selector.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
|
||||
JS
|
||||
when "menu"
|
||||
trigger = component.find(".fk-d-menu__trigger.form-kit__control-menu")
|
||||
trigger.click
|
||||
@ -193,6 +198,14 @@ module PageObjects
|
||||
end
|
||||
end
|
||||
|
||||
def has_field_with_name?(name)
|
||||
has_css?(".form-kit__field[data-name='#{name}']")
|
||||
end
|
||||
|
||||
def has_no_field_with_name?(name)
|
||||
has_no_css?(".form-kit__field[data-name='#{name}']")
|
||||
end
|
||||
|
||||
def container(name)
|
||||
within component do
|
||||
FormKitContainer.new(find(".form-kit__container[data-name='#{name}']"))
|
||||
|
@ -8,6 +8,10 @@ module PageObjects
|
||||
|
||||
BODY_SELECTOR = ""
|
||||
|
||||
def header
|
||||
find(".d-modal__header")
|
||||
end
|
||||
|
||||
def body
|
||||
find(".d-modal__body#{BODY_SELECTOR}")
|
||||
end
|
||||
@ -16,6 +20,14 @@ module PageObjects
|
||||
find(".d-modal__footer")
|
||||
end
|
||||
|
||||
def has_footer?
|
||||
has_css?(".d-modal__footer")
|
||||
end
|
||||
|
||||
def has_no_footer?
|
||||
has_no_css?(".d-modal__footer")
|
||||
end
|
||||
|
||||
def close
|
||||
find(".modal-close").click
|
||||
end
|
||||
|
57
spec/system/page_objects/modals/create_invite.rb
Normal file
57
spec/system/page_objects/modals/create_invite.rb
Normal file
@ -0,0 +1,57 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module PageObjects
|
||||
module Modals
|
||||
class CreateInvite < PageObjects::Modals::Base
|
||||
def modal
|
||||
find(".create-invite-modal")
|
||||
end
|
||||
|
||||
def edit_options_link
|
||||
within(modal) { find(".edit-link-options") }
|
||||
end
|
||||
|
||||
def save_button
|
||||
within(modal) { find(".save-invite") }
|
||||
end
|
||||
|
||||
def copy_button
|
||||
within(modal) { find(".copy-button") }
|
||||
end
|
||||
|
||||
def has_copy_button?
|
||||
within(modal) { has_css?(".copy-button") }
|
||||
end
|
||||
|
||||
def has_invite_link_input?
|
||||
within(modal) { has_css?("input.invite-link") }
|
||||
end
|
||||
|
||||
def invite_link_input
|
||||
within(modal) { find("input.invite-link") }
|
||||
end
|
||||
|
||||
def link_limits_info_paragraph
|
||||
within(modal) { find("p.link-limits-info") }
|
||||
end
|
||||
|
||||
def form
|
||||
PageObjects::Components::FormKit.new(".create-invite-modal .form-kit")
|
||||
end
|
||||
|
||||
def choose_topic(topic)
|
||||
topic_picker = PageObjects::Components::SelectKit.new(".topic-chooser")
|
||||
topic_picker.expand
|
||||
topic_picker.search(topic.id)
|
||||
topic_picker.select_row_by_index(0)
|
||||
end
|
||||
|
||||
def choose_groups(groups)
|
||||
group_picker = PageObjects::Components::SelectKit.new(".group-chooser")
|
||||
group_picker.expand
|
||||
groups.each { |group| group_picker.select_row_by_value(group.id) }
|
||||
group_picker.collapse
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
73
spec/system/page_objects/pages/user_invited_pending.rb
Normal file
73
spec/system/page_objects/pages/user_invited_pending.rb
Normal file
@ -0,0 +1,73 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module PageObjects
|
||||
module Pages
|
||||
class UserInvitedPending < PageObjects::Pages::Base
|
||||
class Invite
|
||||
attr_reader :tr_element
|
||||
|
||||
def initialize(tr_element)
|
||||
@tr_element = tr_element
|
||||
end
|
||||
|
||||
def link_type?(key: nil, redemption_count: nil, max_redemption_count: nil)
|
||||
if key && redemption_count && max_redemption_count
|
||||
invite_type_col.has_text?(
|
||||
I18n.t(
|
||||
"js.user.invited.invited_via_link",
|
||||
key: "#{key[0...4]}...",
|
||||
count: redemption_count,
|
||||
max: max_redemption_count,
|
||||
),
|
||||
)
|
||||
else
|
||||
invite_type_col.has_css?(".d-icon-link")
|
||||
end
|
||||
end
|
||||
|
||||
def email_type?(email)
|
||||
invite_type_col.has_text?(email) && invite_type_col.has_css?(".d-icon-envelope")
|
||||
end
|
||||
|
||||
def has_group?(group)
|
||||
invite_type_col.has_css?(".invite-extra", text: group.name)
|
||||
end
|
||||
|
||||
def has_topic?(topic)
|
||||
invite_type_col.has_css?(".invite-extra", text: topic.title)
|
||||
end
|
||||
|
||||
def edit_button
|
||||
tr_element.find(".invite-actions .btn-default")
|
||||
end
|
||||
|
||||
def expiry_date
|
||||
Time.parse(tr_element.find(".invite-expires-at").text).utc
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def invite_type_col
|
||||
tr_element.find(".invite-type")
|
||||
end
|
||||
end
|
||||
|
||||
def visit(user)
|
||||
url = "/u/#{user.username_lower}/invited/pending"
|
||||
page.visit(url)
|
||||
end
|
||||
|
||||
def invite_button
|
||||
find("#user-content .invite-button")
|
||||
end
|
||||
|
||||
def invites_list
|
||||
all("#user-content .user-invite-list tbody tr").map { |row| Invite.new(row) }
|
||||
end
|
||||
|
||||
def latest_invite
|
||||
Invite.new(find("#user-content .user-invite-list tbody tr:first-of-type"))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Reference in New Issue
Block a user