UX: Use icons as bulleted list in invite modal (#13229)

This commit is contained in:
Dan Ungureanu
2021-06-02 16:28:54 +03:00
committed by GitHub
parent d2135b23c4
commit 447d8dfc44
7 changed files with 117 additions and 115 deletions
@@ -1,11 +1,11 @@
import Controller from "@ember/controller"; import Controller from "@ember/controller";
import { action } from "@ember/object"; import { action } from "@ember/object";
import { equal } from "@ember/object/computed"; import { empty, notEmpty } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
import { extractError } from "discourse/lib/ajax-error"; import { extractError } from "discourse/lib/ajax-error";
import { getNativeContact } from "discourse/lib/pwa-utils";
import { bufferedProperty } from "discourse/mixins/buffered-content"; import { bufferedProperty } from "discourse/mixins/buffered-content";
import ModalFunctionality from "discourse/mixins/modal-functionality"; import ModalFunctionality from "discourse/mixins/modal-functionality";
import { getNativeContact } from "discourse/lib/pwa-utils";
import Group from "discourse/models/group"; import Group from "discourse/models/group";
import Invite from "discourse/models/invite"; import Invite from "discourse/models/invite";
import I18n from "I18n"; import I18n from "I18n";
@@ -24,7 +24,8 @@ export default Controller.extend(
limitToEmail: false, limitToEmail: false,
autogenerated: false, autogenerated: false,
type: "link", isLink: empty("buffered.email"),
isEmail: notEmpty("buffered.email"),
onShow() { onShow() {
Group.findAll().then((groups) => { Group.findAll().then((groups) => {
@@ -52,10 +53,7 @@ export default Controller.extend(
}, },
setInvite(invite) { setInvite(invite) {
this.setProperties({ this.set("invite", invite);
invite,
type: invite.email ? "email" : "link",
});
}, },
setAutogenerated(value) { setAutogenerated(value) {
@@ -70,7 +68,7 @@ export default Controller.extend(
const data = { ...this.buffered.buffer }; const data = { ...this.buffered.buffer };
if (data.groupIds !== undefined) { if (data.groupIds !== undefined) {
data.group_ids = data.groupIds; data.group_ids = data.groupIds.length > 0 ? data.groupIds : "";
delete data.groupIds; delete data.groupIds;
} }
@@ -80,13 +78,12 @@ export default Controller.extend(
delete data.topicTitle; delete data.topicTitle;
} }
if (this.type === "link") { if (this.isLink) {
if (this.buffered.get("email")) { if (this.invite.email) {
data.email = ""; data.email = data.custom_message = "";
data.custom_message = "";
} }
} else if (this.type === "email") { } else if (this.isEmail) {
if (this.buffered.get("max_redemptions_allowed") > 1) { if (this.invite.max_redemptions_allowed > 1) {
data.max_redemptions_allowed = 1; data.max_redemptions_allowed = 1;
} }
@@ -106,7 +103,7 @@ export default Controller.extend(
this.rollbackBuffer(); this.rollbackBuffer();
this.setAutogenerated(opts.autogenerated); this.setAutogenerated(opts.autogenerated);
if (!this.autogenerated) { if (!this.autogenerated) {
if (this.type === "email" && opts.sendEmail) { if (this.isEmail && opts.sendEmail) {
this.send("closeModal"); this.send("closeModal");
} else { } else {
this.appEvents.trigger("modal-body:flash", { this.appEvents.trigger("modal-body:flash", {
@@ -126,9 +123,6 @@ export default Controller.extend(
); );
}, },
isLink: equal("type", "link"),
isEmail: equal("type", "email"),
@discourseComputed( @discourseComputed(
"currentUser.staff", "currentUser.staff",
"siteSettings.invite_link_max_redemptions_limit", "siteSettings.invite_link_max_redemptions_limit",
@@ -156,46 +150,16 @@ export default Controller.extend(
return staff || groups.any((g) => g.owner); return staff || groups.any((g) => g.owner);
}, },
@discourseComputed("type", "buffered.email") @discourseComputed("currentUser.staff", "isEmail", "canInviteToGroup")
disabled(type, email) { hasAdvanced(staff, isEmail, canInviteToGroup) {
if (type === "email") { return staff || isEmail || canInviteToGroup;
return !email;
}
return false;
},
@discourseComputed("buffered.hasBufferedChanges", "invite.email", "type")
changed(hasBufferedChanges, inviteEmail, type) {
return hasBufferedChanges || (inviteEmail ? "email" : "link") !== type;
},
@discourseComputed("currentUser.staff", "type")
hasAdvanced(staff, type) {
return staff || type === "email";
}, },
@action @action
copied() { copied() {
if (this.type === "email" && !this.buffered.get("email")) {
return this.appEvents.trigger("modal-body:flash", {
text: I18n.t("user.invited.invite.blank_email"),
messageClass: "error",
});
}
this.save({ sendEmail: false, copy: true }); this.save({ sendEmail: false, copy: true });
}, },
@action
toggleLimitToEmail() {
const limitToEmail = !this.limitToEmail;
this.setProperties({
limitToEmail,
type: limitToEmail ? "email" : "link",
});
},
@action @action
saveInvite(sendEmail) { saveInvite(sendEmail) {
this.appEvents.trigger("modal-body:clearFlash"); this.appEvents.trigger("modal-body:clearFlash");
@@ -1,4 +1,6 @@
<label for="choose-topic-title">{{i18n labelText}}</label> <label for="choose-topic-title">
{{#if labelIcon}}{{d-icon labelIcon}}{{/if}}{{i18n labelText}}
</label>
{{text-field value=topicTitle placeholderKey="choose_topic.title.placeholder" id="choose-topic-title"}} {{text-field value=topicTitle placeholderKey="choose_topic.title.placeholder" id="choose-topic-title"}}
@@ -1,6 +1,8 @@
<div class="future-date-input"> <div class="future-date-input">
<div class="control-group"> <div class="control-group">
<label class={{labelClasses}}>{{displayLabel}}</label> <label class={{labelClasses}}>
{{#if displayLabelIcon}}{{d-icon displayLabelIcon}}{{/if}}{{displayLabel}}
</label>
{{future-date-input-selector {{future-date-input-selector
minimumResultsForSearch=-1 minimumResultsForSearch=-1
statusType=statusType statusType=statusType
@@ -13,33 +13,31 @@
</div> </div>
</div> </div>
<p>{{expiresAtLabel}}</p> <div class="input-group input-expires-at">
<label>{{d-icon "far-clock"}}{{expiresAtLabel}}</label>
</div>
<div class="input-group invite-type"> <div class="input-group input-email">
{{input type="checkbox" id="invite-type" checked=limitToEmail click=(action "toggleLimitToEmail")}} <label for="invite-email">{{d-icon "envelope"}}{{i18n "user.invited.invite.restrict_email"}}</label>
<label for="invite-type">{{i18n "user.invited.invite.restrict_email"}}</label> <div class="invite-input-with-button">
{{input
{{#if isEmail}} id="invite-email"
<div class="invite-input-with-button"> value=buffered.email
{{input placeholderKey="topic.invite_reply.email_placeholder"
id="invite-email" }}
value=buffered.email {{#if capabilities.hasContactPicker}}
placeholderKey="topic.invite_reply.email_placeholder" {{d-button
icon="address-book"
action=(action "searchContact")
class="btn-primary open-contact-picker"
}} }}
{{#if capabilities.hasContactPicker}} {{/if}}
{{d-button </div>
icon="address-book"
action=(action "searchContact")
class="btn-primary open-contact-picker"
}}
{{/if}}
</div>
{{/if}}
</div> </div>
{{#if isLink}} {{#if isLink}}
<div class="input-group invite-max-redemptions"> <div class="input-group invite-max-redemptions">
<label for="invite-max-redemptions">{{i18n "user.invited.invite.max_redemptions_allowed"}}</label> <label for="invite-max-redemptions">{{d-icon "users"}}{{i18n "user.invited.invite.max_redemptions_allowed"}}</label>
{{input {{input
id="invite-max-redemptions" id="invite-max-redemptions"
type="number" type="number"
@@ -50,10 +48,10 @@
</div> </div>
{{/if}} {{/if}}
{{#if isEmail}} {{#if showAdvanced}}
{{#if showAdvanced}} {{#if isEmail}}
<div class="input-group invite-custom-message"> <div class="input-group invite-custom-message">
<label for="invite-message">{{i18n "user.invited.invite.custom_message"}}</label> <label for="invite-message">{{d-icon "envelope"}}{{i18n "user.invited.invite.custom_message"}}</label>
{{textarea id="invite-message" value=buffered.custom_message}} {{textarea id="invite-message" value=buffered.custom_message}}
</div> </div>
{{/if}} {{/if}}
@@ -66,12 +64,13 @@
selectedTopicId=buffered.topicId selectedTopicId=buffered.topicId
topicTitle=buffered.topicTitle topicTitle=buffered.topicTitle
additionalFilters="status:public" additionalFilters="status:public"
labelIcon="hand-point-right"
label="user.invited.invite.invite_to_topic" label="user.invited.invite.invite_to_topic"
}} }}
</div> </div>
{{else if buffered.topicTitle}} {{else if buffered.topicTitle}}
<div class="input-group"> <div class="input-group invite-to-topic">
<label for="invite-topic">{{i18n "user.invited.invite.invite_to_topic"}}</label> <label for="invite-topic">{{d-icon "hand-point-right"}}{{i18n "user.invited.invite.invite_to_topic"}}</label>
{{input {{input
name="invite-topic" name="invite-topic"
class="invite-topic" class="invite-topic"
@@ -85,7 +84,7 @@
{{#if showAdvanced}} {{#if showAdvanced}}
{{#if canInviteToGroup}} {{#if canInviteToGroup}}
<div class="input-group invite-to-groups"> <div class="input-group invite-to-groups">
<label>{{i18n "user.invited.invite.add_to_groups"}}</label> <label>{{d-icon "users"}}{{i18n "user.invited.invite.add_to_groups"}}</label>
{{group-chooser {{group-chooser
content=allGroups content=allGroups
value=buffered.groupIds value=buffered.groupIds
@@ -100,6 +99,7 @@
{{#if currentUser.staff}} {{#if currentUser.staff}}
<div class="input-group invite-expires-at"> <div class="input-group invite-expires-at">
{{future-date-input {{future-date-input
displayLabelIcon="far-clock"
displayLabel=(i18n "user.invited.invite.expires_at") displayLabel=(i18n "user.invited.invite.expires_at")
includeDateTime=true includeDateTime=true
includeMidFuture=true includeMidFuture=true
@@ -118,7 +118,6 @@
label="user.invited.invite.save_invite" label="user.invited.invite.save_invite"
class="btn-primary save-invite" class="btn-primary save-invite"
action=(action "saveInvite") action=(action "saveInvite")
disabled=disabled
}} }}
{{#if isEmail}} {{#if isEmail}}
@@ -127,7 +126,6 @@
label=(if invite.emailed "user.invited.reinvite" "user.invited.invite.send_invite_email") label=(if invite.emailed "user.invited.reinvite" "user.invited.invite.send_invite_email")
class="btn-primary send-invite" class="btn-primary send-invite"
action=(action "saveInvite" true) action=(action "saveInvite" true)
disabled=disabled
}} }}
{{/if}} {{/if}}
@@ -1,7 +1,6 @@
import { click, fillIn, visit } from "@ember/test-helpers"; import { click, fillIn, visit } from "@ember/test-helpers";
import { acceptance } from "discourse/tests/helpers/qunit-helpers"; import { acceptance } from "discourse/tests/helpers/qunit-helpers";
import { test } from "qunit"; import { test } from "qunit";
import I18n from "I18n";
acceptance("Invites - Create & Edit Invite Modal", function (needs) { acceptance("Invites - Create & Edit Invite Modal", function (needs) {
let deleted; let deleted;
@@ -23,7 +22,16 @@ acceptance("Invites - Create & Edit Invite Modal", function (needs) {
}; };
server.post("/invites", () => helper.response(inviteData)); server.post("/invites", () => helper.response(inviteData));
server.put("/invites/1", () => helper.response(inviteData)); server.put("/invites/1", (request) => {
const data = helper.parsePostData(request.requestBody);
if (data.email === "error") {
return helper.response(422, {
errors: ["error isn't a valid email address."],
});
} else {
return helper.response(inviteData);
}
});
server.delete("/invites", () => { server.delete("/invites", () => {
deleted = true; deleted = true;
@@ -95,11 +103,11 @@ acceptance("Invites - Create & Edit Invite Modal", function (needs) {
await visit("/u/eviltrout/invited/pending"); await visit("/u/eviltrout/invited/pending");
await click(".invite-controls .btn:first-child"); await click(".invite-controls .btn:first-child");
await click("#invite-type"); await fillIn("#invite-email", "error");
await click(".invite-link .btn"); await click(".invite-link .btn");
assert.equal( assert.equal(
find("#modal-alert").text(), find("#modal-alert").text(),
I18n.t("user.invited.invite.blank_email") "error isn't a valid email address."
); );
}); });
}); });
@@ -172,11 +180,9 @@ acceptance("Invites - Email Invites", function (needs) {
await visit("/u/eviltrout/invited/pending"); await visit("/u/eviltrout/invited/pending");
await click(".invite-controls .btn:first-child"); await click(".invite-controls .btn:first-child");
await click("#invite-type");
assert.ok(find("#invite-email").length, "shows email field"); assert.ok(find("#invite-email").length, "shows email field");
await fillIn("#invite-email", "test@example.com"); await fillIn("#invite-email", "test@example.com");
assert.ok(find(".save-invite").length, "shows save without email button"); assert.ok(find(".save-invite").length, "shows save without email button");
await click(".save-invite"); await click(".save-invite");
assert.ok( assert.ok(
+49 -18
View File
@@ -832,7 +832,7 @@
.create-invite-modal, .create-invite-modal,
.share-topic-modal { .share-topic-modal {
.input-group { .input-group {
margin-bottom: 1em; margin-bottom: 0.5em;
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
@@ -841,24 +841,24 @@
input[type="text"] { input[type="text"] {
width: 100%; width: 100%;
} }
}
.invite-type { textarea#invite-message,
input[type="checkbox"] { &.invite-to-topic input[type="text"],
display: inline; .group-chooser,
vertical-align: middle; .user-chooser,
margin-top: -1px; .future-date-input-selector {
margin-left: 25px;
width: calc(100% - 25px);
} }
label { &.invite-to-topic input[type="radio"] {
display: inline-block; margin-left: 10px;
} }
}
.group-chooser, label .d-icon {
.user-chooser, color: var(--primary-medium);
.future-date-input-selector { margin-right: 10px;
width: 100%; }
} }
.input-group input[type="text"], .input-group input[type="text"],
@@ -868,10 +868,6 @@
height: 34px; height: 34px;
} }
.input-group .btn {
vertical-align: top;
}
.invite-input-with-button { .invite-input-with-button {
display: flex; display: flex;
@@ -880,6 +876,16 @@
} }
} }
.input-group.input-expires-at,
.input-group.input-email,
.input-group.invite-max-redemptions {
margin-bottom: 0;
input[type="text"] {
width: unset;
}
}
.future-date-input { .future-date-input {
.date-picker-wrapper { .date-picker-wrapper {
input { input {
@@ -900,6 +906,25 @@
} }
} }
.input-group.input-email {
align-items: baseline;
display: flex;
label {
display: inline;
}
.invite-input-with-button {
display: inline-flex;
flex: 1;
input[type="text"] {
flex: 1;
margin-left: 5px;
}
}
}
.invite-max-redemptions { .invite-max-redemptions {
label { label {
display: inline; display: inline;
@@ -910,6 +935,12 @@
} }
} }
.invite-to-topic {
#choose-topic-title {
margin-bottom: 0;
}
}
.show-advanced { .show-advanced {
margin-left: auto; margin-left: auto;
margin-right: 0; margin-right: 0;
+7 -8
View File
@@ -1534,7 +1534,7 @@ en:
new_title: "Create Invite" new_title: "Create Invite"
edit_title: "Edit Invite" edit_title: "Edit Invite"
instructions: "Share this link to instantly grant access to this site:" instructions: "Share this link to instantly grant access to this site"
copy_link: "copy link" copy_link: "copy link"
expires_in_time: "Expires in %{time}." expires_in_time: "Expires in %{time}."
expired_at_time: "Expired at %{time}." expired_at_time: "Expired at %{time}."
@@ -1542,21 +1542,20 @@ en:
show_advanced: "Show Advanced Options" show_advanced: "Show Advanced Options"
hide_advanced: "Hide Advanced Options" hide_advanced: "Hide Advanced Options"
restrict_email: "Restrict the invite to one email address" restrict_email: "Restrict to one email address"
max_redemptions_allowed: "Max number of uses:" max_redemptions_allowed: "Max uses"
add_to_groups: "Add to groups:" add_to_groups: "Add to groups"
invite_to_topic: "Send to topic on first login:" invite_to_topic: "Arrive at this topic"
expires_at: "Expire after:" expires_at: "Expire after"
custom_message: "Optional personal message:" custom_message: "Optional personal message"
send_invite_email: "Save and Send Email" send_invite_email: "Save and Send Email"
save_invite: "Save Invite" save_invite: "Save Invite"
invite_saved: "Invite saved." invite_saved: "Invite saved."
invite_copied: "Invite link copied." invite_copied: "Invite link copied."
blank_email: "Invite link not copied. Email address is required."
bulk_invite: bulk_invite:
none: "No invitations to display on this page." none: "No invitations to display on this page."