[WIP] FEATURE: merge share and invite actions together (#7021)

This commit also:
- removes [+ New Topic] behaviour from share, this feature has been duplicated in composer actions, months ago
- introduces our new experimental spacing standard for css: eg: `s(2)`
- introduces a new panel UI for modals
This commit is contained in:
Joffrey JAFFEUX 2019-02-20 15:42:44 +01:00 committed by GitHub
parent 6a8007e5fb
commit 04a63cfaaa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 938 additions and 683 deletions

View File

@ -1,9 +1,17 @@
import { on } from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({
classNameBindings: [":modal", ":d-modal", "modalClass", "modalStyle"],
classNameBindings: [
":modal",
":d-modal",
"modalClass",
"modalStyle",
"hasPanels"
],
attributeBindings: ["data-keyboard"],
dismissable: true,
title: null,
subtitle: null,
init() {
this._super(...arguments);

View File

@ -30,6 +30,7 @@ export default Ember.Component.extend({
? []
: [groupNames],
single: this.get("single"),
fullWidthWrap: this.get("fullWidthWrap"),
updateData: opts && opts.updateData ? opts.updateData : false,
onChangeItems: items => {
selectedGroups = items;

View File

@ -1,34 +1,30 @@
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { emailValid } from "discourse/lib/utilities";
import computed from "ember-addons/ember-computed-decorators";
import Group from "discourse/models/group";
import Invite from "discourse/models/invite";
import { i18n } from "discourse/lib/computed";
export default Ember.Controller.extend(ModalFunctionality, {
userInvitedShow: Ember.inject.controller("user-invited-show"),
export default Ember.Component.extend({
tagName: null,
// If this isn't defined, it will proxy to the user model on the preferences
inviteModel: Ember.computed.alias("panel.model.inviteModel"),
userInvitedShow: Ember.computed.alias("panel.model.userInvitedShow"),
// If this isn't defined, it will proxy to the user topic on the preferences
// page which is wrong.
emailOrUsername: null,
hasCustomMessage: false,
hasCustomMessage: false,
customMessage: null,
inviteIcon: "envelope",
invitingExistingUserToTopic: false,
@computed("isMessage", "invitingToTopic")
title(isMessage, invitingToTopic) {
if (isMessage) {
return "topic.invite_private.title";
} else if (invitingToTopic) {
return "topic.invite_reply.title";
} else {
return "user.invited.create";
}
},
isAdmin: Ember.computed.alias("currentUser.admin"),
@computed
isAdmin() {
return this.currentUser.admin;
willDestroyElement() {
this._super(...arguments);
this.reset();
},
@computed(
@ -36,9 +32,9 @@ export default Ember.Controller.extend(ModalFunctionality, {
"emailOrUsername",
"invitingToTopic",
"isPrivateTopic",
"model.groupNames",
"model.saving",
"model.details.can_invite_to"
"topic.groupNames",
"topic.saving",
"topic.details.can_invite_to"
)
disabled(
isAdmin,
@ -51,26 +47,39 @@ export default Ember.Controller.extend(ModalFunctionality, {
) {
if (saving) return true;
if (Ember.isEmpty(emailOrUsername)) return true;
const emailTrimmed = emailOrUsername.trim();
// when inviting to forum, email must be valid
if (!invitingToTopic && !emailValid(emailTrimmed)) return true;
// normal users (not admin) can't invite users to private topic via email
if (!isAdmin && isPrivateTopic && emailValid(emailTrimmed)) return true;
// when inviting to private topic via email, group name must be specified
if (isPrivateTopic && Ember.isEmpty(groupNames) && emailValid(emailTrimmed))
if (!invitingToTopic && !emailValid(emailTrimmed)) {
return true;
}
// normal users (not admin) can't invite users to private topic via email
if (!isAdmin && isPrivateTopic && emailValid(emailTrimmed)) {
return true;
}
// when inviting to private topic via email, group name must be specified
if (
isPrivateTopic &&
Ember.isEmpty(groupNames) &&
emailValid(emailTrimmed)
) {
return true;
}
if (can_invite_to) return false;
return false;
},
@computed(
"isAdmin",
"emailOrUsername",
"model.saving",
"inviteModel.saving",
"isPrivateTopic",
"model.groupNames",
"inviteModel.groupNames",
"hasCustomMessage"
)
disabledCopyLink(
@ -84,54 +93,65 @@ export default Ember.Controller.extend(ModalFunctionality, {
if (hasCustomMessage) return true;
if (saving) return true;
if (Ember.isEmpty(emailOrUsername)) return true;
const email = emailOrUsername.trim();
// email must be valid
if (!emailValid(email)) return true;
// normal users (not admin) can't invite users to private topic via email
if (!isAdmin && isPrivateTopic && emailValid(email)) return true;
// when inviting to private topic via email, group name must be specified
if (isPrivateTopic && Ember.isEmpty(groupNames) && emailValid(email))
if (!emailValid(email)) {
return true;
}
// normal users (not admin) can't invite users to private topic via email
if (!isAdmin && isPrivateTopic && emailValid(email)) {
return true;
}
// when inviting to private topic via email, group name must be specified
if (isPrivateTopic && Ember.isEmpty(groupNames) && emailValid(email)) {
return true;
}
return false;
},
@computed("model.saving")
@computed("inviteModel.saving")
buttonTitle(saving) {
return saving ? "topic.inviting" : "topic.invite_reply.action";
},
// We are inviting to a topic if the model isn't the current user.
// We are inviting to a topic if the topic isn't the current user.
// The current user would mean we are inviting to the forum in general.
@computed("model")
invitingToTopic(model) {
return model !== this.currentUser;
@computed("inviteModel")
invitingToTopic(inviteModel) {
return inviteModel !== this.currentUser;
},
@computed("model", "model.details.can_invite_via_email")
canInviteViaEmail(model, can_invite_via_email) {
return this.get("model") === this.currentUser ? true : can_invite_via_email;
@computed("inviteModel", "inviteModel.details.can_invite_via_email")
canInviteViaEmail(inviteModel, canInviteViaEmail) {
return this.get("inviteModel") === this.currentUser
? true
: canInviteViaEmail;
},
@computed("isMessage", "canInviteViaEmail")
showCopyInviteButton(isMessage, canInviteViaEmail) {
return canInviteViaEmail && !isMessage;
@computed("isPM", "canInviteViaEmail")
showCopyInviteButton(isPM, canInviteViaEmail) {
return canInviteViaEmail && !isPM;
},
topicId: Ember.computed.alias("model.id"),
topicId: Ember.computed.alias("inviteModel.id"),
// Is Private Topic? (i.e. visible only to specific group members)
// eg: visible only to specific group members
isPrivateTopic: Ember.computed.and(
"invitingToTopic",
"model.category.read_restricted"
"inviteModel.category.read_restricted"
),
// Is Private Message?
isMessage: Ember.computed.equal("model.archetype", "private_message"),
isPM: Ember.computed.equal("inviteModel.archetype", "private_message"),
// Allow Existing Members? (username autocomplete)
// scope to allowed usernames
allowExistingMembers: Ember.computed.alias("invitingToTopic"),
@computed("isAdmin", "model.group_users")
@computed("isAdmin", "inviteModel.group_users")
isGroupOwnerOrAdmin(isAdmin, groupUsers) {
return (
isAdmin || (groupUsers && groupUsers.some(groupUser => groupUser.owner))
@ -143,7 +163,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
"isGroupOwnerOrAdmin",
"emailOrUsername",
"isPrivateTopic",
"isMessage",
"isPM",
"invitingToTopic",
"canInviteViaEmail"
)
@ -151,14 +171,14 @@ export default Ember.Controller.extend(ModalFunctionality, {
isGroupOwnerOrAdmin,
emailOrUsername,
isPrivateTopic,
isMessage,
isPM,
invitingToTopic,
canInviteViaEmail
) {
return (
isGroupOwnerOrAdmin &&
canInviteViaEmail &&
!isMessage &&
!isPM &&
(emailValid(emailOrUsername) || isPrivateTopic || !invitingToTopic)
);
},
@ -166,13 +186,14 @@ export default Ember.Controller.extend(ModalFunctionality, {
@computed("emailOrUsername")
showCustomMessage(emailOrUsername) {
return (
this.get("model") === this.currentUser || emailValid(emailOrUsername)
this.get("inviteModel") === this.currentUser ||
emailValid(emailOrUsername)
);
},
// Instructional text for the modal.
@computed(
"isMessage",
"isPM",
"invitingToTopic",
"emailOrUsername",
"isPrivateTopic",
@ -180,7 +201,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
"canInviteViaEmail"
)
inviteInstructions(
isMessage,
isPM,
invitingToTopic,
emailOrUsername,
isPrivateTopic,
@ -190,7 +211,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
if (!canInviteViaEmail) {
// can't invite via email, only existing users
return I18n.t("topic.invite_reply.sso_enabled");
} else if (isMessage) {
} else if (isPM) {
// inviting to a message
return I18n.t("topic.invite_private.email_or_username");
} else if (invitingToTopic) {
@ -222,14 +243,14 @@ export default Ember.Controller.extend(ModalFunctionality, {
},
groupFinder(term) {
return Group.findAll({ term: term, ignore_automatic: true });
return Group.findAll({ term, ignore_automatic: true });
},
@computed("isMessage", "emailOrUsername", "invitingExistingUserToTopic")
successMessage(isMessage, emailOrUsername, invitingExistingUserToTopic) {
@computed("isPM", "emailOrUsername", "invitingExistingUserToTopic")
successMessage(isPM, emailOrUsername, invitingExistingUserToTopic) {
if (this.get("hasGroups")) {
return I18n.t("topic.invite_private.success_group");
} else if (isMessage) {
} else if (isPM) {
return I18n.t("topic.invite_private.success");
} else if (invitingExistingUserToTopic) {
return I18n.t("topic.invite_reply.success_existing_email", {
@ -242,9 +263,9 @@ export default Ember.Controller.extend(ModalFunctionality, {
}
},
@computed("isMessage")
errorMessage(isMessage) {
return isMessage
@computed("isPM")
errorMessage(isPM) {
return isPM
? I18n.t("topic.invite_private.error")
: I18n.t("topic.invite_reply.error");
},
@ -256,18 +277,18 @@ export default Ember.Controller.extend(ModalFunctionality, {
: "topic.invite_reply.username_placeholder";
},
@computed
customMessagePlaceholder() {
return I18n.t("invite.custom_message_placeholder");
},
customMessagePlaceholder: i18n("invite.custom_message_placeholder"),
// Reset the modal to allow a new user to be invited.
reset() {
this.set("emailOrUsername", null);
this.set("hasCustomMessage", false);
this.set("customMessage", null);
this.set("invitingExistingUserToTopic", false);
this.get("model").setProperties({
this.setProperties({
emailOrUsername: null,
hasCustomMessage: false,
customMessage: null,
invitingExistingUserToTopic: false
});
this.get("inviteModel").setProperties({
groupNames: null,
error: false,
saving: false,
@ -278,24 +299,23 @@ export default Ember.Controller.extend(ModalFunctionality, {
actions: {
createInvite() {
const self = this;
if (this.get("disabled")) {
return;
}
const groupNames = this.get("model.groupNames"),
userInvitedController = this.get("userInvitedShow"),
model = this.get("model");
const groupNames = this.get("inviteModel.groupNames");
const userInvitedController = this.get("userInvitedShow");
const model = this.get("inviteModel");
model.setProperties({ saving: true, error: false });
const onerror = e => {
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
self.set("errorMessage", e.jqXHR.responseJSON.errors[0]);
this.set("errorMessage", e.jqXHR.responseJSON.errors[0]);
} else {
self.set(
this.set(
"errorMessage",
self.get("isMessage")
this.get("isPM")
? I18n.t("topic.invite_private.error")
: I18n.t("topic.invite_reply.error")
);
@ -304,18 +324,18 @@ export default Ember.Controller.extend(ModalFunctionality, {
};
if (this.get("hasGroups")) {
return this.get("model")
return this.get("inviteModel")
.createGroupInvite(this.get("emailOrUsername").trim())
.then(data => {
model.setProperties({ saving: false, finished: true });
this.get("model.details.allowed_groups").pushObject(
this.get("inviteModel.details.allowed_groups").pushObject(
Ember.Object.create(data.group)
);
this.appEvents.trigger("post-stream:refresh");
})
.catch(onerror);
} else {
return this.get("model")
return this.get("inviteModel")
.createInvite(
this.get("emailOrUsername").trim(),
groupNames,
@ -323,19 +343,18 @@ export default Ember.Controller.extend(ModalFunctionality, {
)
.then(result => {
model.setProperties({ saving: false, finished: true });
if (!this.get("invitingToTopic")) {
if (!this.get("invitingToTopic") && userInvitedController) {
Invite.findInvitedBy(
this.currentUser,
userInvitedController.get("filter")
).then(invite_model => {
userInvitedController.set("model", invite_model);
userInvitedController.set(
"totalInvites",
invite_model.invites.length
);
).then(inviteModel => {
userInvitedController.setProperties({
model: inviteModel,
totalInvites: inviteModel.invites.length
});
});
} else if (this.get("isMessage") && result && result.user) {
this.get("model.details.allowed_users").pushObject(
} else if (this.get("isPM") && result && result.user) {
this.get("inviteModel.details.allowed_users").pushObject(
Ember.Object.create(result.user)
);
this.appEvents.trigger("post-stream:refresh");
@ -353,24 +372,21 @@ export default Ember.Controller.extend(ModalFunctionality, {
},
generateInvitelink() {
const self = this;
if (this.get("disabled")) {
return;
}
const groupNames = this.get("model.groupNames"),
userInvitedController = this.get("userInvitedShow"),
model = this.get("model");
var topicId = null;
if (this.get("invitingToTopic")) {
topicId = this.get("model.id");
}
const groupNames = this.get("inviteModel.groupNames");
const userInvitedController = this.get("userInvitedShow");
const model = this.get("inviteModel");
model.setProperties({ saving: true, error: false });
return this.get("model")
let topicId;
if (this.get("invitingToTopic")) {
topicId = this.get("inviteModel.id");
}
return model
.generateInviteLink(
this.get("emailOrUsername").trim(),
groupNames,
@ -382,24 +398,26 @@ export default Ember.Controller.extend(ModalFunctionality, {
finished: true,
inviteLink: result
});
Invite.findInvitedBy(
this.currentUser,
userInvitedController.get("filter")
).then(invite_model => {
userInvitedController.set("model", invite_model);
userInvitedController.set(
"totalInvites",
invite_model.invites.length
);
});
if (userInvitedController) {
Invite.findInvitedBy(
this.currentUser,
userInvitedController.get("filter")
).then(inviteModel => {
userInvitedController.setProperties({
model: inviteModel,
totalInvites: inviteModel.invites.length
});
});
}
})
.catch(function(e) {
.catch(e => {
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
self.set("errorMessage", e.jqXHR.responseJSON.errors[0]);
this.set("errorMessage", e.jqXHR.responseJSON.errors[0]);
} else {
self.set(
this.set(
"errorMessage",
self.get("isMessage")
this.get("isPM")
? I18n.t("topic.invite_private.error")
: I18n.t("topic.invite_reply.error")
);
@ -411,7 +429,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
showCustomMessageBox() {
this.toggleProperty("hasCustomMessage");
if (this.get("hasCustomMessage")) {
if (this.get("model") === this.currentUser) {
if (this.get("inviteModel") === this.currentUser) {
this.set(
"customMessage",
I18n.t("invite.custom_message_template_forum")

View File

@ -0,0 +1,11 @@
import { fmt } from "discourse/lib/computed";
export default Ember.Component.extend({
panel: null,
panelComponent: fmt("panel.id", "%@-panel"),
classNameBindings: ["panel.id"],
classNames: ["modal-panel"]
});

View File

@ -0,0 +1,17 @@
import { propertyEqual } from "discourse/lib/computed";
export default Ember.Component.extend({
tagName: "li",
classNames: ["modal-tab"],
panel: null,
selectedPanel: null,
panelsLength: null,
classNameBindings: ["isActive", "singleTab", "panel.id"],
singleTab: Ember.computed.equal("panelsLength", 1),
title: Ember.computed.alias("panel.title"),
isActive: propertyEqual("panel.id", "selectedPanel.id"),
click() {
this.onSelectPanel(this.get("panel"));
}
});

View File

@ -1,13 +0,0 @@
import Button from "discourse/components/d-button";
export default Button.extend({
classNames: ["btn-default", "share"],
icon: "link",
title: "topic.share.help",
label: "topic.share.title",
attributeBindings: ["url:data-share-url"],
click() {
return true;
}
});

View File

@ -0,0 +1,105 @@
import { escapeExpression } from "discourse/lib/utilities";
import { longDateNoYear } from "discourse/lib/formatter";
import { default as computed } from "ember-addons/ember-computed-decorators";
import Sharing from "discourse/lib/sharing";
export default Ember.Component.extend({
tagName: null,
date: Ember.computed.alias("panel.model.date"),
type: Ember.computed.alias("panel.model.type"),
postNumber: Ember.computed.alias("panel.model.postNumber"),
postId: Ember.computed.alias("panel.model.postId"),
topic: Ember.computed.alias("panel.model.topic"),
@computed
sources() {
return Sharing.activeSources(this.siteSettings.share_links);
},
@computed("date")
postDate(date) {
return date ? longDateNoYear(new Date(date)) : null;
},
@computed("type", "postNumber", "postDate", "topic.title")
shareTitle(type, postNumber, postDate, topicTitle) {
topicTitle = escapeExpression(topicTitle);
if (type === "topic") {
return I18n.t("share.topic", { topicTitle });
}
if (postNumber) {
return I18n.t("share.post", { postNumber, postDate });
}
return I18n.t("share.topic", { topicTitle });
},
@computed("topic.shareUrl")
shareUrl(shareUrl) {
if (Ember.isEmpty(shareUrl)) {
return;
}
// Relative urls
if (shareUrl.indexOf("/") === 0) {
const location = window.location;
shareUrl = `${location.protocol}//${location.host}${shareUrl}`;
}
return encodeURI(shareUrl);
},
didInsertElement() {
this._super(...arguments);
const shareUrl = this.get("shareUrl");
const $linkInput = this.$(".topic-share-url");
const $linkForTouch = this.$(".topic-share-url-for-touch a");
Ember.run.schedule("afterRender", () => {
if (!this.capabilities.touch) {
$linkForTouch.parent().remove();
$linkInput
.val(shareUrl)
.select()
.focus();
} else {
$linkInput.remove();
$linkForTouch.attr("href", shareUrl).text(shareUrl);
const range = window.document.createRange();
range.selectNode($linkForTouch[0]);
window.getSelection().addRange(range);
}
});
},
actions: {
share(source) {
const url = source.generateUrl(
this.get("shareUrl"),
this.get("topic.title")
);
const options = {
menubar: "no",
toolbar: "no",
resizable: "yes",
scrollbars: "yes",
width: 600,
height: source.popupHeight || 315
};
const stringOptions = Object.keys(options)
.map(k => `${k}=${options[k]}`)
.join(",");
if (source.shouldOpenInPopup) {
window.open(url, "", stringOptions);
} else {
window.open(url, "_blank");
}
}
}
});

View File

@ -1,195 +0,0 @@
import { wantsNewWindow } from "discourse/lib/intercept-click";
import { longDateNoYear } from "discourse/lib/formatter";
import computed from "ember-addons/ember-computed-decorators";
import Sharing from "discourse/lib/sharing";
export default Ember.Component.extend({
elementId: "share-link",
classNameBindings: ["visible"],
link: null,
visible: null,
@computed
sources() {
return Sharing.activeSources(this.siteSettings.share_links);
},
@computed("type", "postNumber")
shareTitle(type, postNumber) {
if (type === "topic") {
return I18n.t("share.topic");
}
if (postNumber) {
return I18n.t("share.post", { postNumber });
}
return I18n.t("share.topic");
},
@computed("date")
displayDate(date) {
return longDateNoYear(new Date(date));
},
_focusUrl() {
const link = this.get("link");
if (!this.capabilities.touch) {
const $linkInput = $("#share-link input");
$linkInput.val(link);
// Wait for the fade-in transition to finish before selecting the link:
window.setTimeout(() => $linkInput.select().focus(), 160);
} else {
const $linkForTouch = $("#share-link .share-for-touch a");
$linkForTouch.attr("href", link);
$linkForTouch.text(link);
const range = window.document.createRange();
range.selectNode($linkForTouch[0]);
window.getSelection().addRange(range);
}
},
_showUrl($target, url) {
const $currentTargetOffset = $target.offset();
const $this = this.$();
if (Ember.isEmpty(url)) {
return;
}
// Relative urls
if (url.indexOf("/") === 0) {
url = window.location.protocol + "//" + window.location.host + url;
}
const shareLinkWidth = $this.width();
let x = $currentTargetOffset.left - shareLinkWidth / 2;
if (x < 25) {
x = 25;
}
if (x + shareLinkWidth > $(window).width()) {
x -= shareLinkWidth / 2;
}
const header = $(".d-header");
let y = $currentTargetOffset.top - ($this.height() + 20);
if (y < header.offset().top + header.height()) {
y = $currentTargetOffset.top + 10;
}
$this.css({ top: "" + y + "px" });
if (!this.site.mobileView) {
$this.css({ left: "" + x + "px" });
}
this.set("link", encodeURI(url));
this.set("visible", true);
Ember.run.scheduleOnce("afterRender", this, this._focusUrl);
},
_webShare(url) {
// We can pass title and text too, but most share targets do their own oneboxing
return navigator.share({ url });
},
didInsertElement() {
this._super(...arguments);
const $html = $("html");
$html.on("mousedown.outside-share-link", e => {
// Use mousedown instead of click so this event is handled before routing occurs when a
// link is clicked (which is a click event) while the share dialog is showing.
if (this.$().has(e.target).length !== 0) {
return;
}
this.send("close");
return true;
});
$html.on(
"click.discourse-share-link",
"button[data-share-url], .post-info .post-date[data-share-url]",
e => {
// if they want to open in a new tab, let it so
if (wantsNewWindow(e)) {
return true;
}
e.preventDefault();
const $currentTarget = $(e.currentTarget);
const url = $currentTarget.data("share-url");
const postNumber = $currentTarget.data("post-number");
const postId = $currentTarget.closest("article").data("post-id");
const date = $currentTarget.children().data("time");
this.setProperties({ postNumber, date, postId });
// use native webshare only when the user clicks on the "chain" icon
// navigator.share needs HTTPS, returns undefined on HTTP
if (navigator.share && !$currentTarget.hasClass("post-date")) {
this._webShare(url).catch(() => {
// if navigator fails for unexpected reason fallback to popup
this._showUrl($currentTarget, url);
});
} else {
this._showUrl($currentTarget, url);
}
return false;
}
);
$html.on("keydown.share-view", e => {
if (e.keyCode === 27) {
this.send("close");
}
});
this.appEvents.on("share:url", (url, $target) =>
this._showUrl($target, url)
);
},
willDestroyElement() {
this._super(...arguments);
$("html")
.off("click.discourse-share-link")
.off("mousedown.outside-share-link")
.off("keydown.share-view");
},
actions: {
replyAsNewTopic() {
const postStream = this.get("topic.postStream");
const postId =
this.get("postId") || postStream.findPostIdForPostNumber(1);
const post = postStream.findLoadedPost(postId);
this.get("replyAsNewTopic")(post);
this.send("close");
},
close() {
this.setProperties({
link: null,
postNumber: null,
postId: null,
visible: false
});
},
share(source) {
const url = source.generateUrl(this.get("link"), this.get("topic.title"));
if (source.shouldOpenInPopup) {
window.open(
url,
"",
"menubar=no,toolbar=no,resizable=yes,scrollbars=yes,width=600,height=" +
(source.popupHeight || 315)
);
} else {
window.open(url, "_blank");
}
}
}
});

View File

@ -34,7 +34,8 @@ export default TextField.extend({
single = bool("single"),
allowAny = bool("allowAny"),
disabled = bool("disabled"),
disallowEmails = bool("disallowEmails");
disallowEmails = bool("disallowEmails"),
fullWidthWrap = bool("fullWidthWrap");
function excludedUsernames() {
// hack works around some issues with allowAny eventing
@ -54,6 +55,7 @@ export default TextField.extend({
single: single,
allowAny: allowAny,
updateData: opts && opts.updateData ? opts.updateData : false,
fullWidthWrap,
dataSource(term) {
var results = userSearch({

View File

@ -1,3 +1,5 @@
import showModal from "discourse/lib/show-modal";
import { share } from "discourse/lib/pwa-utils";
import { registerTopicFooterButton } from "discourse/lib/register-topic-footer-button";
export default {
@ -5,25 +7,100 @@ export default {
initialize() {
registerTopicFooterButton({
id: "share",
id: "native-share",
icon: "link",
priority: 999,
label: "topic.share.title",
title: "topic.share.help",
action() {
this.appEvents.trigger(
"share:url",
this.get("topic.shareUrl"),
$("#topic-footer-buttons")
share({ url: this.get("topic.shareUrl") }).catch(() =>
showModal("share-and-invite", {
modalClass: "share-and-invite",
panels: [
{
id: "share",
title: "topic.share.title",
model: { topic: this.get("topic") }
}
]
})
);
},
dropdown: true,
classNames: ["native-share"],
dependentKeys: ["topic.shareUrl", "topic.isPrivateMessage"],
displayed() {
return window.navigator.share;
}
});
registerTopicFooterButton({
id: "share-and-invite",
icon: "link",
priority: 999,
label: "topic.share.title",
title: "topic.share.help",
action() {
const modal = () => {
const panels = [
{
id: "share",
title: "topic.share.extended_title",
model: {
topic: this.get("topic")
}
}
];
if (this.get("canInviteTo") && !this.get("inviteDisabled")) {
let invitePanelTitle;
if (this.get("isPM")) {
invitePanelTitle = "topic.invite_private.title";
} else if (this.get("invitingToTopic")) {
invitePanelTitle = "topic.invite_reply.title";
} else {
invitePanelTitle = "user.invited.create";
}
panels.push({
id: "invite",
title: invitePanelTitle,
model: {
inviteModel: this.get("topic")
}
});
}
showModal("share-and-invite", {
model: this.get("topic"),
modalClass: "share-and-invite",
panels
});
};
if (window.navigator.share) {
window.navigator
.share({ url: this.get("topic.shareUrl") })
.catch(() => modal());
} else {
modal();
}
},
dropdown() {
return this.site.mobileView;
},
classNames: ["share"],
dependentKeys: ["topic.shareUrl", "topic.isPrivateMessage"],
classNames: ["share-and-invite"],
dependentKeys: [
"topic.shareUrl",
"topic.isPrivateMessage",
"canInviteTo",
"inviteDisabled",
"isPM",
"invitingToTopic"
],
displayed() {
return !this.get("topic.isPrivateMessage");
return !(this.site.mobileView && window.navigator.share);
}
});
@ -47,26 +124,6 @@ export default {
}
});
registerTopicFooterButton({
id: "invite",
icon: "users",
priority: 997,
label: "topic.invite_reply.title",
title: "topic.invite_reply.help",
action: "showInvite",
dropdown() {
return this.site.mobileView;
},
classNames: ["invite-topic"],
dependentKeys: ["canInviteTo", "inviteDisabled"],
displayed() {
return this.get("canInviteTo");
},
disabled() {
return this.get("inviteDisabled");
}
});
registerTopicFooterButton({
dependentKeys: ["topic.bookmarked", "topic.isPrivateMessage"],
id: "bookmark",

View File

@ -201,7 +201,10 @@ export default function(options) {
wrap = this.wrap(
"<div class='ac-wrap clearfix" + (disabled ? " disabled" : "") + "'/>"
).parent();
wrap.width(width);
if (!options.fullWidthWrap) {
wrap.width(width);
}
}
if (options.single && !options.width) {

View File

@ -0,0 +1,12 @@
export function share(data) {
return new Ember.RSVP.Promise((resolve, reject) => {
if (window.location.protocol === "https:" && window.navigator.share) {
window.navigator
.share(data)
.catch(reject)
.then(resolve);
} else {
reject();
}
});
}

View File

@ -64,6 +64,24 @@ export default function(name, opts) {
modalController.set("title", I18n.t(opts.title));
}
if (opts.panels) {
modalController.setProperties({
panels: opts.panels,
selectedPanel: opts.panels[0]
});
if (controller.actions.onSelectPanel) {
modalController.set("onSelectPanel", controller.actions.onSelectPanel);
}
modalController.set(
"modalClass",
`${modalController.get("modalClass")} has-tabs`
);
} else {
modalController.setProperties({ panels: [], selectedPanel: null });
}
controller.set("modal", modalController);
const model = opts.model;
if (model) {

View File

@ -16,6 +16,11 @@ export default Ember.Mixin.create({
actions: {
closeModal() {
this.get("modal").send("closeModal");
this.set("panels", []);
},
onSelectPanel(panel) {
this.set("selectedPanel", panel);
}
}
});

View File

@ -92,11 +92,6 @@ const TopicRoute = Discourse.Route.extend({
this.controllerFor("feature_topic").reset();
},
showInvite() {
showModal("invite", { model: this.modelFor("topic") });
this.controllerFor("invite").reset();
},
showHistory(model, revision) {
showModal("history", { model });
const historyController = this.controllerFor("history");

View File

@ -30,8 +30,19 @@ export default Discourse.Route.extend({
actions: {
showInvite() {
showModal("invite", { model: this.currentUser });
this.controllerFor("invite").reset();
showModal("share-and-invite", {
modalClass: "share-and-invite",
panels: [
{
id: "invite",
title: "user.invited.create",
model: {
inviteModel: this.currentUser,
userInvitedShow: this.controllerFor("user-invited-show")
}
}
]
});
}
}
});

View File

@ -10,13 +10,25 @@
</div>
{{/if}}
<div class="title">
<h3>{{title}}</h3>
{{#if panels}}
<ul class="modal-tabs">
{{#each panels as |panel|}}
{{modal-tab
panel=panel
panelsLength=panels.length
selectedPanel=selectedPanel
onSelectPanel=onSelectPanel}}
{{/each}}
</ul>
{{else}}
<div class="title">
<h3>{{title}}</h3>
{{#if subtitle}}
<p>{{subtitle}}</p>
{{/if}}
</div>
{{#if subtitle}}
<p>{{subtitle}}</p>
{{/if}}
</div>
{{/if}}
</div>
<div id='modal-alert'></div>

View File

@ -0,0 +1,93 @@
{{#if inviteModel.error}}
<div class="alert alert-error">
<button class="close" data-dismiss="alert">×</button>
<div class="error-message">
{{{errorMessage}}}
</div>
</div>
{{/if}}
<div class="body">
{{#if inviteModel.finished}}
{{#if inviteModel.inviteLink}}
{{generated-invite-link link=inviteModel.inviteLink email=emailOrUsername}}
{{else}}
<div class="success-message">
{{{successMessage}}}
</div>
{{/if}}
{{else}}
<div class="invite-user-control">
<label class="instructions">{{inviteInstructions}}</label>
{{#if allowExistingMembers}}
{{user-selector
fullWidthWrap=true
single=true
allowAny=true
excludeCurrentUser=true
includeMessageableGroups=isMessage
hasGroups=hasGroups
usernames=emailOrUsername
placeholderKey=placeholderKey
class="invite-user-input"
autocomplete="off"}}
{{else}}
{{text-field
class="email-or-username-input"
value=emailOrUsername
placeholderKey="topic.invite_reply.email_placeholder"}}
{{/if}}
</div>
{{#if showGroups}}
<div class="group-access-control">
<label class="instructions {{showGroupsClass}}">
{{i18n "topic.automatically_add_to_groups"}}
</label>
{{group-selector
fullWidthWrap=true
groupFinder=groupFinder
groupNames=inviteModel.groupNames
placeholderKey="topic.invite_private.group_name"}}
</div>
{{/if}}
{{#if showCustomMessage}}
<div class="show-custom-message-control">
<label class="instructions">
{{discourse-linked-text
class="optional"
action=(action "showCustomMessageBox")
text="invite.custom_message"}}
</label>
{{#if hasCustomMessage}}
{{textarea value=customMessage placeholder=customMessagePlaceholder}}
{{/if}}
</div>
{{/if}}
{{/if}}
</div>
<div class="footer">
{{#if inviteModel.finished}}
{{d-button
class="btn-primary"
action=(route-action "closeModal")
label="close"}}
{{else}}
{{d-button
icon=inviteIcon
action=(action "createInvite")
class="btn-primary send-invite"
disabled=disabled
label=buttonTitle}}
{{#if showCopyInviteButton}}
{{d-button
icon="link"
action=(action "generateInvitelink")
class="btn-primary generate-invite-link"
disabled=disabledCopyLink
label="user.invited.generate_link"}}
{{/if}}
{{/if}}
</div>

View File

@ -0,0 +1 @@
{{component panelComponent panel=panel close=(route-action "closeModal")}}

View File

@ -0,0 +1 @@
{{i18n title}}

View File

@ -0,0 +1,14 @@
<div class="header">
<h3 class="title">{{{shareTitle}}}</h3>
</div>
<div class="body">
{{input value=shareUrl class="topic-share-url"}}
<div class="topic-share-url-for-touch"><a></a></div>
<div class="sources">
{{#each sources as |source|}}
{{share-source source=source title=topic.title action=(action "share")}}
{{/each}}
</div>
</div>

View File

@ -1,28 +0,0 @@
<h3>{{shareTitle}}</h3>
{{#if date}}
<span class="date">{{displayDate}}</span>
{{/if}}
<div>
<input type='text'>
<div class="share-for-touch"><div class="overflow-ellipsis"><a></a></div></div>
</div>
{{#each sources as |s|}}
{{share-source source=s title=model.title action=(action "share")}}
{{/each}}
{{#if topic.details.can_reply_as_new_topic}}
<div class='reply-as-new-topic'>
{{#if topic.isPrivateMessage}}
<a href {{action "replyAsNewTopic"}} aria-label={{i18n 'post.reply_as_new_private_message'}} title={{i18n 'post.reply_as_new_private_message'}}>{{d-icon "plus"}}{{i18n 'user.new_private_message'}}</a>
{{else}}
<a href {{action "replyAsNewTopic"}} aria-label={{i18n 'post.reply_as_new_topic'}} title={{i18n 'post.reply_as_new_topic'}}>{{d-icon "plus"}}{{i18n 'topic.create'}}</a>
{{/if}}
</div>
{{/if}}
<div class='link'>
<a href {{action "close"}} class="close-share" aria-label={{i18n 'share.close'}} title={{i18n 'share.close'}}>{{d-icon "times"}}</a>
</div>

View File

@ -2,10 +2,11 @@
modalClass=modalClass
title=title
subtitle=subtitle
panels=panels
selectedPanel=selectedPanel
onSelectPanel=onSelectPanel
class="hidden"
errors=errors
closeModal=(route-action "closeModal")}}
{{outlet "modalBody"}}
{{/d-modal}}

View File

@ -1,52 +0,0 @@
{{#d-modal-body id="invite-modal" title=title}}
{{#if model.error}}
<div class="alert alert-error">
<button class="close" data-dismiss="alert">×</button>
{{{errorMessage}}}
</div>
{{/if}}
{{#if model.finished}}
{{#if model.inviteLink}}
{{generated-invite-link link=model.inviteLink email=emailOrUsername}}
{{else}}
{{{successMessage}}}
{{/if}}
{{else}}
<label>{{inviteInstructions}}</label>
{{#if allowExistingMembers}}
{{user-selector
single=true
allowAny=true
excludeCurrentUser=true
includeMessageableGroups=isMessage
hasGroups=hasGroups
usernames=emailOrUsername
placeholderKey=placeholderKey
autocomplete="discourse"}}
{{else}}
{{text-field value=emailOrUsername placeholderKey="topic.invite_reply.email_placeholder"}}
{{/if}}
{{#if showGroups}}
<label><span class={{showGroupsClass}}>{{i18n 'topic.automatically_add_to_groups'}}</span></label>
{{group-selector groupFinder=groupFinder groupNames=model.groupNames placeholderKey="topic.invite_private.group_name"}}
{{/if}}
{{#if showCustomMessage}}
<label>{{discourse-linked-text class="optional" action=(action "showCustomMessageBox") text="invite.custom_message"}}</label>
{{#if hasCustomMessage}}{{textarea value=customMessage placeholder=customMessagePlaceholder}}{{/if}}
{{/if}}
{{/if}}
{{/d-modal-body}}
<div class="modal-footer">
{{#if model.finished}}
{{d-button class="btn-primary" action=(route-action "closeModal") label="close"}}
{{else}}
{{d-button icon=inviteIcon action=(action "createInvite") class="btn-primary" disabled=disabled label=buttonTitle}}
{{#if showCopyInviteButton}}
{{d-button icon="link" action=(action "generateInvitelink") class="btn-primary" disabled=disabledCopyLink label='user.invited.generate_link'}}
{{/if}}
{{/if}}
</div>

View File

@ -0,0 +1,3 @@
{{#d-modal-body}}
{{modal-panel panel=modal.selectedPanel}}
{{/d-modal-body}}

View File

@ -192,7 +192,6 @@
toggleSummary=(action "toggleSummary")
removeAllowedUser=(action "removeAllowedUser")
removeAllowedGroup=(action "removeAllowedGroup")
showInvite=(route-action "showInvite")
topVisibleChanged=(action "topVisibleChanged")
currentPostChanged=(action "currentPostChanged")
currentPostScrolled=(action "currentPostScrolled")
@ -264,11 +263,9 @@
convertToPrivateMessage=(action "convertToPrivateMessage")
toggleBookmark=(action "toggleBookmark")
showFlagTopic=(route-action "showFlagTopic")
showInvite=(route-action "showInvite")
toggleArchiveMessage=(action "toggleArchiveMessage")
editFirstPost=(action "editFirstPost")
replyToPost=(action "replyToPost")
}}
replyToPost=(action "replyToPost")}}
{{else}}
<div id="topic-footer-buttons">
{{d-button icon="reply" class="btn-primary pull-right" action=(route-action "showLogin") label="topic.reply.title"}}
@ -320,8 +317,6 @@
</div>
{{/if}}
{{share-popup topic=model replyAsNewTopic=(action "replyAsNewTopic")}}
{{#if embedQuoteButton}}
{{quote-button quoteState=quoteState selectText=(action "selectText")}}
{{/if}}

View File

@ -2,6 +2,7 @@ import PostCooked from "discourse/widgets/post-cooked";
import DecoratorHelper from "discourse/widgets/decorator-helper";
import { createWidget, applyDecorators } from "discourse/widgets/widget";
import { iconNode } from "discourse-common/lib/icon-library";
import { share } from "discourse/lib/pwa-utils";
import { transformBasicPost } from "discourse/lib/transform-post";
import { postTransformCallbacks } from "discourse/widgets/post-stream";
import { h } from "virtual-dom";
@ -13,6 +14,7 @@ import {
formatUsername
} from "discourse/lib/utilities";
import hbs from "discourse/widgets/hbs-compiler";
import showModal from "discourse/lib/show-modal";
function transformWithCallbacks(post) {
let transformed = transformBasicPost(post);
@ -219,6 +221,71 @@ function showReplyTab(attrs, siteSettings) {
);
}
createWidget("post-date", {
tagName: "div.post-info.post-date",
buildClasses(attrs) {
let classes = "post-date";
const lastWikiEdit =
attrs.wiki && attrs.lastWikiEdit && new Date(attrs.lastWikiEdit);
if (lastWikiEdit) {
classes = `${classes} last-wiki-edit`;
}
return classes;
},
html(attrs) {
return h(
"a",
{
attributes: {
class: "post-date",
"data-share-url": attrs.shareUrl,
"data-post-number": attrs.post_number
}
},
dateNode(this._date(attrs))
);
},
_date(attrs) {
const lastWikiEdit =
attrs.wiki && attrs.lastWikiEdit && new Date(attrs.lastWikiEdit);
const createdAt = new Date(attrs.created_at);
return lastWikiEdit ? lastWikiEdit : createdAt;
},
click() {
const post = this.findAncestorModel();
const modalFallback = () => {
showModal("share-and-invite", {
modalClass: "share-and-invite",
panels: [
{
id: "share",
title: "topic.share.extended_title",
model: {
postNumber: this.attrs.post_number,
shareUrl: this.attrs.shareUrl,
date: this._date(this.attrs),
postId: post.get("id"),
topic: post.get("topic")
}
}
]
});
};
// use native webshare when available
// navigator.share needs HTTPS, returns undefined on HTTP
share({ url: this.attrs.shareUrl }).catch(modalFallback);
}
});
createWidget("post-meta-data", {
tagName: "div.topic-meta-data",
@ -241,21 +308,6 @@ createWidget("post-meta-data", {
);
}
const lastWikiEdit =
attrs.wiki && attrs.lastWikiEdit && new Date(attrs.lastWikiEdit);
const createdAt = new Date(attrs.created_at);
const date = lastWikiEdit ? dateNode(lastWikiEdit) : dateNode(createdAt);
const attributes = {
class: "post-date",
href: attrs.shareUrl,
"data-share-url": attrs.shareUrl,
"data-post-number": attrs.post_number
};
if (lastWikiEdit) {
attributes["class"] += " last-wiki-edit";
}
if (attrs.via_email) {
postInfo.push(this.attach("post-email-indicator", attrs));
}
@ -276,7 +328,7 @@ createWidget("post-meta-data", {
postInfo.push(this.attach("reply-to-tab", attrs));
}
postInfo.push(h("div.post-info.post-date", h("a", { attributes }, date)));
postInfo.push(this.attach("post-date", attrs));
postInfo.push(
h(

View File

@ -4,6 +4,7 @@
@import "common/foundation/base";
@import "common/foundation/mixins";
@import "common/foundation/variables";
@import "common/foundation/spacing";
@import "common/select-kit/admin-agree-flag-dropdown";
@import "common/select-kit/admin-delete-flag-dropdown";
@import "common/select-kit/categories-admin-dropdown";

View File

@ -27,9 +27,9 @@
.modal-header {
display: flex;
align-items: flex-start;
padding: 10px 15px;
border-bottom: 1px solid $primary-low;
align-items: center;
.title {
h3 {
@ -42,6 +42,7 @@
}
.modal-close {
align-self: flex-start;
order: 2;
margin-left: auto;
}
@ -104,6 +105,7 @@
background-clip: padding-box;
box-shadow: shadow("modal");
padding: 1px;
box-sizing: border-box;
@media screen and (min-width: 475px) {
min-width: 475px;
@ -617,7 +619,41 @@
}
}
.modal-tab {
position: absolute;
width: 95%;
.modal:not(.has-tabs) {
.modal-tab {
position: absolute;
width: 95%;
}
}
.modal {
&.has-tabs {
.modal-tabs {
display: inline-flex;
flex-wrap: wrap;
width: calc(100% - 20px);
flex: 1 0 auto;
margin: 0;
.modal-tab {
list-style: none;
padding: s(1 2);
margin-right: s(1);
cursor: pointer;
&.is-active {
color: $secondary;
background: $danger;
&.single-tab {
color: $primary;
background: none;
padding: s(1 0);
font-weight: 700;
font-size: $font-up-3;
}
}
}
}
}
}

View File

@ -1,86 +0,0 @@
// styles that apply to the "share" popup when sharing a link to a post or topic
#share-link {
position: absolute;
left: 20px;
z-index: z("dropdown");
box-shadow: shadow("card");
background-color: $secondary;
padding: 6px 10px 10px 10px;
width: 300px;
display: none;
&.visible {
display: block;
}
input[type="text"] {
width: 96%;
}
.share-for-touch .overflow-ellipsis {
clear: both;
}
.share-for-touch {
margin: 14px 0;
}
h3 {
font-size: $font-0;
}
.copy-text {
display: inline-block;
position: absolute;
margin: 5px 5px 5px 15px;
color: $success;
opacity: 1;
transition: opacity 0.25s;
font-size: $font-0;
&:not(.success) {
opacity: 0;
}
}
.social-link {
margin-left: 2px;
margin-right: 8px;
float: left;
font-size: $font-up-4;
}
.reply-as-new-topic {
float: left;
line-height: $line-height-large;
margin-left: 8px;
margin-top: 0.5em;
.fa {
margin-right: 5px;
}
}
.link {
margin-right: 2px;
float: right;
font-size: $font-up-3;
a {
color: dark-light-choose($primary-medium, $secondary-medium);
}
}
h3 {
margin: 5px 0;
float: left;
}
.date {
float: right;
margin: 5px;
color: dark-light-choose($primary-medium, $secondary-medium);
}
input[type="text"] {
font-size: $font-up-1;
margin-bottom: 10px;
}
}
.discourse-no-touch #share-link .share-for-touch {
display: none;
}
.discourse-touch #share-link input[type="text"] {
display: none;
}

View File

@ -0,0 +1,86 @@
.share-and-invite.modal {
.modal-body {
max-width: 475px;
}
}
.share-and-invite.modal .share.modal-panel {
.header {
display: flex;
flex-direction: row;
align-items: center;
.title {
font-size: $font-0;
font-weight: normal;
margin-bottom: s(2);
}
}
.body {
display: flex;
flex-direction: column;
.topic-share-url {
width: 100%;
}
.topic-share-url-for-touch {
width: 290px;
@extend .overflow-ellipsis;
}
.topic-share-url-for-touch,
.topic-share-url {
margin-bottom: s(2);
}
.sources {
display: flex;
align-items: center;
flex-wrap: wrap;
flex-direction: row;
.social-link {
font-size: $font-up-4;
}
}
}
}
.share-and-invite.modal .invite.modal-panel {
.error-message,
.success-message {
margin-bottom: s(2);
}
.body {
.invite-user-control,
.group-access-control,
.show-custom-message-control {
margin-bottom: s(4);
}
.instructions {
margin-bottom: s(2);
}
.email-or-username-input {
width: 100%;
}
}
.footer {
display: flex;
align-items: center;
.btn-primary {
margin-right: s(2);
&:last-child {
margin-right: 0;
}
}
}
}

View File

@ -0,0 +1,17 @@
$base-space: 4px;
@function s(
$sizes: (
0
)
) {
$spaces: ();
@each $size in $sizes {
@if ($size == 0) {
// strip units from 0 values
@return $size / ($size * 0 + 1);
}
$spaces: append($spaces, ($size * $base-space));
}
@return $spaces;
}

View File

@ -587,11 +587,6 @@ video {
}
}
#share-link {
width: 365px;
margin-left: -4px;
}
.post-select {
float: right;
margin-right: 20px;

View File

@ -42,7 +42,6 @@
.close {
font-size: $font-up-4;
padding: 10px 15px 5px 5px;
color: $primary;
}

View File

@ -350,12 +350,6 @@ iframe {
position: relative;
}
#share-link {
width: 290px;
left: auto;
right: 4px;
}
.selected-posts {
padding: 0.1em 0.7em;
}

View File

@ -135,12 +135,12 @@ en:
next_month: "Next Month"
placeholder: date
share:
topic: "share a link to this topic"
post: "post #%{postNumber}"
topic: "<b>Topic</b>: %{topicTitle}"
post: "<b>Post #%{postNumber}</b>, %{postDate}"
close: "close"
twitter: "share this link on Twitter"
facebook: "share this link on Facebook"
email: "send this link in an email"
twitter: "Share this link on Twitter"
facebook: "Share this link on Facebook"
email: "Send this link in an email"
action_codes:
public_topic: "made this topic public %{when}"
@ -1948,6 +1948,7 @@ en:
share:
title: "Share"
extended_title: "Share a link"
help: "share a link to this topic"
print:

View File

@ -0,0 +1,88 @@
import { acceptance } from "helpers/qunit-helpers";
acceptance("Share and Invite modal - desktop", {
loggedIn: true
});
QUnit.test("Topic footer button", async assert => {
await visit("/t/internationalization-localization/280");
assert.ok(
exists("#topic-footer-button-share-and-invite"),
"the button exists"
);
await click("#topic-footer-button-share-and-invite");
assert.ok(exists(".share-and-invite.modal"), "it shows the modal");
assert.ok(
exists(".share-and-invite.modal .modal-tab.share"),
"it shows the share tab"
);
assert.ok(
exists(".share-and-invite.modal .modal-tab.share.is-active"),
"it activates the share tab by default"
);
assert.ok(
exists(".share-and-invite.modal .modal-tab.invite"),
"it shows the invite tab"
);
assert.equal(
find(".share-and-invite.modal .modal-panel.share .title").text(),
"Topic: Internationalization / localization",
"it shows the topic title"
);
assert.ok(
find(".share-and-invite.modal .modal-panel.share .topic-share-url")
.val()
.includes("/t/internationalization-localization/280?u=eviltrout"),
"it shows the topic sharing url"
);
assert.equal(
find(".share-and-invite.modal .social-link").length,
3,
"it shows social sources"
);
await click(".share-and-invite.modal .modal-tab.invite");
assert.ok(
exists(".share-and-invite.modal .modal-panel.invite .send-invite:disabled"),
"send invite button is disabled"
);
assert.ok(
exists(
".share-and-invite.modal .modal-panel.invite .generate-invite-link:disabled"
),
"generate invite button is disabled"
);
});
QUnit.test("Post date link", async assert => {
await visit("/t/internationalization-localization/280");
await click("#post_2 .post-info.post-date a");
assert.ok(exists(".share-and-invite.modal"), "it shows the modal");
assert.ok(
exists(".share-and-invite.modal .modal-tab.share"),
"it shows the share tab"
);
assert.ok(
exists(".share-and-invite.modal .modal-tab.share.single-tab"),
"it shows only one tab"
);
assert.ok(
!exists(".share-and-invite.modal .modal-tab.invite"),
"it doesnt show the invite tab"
);
});

View File

@ -0,0 +1,77 @@
import { acceptance } from "helpers/qunit-helpers";
acceptance("Share and Invite modal - mobile", {
loggedIn: true,
mobileView: true
});
QUnit.test("Topic footer mobile button", async assert => {
await visit("/t/internationalization-localization/280");
assert.ok(
!exists("#topic-footer-button-share-and-invite"),
"the button doesnt exist"
);
const subject = selectKit(".topic-footer-mobile-dropdown");
await subject.expand();
await subject.selectRowByValue("share-and-invite");
assert.ok(exists(".share-and-invite.modal"), "it shows the modal");
assert.ok(
exists(".share-and-invite.modal .modal-tab.share"),
"it shows the share tab"
);
assert.ok(
exists(".share-and-invite.modal .modal-tab.share.is-active"),
"it activates the share tab by default"
);
assert.ok(
!exists(".share-and-invite.modal .modal-tab.invite"),
"it doesnt show the invite tab"
);
assert.equal(
find(".share-and-invite.modal .modal-panel.share .title").text(),
"Topic: Internationalization / localization",
"it shows the topic title"
);
assert.ok(
find(".share-and-invite.modal .modal-panel.share .topic-share-url")
.val()
.includes("/t/internationalization-localization/280?u=eviltrout"),
"it shows the topic sharing url"
);
assert.equal(
find(".share-and-invite.modal .social-link").length,
3,
"it shows social sources"
);
});
QUnit.test("Post date link", async assert => {
await visit("/t/internationalization-localization/280");
await click("#post_2 .post-info.post-date a");
assert.ok(exists(".share-and-invite.modal"), "it shows the modal");
assert.ok(
exists(".share-and-invite.modal .modal-tab.share"),
"it shows the share tab"
);
assert.ok(
exists(".share-and-invite.modal .modal-tab.share.single-tab"),
"it shows only one tab"
);
assert.ok(
!exists(".share-and-invite.modal .modal-tab.invite"),
"it doesnt show the invite tab"
);
});

View File

@ -22,29 +22,6 @@ acceptance("Topic", {
}
});
QUnit.test("Share Popup", async assert => {
await visit("/t/internationalization-localization/280");
assert.ok(!exists("#share-link.visible"), "it is not visible");
await click("button[data-share-url]");
assert.ok(exists("#share-link.visible"), "it shows the popup");
await click("#share-link .close-share");
assert.ok(!exists("#share-link.visible"), "it closes the popup");
// TODO tgxworld This fails on Travis but we need to push the security fix out
// first.
// click('#topic-footer-buttons .btn.create');
// fillIn('.d-editor-input', '<h2><div data-share-url="something">Click</button><h2>');
//
// click('#reply-control .btn.create');
// click('h2 div[data-share-url]');
//
// andThen(() => {
// ok(!exists('#share-link.visible'), 'it does not show the popup');
// });
});
QUnit.test("Showing and hiding the edit controls", async assert => {
await visit("/t/internationalization-localization/280");
@ -98,69 +75,6 @@ QUnit.test("Marking a topic as wiki", async assert => {
assert.ok(find("a.wiki").length === 1, "it shows the wiki icon");
});
QUnit.test("Reply as new topic", async assert => {
await visit("/t/internationalization-localization/280");
await click("button.share:eq(0)");
await click(".reply-as-new-topic a");
assert.ok(exists(".d-editor-input"), "the composer input is visible");
assert.equal(
find(".d-editor-input")
.val()
.trim(),
`Continuing the discussion from [Internationalization / localization](${
window.location.origin
}/t/internationalization-localization/280):`,
"it fills composer with the ring string"
);
assert.equal(
selectKit(".category-chooser")
.header()
.value(),
"2",
"it fills category selector with the right category"
);
});
QUnit.test("Reply as new message", async assert => {
await visit("/t/pm-for-testing/12");
await click("button.share:eq(0)");
await click(".reply-as-new-topic a");
assert.ok(exists(".d-editor-input"), "the composer input is visible");
assert.equal(
find(".d-editor-input")
.val()
.trim(),
`Continuing the discussion from [PM for testing](${
window.location.origin
}/t/pm-for-testing/12):`,
"it fills composer with the ring string"
);
const targets = find(".item span", ".composer-fields");
assert.equal(
$(targets[0]).text(),
"someguy",
"it fills up the composer with the right user to start the PM to"
);
assert.equal(
$(targets[1]).text(),
"test",
"it fills up the composer with the right user to start the PM to"
);
assert.equal(
$(targets[2]).text(),
"Group",
"it fills up the composer with the right group to start the PM to"
);
});
QUnit.test("Visit topic routes", async assert => {
await visit("/t/12");

View File

@ -1,15 +0,0 @@
import componentTest from "helpers/component-test";
moduleForComponent("share-button", { integration: true });
componentTest("share button", {
template: '{{share-button url="https://eviltrout.com"}}',
test(assert) {
assert.ok(this.$(`button.share`).length, "it has all the classes");
assert.ok(
this.$(`button[data-share-url="https://eviltrout.com"]`).length,
"it has the data attribute for sharing"
);
}
});

View File

@ -2136,6 +2136,7 @@ export default {
pinned: false,
pinned_at: null,
details: {
can_invite_via_email: true,
auto_close_at: null,
auto_close_hours: null,
auto_close_based_on_last_post: false,