diff --git a/.circleci/config.yml b/.circleci/config.yml index 99a022451d9..978f852ba41 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -38,7 +38,7 @@ jobs: name: Restoring Meteor dev_bundle cache key: dev_bundle - - run: npm test + # - run: npm test - run: reaction test - save_cache: diff --git a/client/modules/accounts/components/auth/index.js b/client/modules/accounts/components/auth/index.js deleted file mode 100644 index 526fc99f7cf..00000000000 --- a/client/modules/accounts/components/auth/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export LoginButtons from "./loginButtons"; -export SignIn from "./signIn"; -export SignUp from "./signUp"; diff --git a/client/modules/accounts/components/passwordReset/forgot.js b/client/modules/accounts/components/forgotPassword.js similarity index 100% rename from client/modules/accounts/components/passwordReset/forgot.js rename to client/modules/accounts/components/forgotPassword.js diff --git a/client/modules/accounts/components/helpers/index.js b/client/modules/accounts/components/helpers/index.js deleted file mode 100644 index be7e8d25998..00000000000 --- a/client/modules/accounts/components/helpers/index.js +++ /dev/null @@ -1 +0,0 @@ -export LoginFormMessages from "./loginFormMessages"; diff --git a/client/modules/accounts/components/helpers/loginFormMessages.js b/client/modules/accounts/components/helpers/loginFormMessages.js deleted file mode 100644 index a07b8ccc0d3..00000000000 --- a/client/modules/accounts/components/helpers/loginFormMessages.js +++ /dev/null @@ -1,41 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; - -class LoginFormMessages extends Component { - static propTypes = { - formMessages: PropTypes.object, - loginFormMessages: PropTypes.func - } - - renderFormMessages() { - if (this.props.loginFormMessages) { - if (this.props.formMessages.info) { - return ( -
-

- {this.props.loginFormMessages()} -

-
- ); - } else if (this.props.formMessages.alerts) { - return ( -
-

- {this.props.loginFormMessages()} -

-
- ); - } - } - } - - render() { - return ( -
- {this.renderFormMessages()} -
- ); - } -} - -export default LoginFormMessages; diff --git a/client/modules/accounts/components/index.js b/client/modules/accounts/components/index.js index 2d0c5401ca8..1eab4a3b539 100644 --- a/client/modules/accounts/components/index.js +++ b/client/modules/accounts/components/index.js @@ -1,3 +1,8 @@ -export { SignIn, SignUp, LoginButtons } from "./auth"; -export { Forgot, UpdatePasswordOverlay } from "./passwordReset"; -export { LoginFormMessages } from "./helpers"; +export { default as ForgotPassword } from "./forgotPassword"; +export { default as Login } from "./login"; +export { default as LoginButtons } from "./loginButtons"; +export { default as LoginFormMessages } from "./loginFormMessages"; +export { default as MainDropdown } from "./mainDropdown"; +export { default as SignIn } from "./signIn"; +export { default as SignUp } from "./signUp"; +export { default as UpdatePasswordOverlay } from "./updatePasswordOverlay"; diff --git a/client/modules/accounts/containers/auth/loginContainer.js b/client/modules/accounts/components/login.js similarity index 73% rename from client/modules/accounts/containers/auth/loginContainer.js rename to client/modules/accounts/components/login.js index 37ecbd3fbdb..ef379594b4f 100644 --- a/client/modules/accounts/containers/auth/loginContainer.js +++ b/client/modules/accounts/components/login.js @@ -1,17 +1,21 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; +import { Components, registerComponent } from "@reactioncommerce/reaction-components"; import { Random } from "meteor/random"; -import { composeWithTracker } from "/lib/api/compose"; -import AuthContainer from "./authContainer"; -import { ForgotContainer } from "../passwordReset"; -class LoginContainer extends Component { +class Login extends Component { static propTypes = { credentials: PropTypes.object, loginFormCurrentView: PropTypes.string, uniqueId: PropTypes.string } + static defaultProps = { + credentials: {}, + loginFormCurrentView: "loginFormSignInView", + uniqueId: Random.id() + } + constructor(props) { super(props); @@ -51,7 +55,7 @@ class LoginContainer extends Component { render() { if (this.state.currentView === "loginFormSignInView" || this.state.currentView === "loginFormSignUpView") { return ( - { + if (props.loginFormMessages) { + if (props.formMessages.info) { + return ( +
+

+ {props.loginFormMessages()} +

+
+ ); + } else if (props.formMessages.alerts) { + return ( +
+

+ {props.loginFormMessages()} +

+
+ ); + } + } + return null; +}; + +LoginFormMessages.propTypes = { + formMessages: PropTypes.object, + loginFormMessages: PropTypes.func +}; + +export default LoginFormMessages; diff --git a/client/modules/accounts/components/dropdown/mainDropdown.js b/client/modules/accounts/components/mainDropdown.js similarity index 83% rename from client/modules/accounts/components/dropdown/mainDropdown.js rename to client/modules/accounts/components/mainDropdown.js index 76191be9f24..c51cafdf9c6 100644 --- a/client/modules/accounts/components/dropdown/mainDropdown.js +++ b/client/modules/accounts/components/mainDropdown.js @@ -1,8 +1,9 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; +import { Components } from "@reactioncommerce/reaction-components"; import { Reaction } from "/client/api"; -import { Button, DropDownMenu, MenuItem, Translation } from "/imports/plugins/core/ui/client/components"; -import { LoginContainer } from "../../containers/auth"; +import { Translation } from "/imports/plugins/core/ui/client/components"; +import Login from "./login"; const iconStyle = { margin: "10px 10px 10px 6px", @@ -20,7 +21,7 @@ const menuStyle = { class MainDropdown extends Component { static propTypes = { adminShortcuts: PropTypes.object, - currentUser: PropTypes.oneOfType( + currentAccount: PropTypes.oneOfType( [PropTypes.bool, PropTypes.object] ), handleChange: PropTypes.func, @@ -33,18 +34,18 @@ class MainDropdown extends Component { buttonElement() { return ( - + ); } renderAdminIcons() { return ( Reaction.Apps(this.props.adminShortcuts).map((shortcut) => ( - ( - - + ); @@ -101,10 +102,10 @@ class MainDropdown extends Component { render() { return ( -
- {this.props.currentUser ? +
+ {this.props.currentAccount ?
- - {this.renderUserIcons()} {this.renderAdminIcons()} {this.renderSignOutButton()} - - +
:
diff --git a/client/modules/accounts/components/passwordReset/index.js b/client/modules/accounts/components/passwordReset/index.js deleted file mode 100644 index dffa716c048..00000000000 --- a/client/modules/accounts/components/passwordReset/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export Forgot from "./forgot"; -export UpdatePasswordOverlay from "./updatePasswordOverlay"; diff --git a/client/modules/accounts/components/auth/signIn.js b/client/modules/accounts/components/signIn.js similarity index 100% rename from client/modules/accounts/components/auth/signIn.js rename to client/modules/accounts/components/signIn.js diff --git a/client/modules/accounts/components/auth/signUp.js b/client/modules/accounts/components/signUp.js similarity index 100% rename from client/modules/accounts/components/auth/signUp.js rename to client/modules/accounts/components/signUp.js diff --git a/client/modules/accounts/components/passwordReset/updatePasswordOverlay.js b/client/modules/accounts/components/updatePasswordOverlay.js similarity index 100% rename from client/modules/accounts/components/passwordReset/updatePasswordOverlay.js rename to client/modules/accounts/components/updatePasswordOverlay.js diff --git a/client/modules/accounts/containers/auth/authContainer.js b/client/modules/accounts/containers/auth.js similarity index 92% rename from client/modules/accounts/containers/auth/authContainer.js rename to client/modules/accounts/containers/auth.js index 7de73268c0e..4c4437c54e2 100644 --- a/client/modules/accounts/containers/auth/authContainer.js +++ b/client/modules/accounts/containers/auth.js @@ -1,13 +1,13 @@ import _ from "lodash"; import React, { Component } from "react"; import PropTypes from "prop-types"; +import { registerComponent, composeWithTracker } from "@reactioncommerce/reaction-components"; import { Meteor } from "meteor/meteor"; import { Accounts } from "meteor/accounts-base"; import { Router } from "/client/api"; -import { composeWithTracker } from "/lib/api/compose"; -import { SignIn, SignUp, LoginButtons } from "../../components"; -import { MessagesContainer } from "../helpers"; -import { ServiceConfigHelper } from "../../helpers"; +import { SignIn, SignUp, LoginButtons } from "../components"; +import MessagesContainer from "./messages"; +import { ServiceConfigHelper } from "../helpers"; import { LoginFormSharedHelpers } from "/client/modules/accounts/helpers"; import { LoginFormValidation } from "/lib/api"; @@ -22,7 +22,7 @@ class AuthContainer extends Component { super(props); this.state = { - formMessages: props.formMessages, + formMessages: props.formMessages || {}, isLoading: false }; @@ -210,12 +210,9 @@ class AuthContainer extends Component { } function composer(props, onData) { - const formMessages = {}; - - onData(null, { - formMessages, - currentRoute: Router.current() - }); + onData(null, { currentRoute: Router.current() }); } +registerComponent("AuthContainer", AuthContainer, composeWithTracker(composer)); + export default composeWithTracker(composer)(AuthContainer); diff --git a/client/modules/accounts/containers/auth/index.js b/client/modules/accounts/containers/auth/index.js deleted file mode 100644 index 7e3508baa54..00000000000 --- a/client/modules/accounts/containers/auth/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export AuthContainer from "./authContainer"; -export LoginContainer from "./loginContainer"; diff --git a/client/modules/accounts/containers/dropdown/mainDropdownContainer.js b/client/modules/accounts/containers/dropdown/mainDropdownContainer.js deleted file mode 100644 index 2865225a81c..00000000000 --- a/client/modules/accounts/containers/dropdown/mainDropdownContainer.js +++ /dev/null @@ -1,164 +0,0 @@ -import React, { Component } from "react"; -import { Meteor } from "meteor/meteor"; -import { Accounts } from "meteor/accounts-base"; -import { Roles } from "meteor/alanning:roles"; -import { Session } from "meteor/session"; -import { Gravatar } from "meteor/jparker:gravatar"; -import { Reaction, Logger } from "/client/api"; -import { i18nextDep, i18next } from "/client/api"; -import { composeWithTracker } from "/lib/api/compose"; -import * as Collections from "/lib/collections"; -import { Tags } from "/lib/collections"; -import MainDropdown from "../../components/dropdown/mainDropdown"; - -class MainDropdownContainer extends Component { - constructor(props) { - super(props); - this.handleChange = this.handleChange.bind(this); - } - - handleChange = (event, value) => { - event.preventDefault(); - - if (value === "logout") { - return Meteor.logout((error) => { - if (error) { - Logger.warn("Failed to logout.", error); - } - }); - } - - if (value.name === "createProduct") { - Reaction.setUserPreferences("reaction-dashboard", "viewAs", "administrator"); - Meteor.call("products/createProduct", (error, productId) => { - let currentTag; - let currentTagId; - - if (error) { - throw new Meteor.Error("createProduct error", error); - } else if (productId) { - currentTagId = Session.get("currentTag"); - currentTag = Tags.findOne(currentTagId); - if (currentTag) { - Meteor.call("products/updateProductTags", productId, currentTag.name, currentTagId); - } - // go to new product - Reaction.Router.go("product", { - handle: productId - }); - } - }); - } else if (value.name !== "account/profile") { - return Reaction.showActionView(value); - } else if (value.route || value.name) { - const route = value.name || value.route; - return Reaction.Router.go(route); - } - } - - render() { - return ( -
- -
- ); - } -} - -function getCurrentUser() { - const shopId = Reaction.getShopId(); - const user = Accounts.user() || {}; - - if (!shopId || typeof user !== "object") { - return null; - } - - - // shoppers should always be guests - const isGuest = Roles.userIsInRole(user, "guest", shopId); - // but if a user has never logged in then they are anonymous - const isAnonymous = Roles.userIsInRole(user, "anonymous", shopId); - - const account = Collections.Accounts.findOne(user._id); - - return isGuest && !isAnonymous ? account : null; -} - -function getUserGravatar(currentUser, size) { - const options = { - secure: true, - size: size, - default: "identicon" - }; - const user = currentUser || Accounts.user(); - if (!user) { - return false; - } - const account = Collections.Accounts.findOne(user._id); - // first we check picture exists. Picture has higher priority to display - if (account && account.profile && account.profile.picture) { - return account.profile.picture; - } - if (user.emails && user.emails.length === 1) { - const email = user.emails[0].address; - return Gravatar.imageUrl(email, options); - } -} - -function displayName(displayUser) { - i18nextDep.depend(); - - const user = displayUser || Accounts.user(); - - if (user) { - if (user.name) { - return user.name; - } else if (user.username) { - return user.username; - } else if (user.profile && user.profile.name) { - return user.profile.name; - } - - // todo: previous check was user.services !== "anonymous", "resume". Is this - // new check covers previous check? - if (Roles.userIsInRole(user._id || user.userId, "account/profile", - Reaction.getShopId())) { - return i18next.t("accountsUI.guest", { defaultValue: "Guest" }); - } - } -} - -function getAdminShortcutIcons() { - // get shortcuts with audience permissions based on user roles - const roles = Roles.getRolesForUser(Meteor.userId(), Reaction.getShopId()); - - return { - provides: "shortcut", - enabled: true, - audience: roles - }; -} - -const composer = (props, onData) => { - const currentUser = getCurrentUser(); - const userImage = getUserGravatar(currentUser, 40); - const userName = displayName(currentUser); - const adminShortcuts = getAdminShortcutIcons(); - const userShortcuts = { - provides: "userAccountDropdown", - enabled: true - }; - - onData(null, { - adminShortcuts, - currentUser, - userImage, - userName, - userShortcuts - }); -}; - -export default composeWithTracker(composer)(MainDropdownContainer); diff --git a/client/modules/accounts/containers/passwordReset/forgotContainer.js b/client/modules/accounts/containers/forgotPassword.js similarity index 87% rename from client/modules/accounts/containers/passwordReset/forgotContainer.js rename to client/modules/accounts/containers/forgotPassword.js index b05c5971737..62d31122311 100644 --- a/client/modules/accounts/containers/passwordReset/forgotContainer.js +++ b/client/modules/accounts/containers/forgotPassword.js @@ -1,15 +1,15 @@ import _ from "lodash"; import React, { Component } from "react"; import PropTypes from "prop-types"; +import { registerComponent } from "@reactioncommerce/reaction-components"; import { Meteor } from "meteor/meteor"; import { i18next } from "/client/api"; -import { composeWithTracker } from "/lib/api/compose"; import { MessagesContainer } from "../helpers"; -import { Forgot } from "../../components"; +import { ForgotPassword } from "../components"; import { LoginFormValidation } from "/lib/api"; -class ForgotContainer extends Component { +class ForgotPasswordContainer extends Component { static propTypes = { formMessages: PropTypes.object } @@ -18,7 +18,7 @@ class ForgotContainer extends Component { super(props); this.state = { - formMessages: props.formMessages, + formMessages: props.formMessages || {}, isLoading: false, isDisabled: false }; @@ -99,7 +99,7 @@ class ForgotContainer extends Component { render() { return ( - { - let reasons = ""; - if (this.props.messages.info) { - this.props.messages.info.forEach(function (info) { - reasons = info.reason; - }); - } else if (this.props.messages.alerts) { - this.props.messages.alerts.forEach(function (alert) { - reasons = alert.reason; - }); - } - return reasons; - } - - render() { - return ( - - ); - } -} - -export default MessagesContainer; diff --git a/client/modules/accounts/containers/index.js b/client/modules/accounts/containers/index.js index 7c3d195f51b..7b6b510b09d 100644 --- a/client/modules/accounts/containers/index.js +++ b/client/modules/accounts/containers/index.js @@ -1,3 +1,5 @@ -export { LoginContainer, AuthContainer } from "./auth"; -export { ForgotContainer, UpdatePasswordOverlayContainer } from "./passwordReset"; -export { MessagesContainer } from "./helpers"; +export { default as AuthContainer } from "./auth"; +export { default as ForgotPassword } from "./forgotPassword"; +export { default as MainDropdown } from "./mainDropdown"; +export { default as MessagesContainer } from "./messages"; +export { default as UpdatePasswordOverlay } from "./passwordOverlay"; diff --git a/client/modules/accounts/containers/mainDropdown.js b/client/modules/accounts/containers/mainDropdown.js new file mode 100644 index 00000000000..b344d6d734b --- /dev/null +++ b/client/modules/accounts/containers/mainDropdown.js @@ -0,0 +1,138 @@ +import { compose, withProps } from "recompose"; +import { registerComponent, composeWithTracker, withCurrentAccount } from "@reactioncommerce/reaction-components"; +import { Meteor } from "meteor/meteor"; +import { Accounts } from "meteor/accounts-base"; +import { Roles } from "meteor/alanning:roles"; +import { Session } from "meteor/session"; +import { Gravatar } from "meteor/jparker:gravatar"; +import { Reaction, Logger } from "/client/api"; +import { i18nextDep, i18next } from "/client/api"; +import * as Collections from "/lib/collections"; +import { Tags } from "/lib/collections"; +import MainDropdown from "../components/mainDropdown"; + +function getUserGravatar(currentUser, size) { + const options = { + secure: true, + size: size, + default: "identicon" + }; + const user = currentUser || Accounts.user(); + if (!user) { + return false; + } + const account = Collections.Accounts.findOne(user._id); + // first we check picture exists. Picture has higher priority to display + if (account && account.profile && account.profile.picture) { + return account.profile.picture; + } + if (user.emails && user.emails.length === 1) { + const email = user.emails[0].address; + return Gravatar.imageUrl(email, options); + } +} + +function displayName(displayUser) { + i18nextDep.depend(); + + const user = displayUser || Accounts.user(); + + if (user) { + if (user.name) { + return user.name; + } else if (user.username) { + return user.username; + } else if (user.profile && user.profile.name) { + return user.profile.name; + } + + // todo: previous check was user.services !== "anonymous", "resume". Is this + // new check covers previous check? + if (Roles.userIsInRole(user._id || user.userId, "account/profile", + Reaction.getShopId())) { + return i18next.t("accountsUI.guest", { defaultValue: "Guest" }); + } + } +} + +function getAdminShortcutIcons() { + // get shortcuts with audience permissions based on user roles + const roles = Roles.getRolesForUser(Meteor.userId(), Reaction.getShopId()); + + return { + provides: "shortcut", + enabled: true, + audience: roles + }; +} + +function handleChange(event, value) { + event.preventDefault(); + + if (value === "logout") { + return Meteor.logout((error) => { + if (error) { + Logger.error(error, "Failed to logout."); + } + }); + } + + if (value.name === "createProduct") { + Reaction.setUserPreferences("reaction-dashboard", "viewAs", "administrator"); + Meteor.call("products/createProduct", (error, productId) => { + let currentTag; + let currentTagId; + + if (error) { + throw new Meteor.Error("createProduct error", error); + } else if (productId) { + currentTagId = Session.get("currentTag"); + currentTag = Tags.findOne(currentTagId); + if (currentTag) { + Meteor.call("products/updateProductTags", productId, currentTag.name, currentTagId); + } + // go to new product + Reaction.Router.go("product", { + handle: productId + }); + } + }); + } else if (value.name !== "account/profile") { + return Reaction.showActionView(value); + } else if (value.route || value.name) { + const route = value.name || value.route; + return Reaction.Router.go(route); + } +} + +const composer = ({ currentAccount }, onData) => { + const userImage = getUserGravatar(currentAccount, 40); + const userName = displayName(currentAccount); + const adminShortcuts = getAdminShortcutIcons(); + + onData(null, { + adminShortcuts, + userImage, + userName + }); +}; + +const handlers = { + handleChange, + userShortcuts: { + provides: "userAccountDropdown", + enabled: true + } +}; + +registerComponent("MainDropdown", MainDropdown, [ + withCurrentAccount, + withProps(handlers), + composeWithTracker(composer) +]); + +export default compose( + withCurrentAccount, + withProps(handlers), + composeWithTracker(composer) +)(MainDropdown); diff --git a/client/modules/accounts/containers/messages.js b/client/modules/accounts/containers/messages.js new file mode 100644 index 00000000000..3f165b9c09d --- /dev/null +++ b/client/modules/accounts/containers/messages.js @@ -0,0 +1,45 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { registerComponent } from "@reactioncommerce/reaction-components"; +import { LoginFormMessages } from "../components"; + +const wrapComponent = (Comp) => ( + class LoginFormMessagesContainer extends Component { + static propTypes = { + messages: PropTypes.object + } + + constructor() { + super(); + + this.loginFormMessages = this.loginFormMessages.bind(this); + } + + loginFormMessages = () => { + let reasons = ""; + if (this.props.messages.info) { + this.props.messages.info.forEach(function (info) { + reasons = info.reason; + }); + } else if (this.props.messages.alerts) { + this.props.messages.alerts.forEach(function (alert) { + reasons = alert.reason; + }); + } + return reasons; + } + + render() { + return ( + + ); + } + } +); + +registerComponent("LoginFormMessages", LoginFormMessages, wrapComponent); + +export default wrapComponent(LoginFormMessages); diff --git a/client/modules/accounts/containers/passwordOverlay.js b/client/modules/accounts/containers/passwordOverlay.js new file mode 100644 index 00000000000..285e43ce525 --- /dev/null +++ b/client/modules/accounts/containers/passwordOverlay.js @@ -0,0 +1,128 @@ +import _ from "lodash"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { Components, registerComponent } from "@reactioncommerce/reaction-components"; +import { Accounts } from "meteor/accounts-base"; +import { Random } from "meteor/random"; +import { UpdatePasswordOverlay } from "/client/modules/accounts/components"; +import { TranslationProvider } from "/imports/plugins/core/ui/client/providers"; +import { LoginFormValidation } from "/lib/api"; + +const wrapComponent = (Comp) => ( + class UpdatePasswordOverlayContainer extends Component { + static propTypes = { + callback: PropTypes.func, + formMessages: PropTypes.object, + isOpen: PropTypes.bool, + token: PropTypes.string, + uniqueId: PropTypes.string + } + + static defaultProps = { + formMessages: {}, + uniqueId: Random.id() + } + + constructor(props) { + super(props); + + this.state = { + formMessages: props.formMessages, + isOpen: props.isOpen, + isDisabled: false + }; + + this.handleFormSubmit = this.handleFormSubmit.bind(this); + this.handleFormCancel = this.handleFormCancel.bind(this); + this.formMessages = this.formMessages.bind(this); + this.hasError = this.hasError.bind(this); + } + + handleFormSubmit = (event, passwordValue) => { + event.preventDefault(); + + this.setState({ + isDisabled: true + }); + + const password = passwordValue.trim(); + const validatedPassword = LoginFormValidation.password(password); + const errors = {}; + + if (validatedPassword !== true) { + errors.password = validatedPassword; + } + + if (_.isEmpty(errors) === false) { + this.setState({ + isDisabled: false, + formMessages: { + errors: errors + } + }); + return; + } + + Accounts.resetPassword(this.props.token, password, (error) => { + if (error) { + this.setState({ + isDisabled: false, + formMessages: { + alerts: [error] + } + }); + } else { + this.props.callback(); + + this.setState({ + isOpen: !this.state.isOpen + }); + } + }); + } + + handleFormCancel = (event) => { + event.preventDefault(); + this.setState({ + isOpen: !this.state.isOpen + }); + } + + formMessages = () => { + return ( + + ); + } + + hasError = (error) => { + // True here means the field is valid + // We're checking if theres some other message to display + if (error !== true && typeof error !== "undefined") { + return true; + } + + return false; + } + + render() { + return ( + + + + ); + } + } +); + +registerComponent("UpdatePasswordOverlay", UpdatePasswordOverlay, wrapComponent); + +export default wrapComponent(UpdatePasswordOverlay); diff --git a/client/modules/accounts/containers/passwordReset/index.js b/client/modules/accounts/containers/passwordReset/index.js deleted file mode 100644 index ff6c77aa157..00000000000 --- a/client/modules/accounts/containers/passwordReset/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export ForgotContainer from "./forgotContainer"; -export UpdatePasswordOverlayContainer from "./passwordOverlayContainer"; diff --git a/client/modules/accounts/containers/passwordReset/passwordOverlayContainer.js b/client/modules/accounts/containers/passwordReset/passwordOverlayContainer.js deleted file mode 100644 index 494e65767d5..00000000000 --- a/client/modules/accounts/containers/passwordReset/passwordOverlayContainer.js +++ /dev/null @@ -1,132 +0,0 @@ -import _ from "lodash"; -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import { Accounts } from "meteor/accounts-base"; -import { Random } from "meteor/random"; -import { composeWithTracker } from "/lib/api/compose"; -import { UpdatePasswordOverlay } from "/client/modules/accounts/components"; -import { MessagesContainer } from "/client/modules/accounts/containers/helpers"; -import { TranslationProvider } from "/imports/plugins/core/ui/client/providers"; -import { LoginFormValidation } from "/lib/api"; - -class UpdatePasswordOverlayContainer extends Component { - static propTypes = { - callback: PropTypes.func, - formMessages: PropTypes.object, - isOpen: PropTypes.bool, - token: PropTypes.string, - uniqueId: PropTypes.string - } - - constructor(props) { - super(props); - - this.state = { - formMessages: props.formMessages, - isOpen: props.isOpen, - isDisabled: false - }; - - this.handleFormSubmit = this.handleFormSubmit.bind(this); - this.handleFormCancel = this.handleFormCancel.bind(this); - this.formMessages = this.formMessages.bind(this); - this.hasError = this.hasError.bind(this); - } - - handleFormSubmit = (event, passwordValue) => { - event.preventDefault(); - - this.setState({ - isDisabled: true - }); - - const password = passwordValue.trim(); - const validatedPassword = LoginFormValidation.password(password); - const errors = {}; - - if (validatedPassword !== true) { - errors.password = validatedPassword; - } - - if (_.isEmpty(errors) === false) { - this.setState({ - isDisabled: false, - formMessages: { - errors: errors - } - }); - return; - } - - Accounts.resetPassword(this.props.token, password, (error) => { - if (error) { - this.setState({ - isDisabled: false, - formMessages: { - alerts: [error] - } - }); - } else { - this.props.callback(); - - this.setState({ - isOpen: !this.state.isOpen - }); - } - }); - } - - handleFormCancel = (event) => { - event.preventDefault(); - this.setState({ - isOpen: !this.state.isOpen - }); - } - - formMessages = () => { - return ( - - ); - } - - hasError = (error) => { - // True here means the field is valid - // We're checking if theres some other message to display - if (error !== true && typeof error !== "undefined") { - return true; - } - - return false; - } - - render() { - return ( - - - - ); - } -} - -function composer(props, onData) { - const uniqueId = Random.id(); - const formMessages = {}; - - onData(null, { - uniqueId, - formMessages - }); -} - -export default composeWithTracker(composer)(UpdatePasswordOverlayContainer); diff --git a/client/modules/accounts/templates/dropdown/dropdown.html b/client/modules/accounts/templates/dropdown/dropdown.html deleted file mode 100644 index e8bf1d42cd4..00000000000 --- a/client/modules/accounts/templates/dropdown/dropdown.html +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - diff --git a/client/modules/accounts/templates/dropdown/dropdown.js b/client/modules/accounts/templates/dropdown/dropdown.js deleted file mode 100644 index 3c621630346..00000000000 --- a/client/modules/accounts/templates/dropdown/dropdown.js +++ /dev/null @@ -1,78 +0,0 @@ -import { Reaction, Logger } from "/client/api"; -import { Tags } from "/lib/collections"; -import { Session } from "meteor/session"; -import { Meteor } from "meteor/meteor"; -import { Template } from "meteor/templating"; -import { Roles } from "meteor/alanning:roles"; - -Template.loginDropdown.events({ - - /** - * Submit sign up form - * @param {Event} event - jQuery Event - * @param {Template} template - Blaze Template - * @return {void} - */ - "click #logout": (event, template) => { - event.preventDefault(); - template.$(".dropdown-toggle").dropdown("toggle"); - // Meteor.logoutOtherClients(); - Meteor.logout((error) => { - if (error) { - Logger.warn("Failed to logout.", error); - } - }); - }, - - /** - * Submit sign up form - * @param {Event} event - jQuery Event - * @param {Template} template - Blaze Template - * @return {void} - */ - "click .user-accounts-dropdown-apps a": function (event, template) { - if (this.name === "createProduct") { - event.preventDefault(); - event.stopPropagation(); - - Meteor.call("products/createProduct", (error, productId) => { - let currentTag; - let currentTagId; - - if (error) { - throw new Meteor.Error("createProduct error", error); - } else if (productId) { - currentTagId = Session.get("currentTag"); - currentTag = Tags.findOne(currentTagId); - if (currentTag) { - Meteor.call("products/updateProductTags", productId, currentTag.name, currentTagId); - } - Reaction.Router.go("product", { - handle: productId - }); - } - }); - } else if (this.name !== "account/profile") { - event.preventDefault(); - /** TMP **/ - Reaction.showActionView(this); - } else if (this.route || this.name) { - const route = this.name || this.route; - Reaction.Router.go(route); - } - template.$(".dropdown-toggle").dropdown("toggle"); - } -}); - -Template.accountsDropdownApps.helpers({ - reactionAppsOptions() { - // get shortcuts with audience permissions based on user roles - const roles = Roles.getRolesForUser(Meteor.userId(), Reaction.getShopId()); - - return { - provides: "shortcut", - enabled: true, - audience: roles - }; - } -}); diff --git a/client/modules/accounts/templates/dropdown/helpers/helpers.js b/client/modules/accounts/templates/dropdown/helpers/helpers.js deleted file mode 100644 index 28c30b14523..00000000000 --- a/client/modules/accounts/templates/dropdown/helpers/helpers.js +++ /dev/null @@ -1,19 +0,0 @@ -import { Template } from "meteor/templating"; - -export const LoginFormSharedHelpers = { - messages: function () { - return Template.instance().formMessages.get(); - }, - - hasError(error) { - // True here means the field is valid - // We're checking if theres some other message to display - if (error !== true && typeof error !== "undefined") { - return "has-error has-feedback"; - } - }, - capitalize: function (str) { - const finalString = str === null ? "" : String(str); - return finalString.charAt(0).toUpperCase() + finalString.slice(1); - } -}; diff --git a/client/modules/accounts/templates/dropdown/helpers/index.js b/client/modules/accounts/templates/dropdown/helpers/index.js deleted file mode 100644 index 6bc7dc7058d..00000000000 --- a/client/modules/accounts/templates/dropdown/helpers/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { LoginFormSharedHelpers } from "./helpers"; -export { ServiceConfigHelper } from "./util"; diff --git a/client/modules/accounts/templates/dropdown/helpers/templates.js b/client/modules/accounts/templates/dropdown/helpers/templates.js deleted file mode 100644 index fdf7673e1b9..00000000000 --- a/client/modules/accounts/templates/dropdown/helpers/templates.js +++ /dev/null @@ -1,82 +0,0 @@ -import { Meteor } from "meteor/meteor"; -import { Template } from "meteor/templating"; -import { Accounts } from "meteor/accounts-base"; -import { Roles } from "meteor/alanning:roles"; -import { Gravatar } from "meteor/jparker:gravatar"; -import { Reaction, i18next, i18nextDep } from "/client/api"; -import * as Collections from "/lib/collections"; - -Template.registerHelper("getGravatar", function (currentUser, size) { - const options = { - secure: true, - size: size, - default: "identicon" - }; - const user = currentUser || Accounts.user(); - if (!user) return false; - const account = Collections.Accounts.findOne(user._id); - // first we check picture exists. Picture has higher priority to display - if (account && account.profile && account.profile.picture) { - return account.profile.picture; - } - if (user.emails && user.emails.length === 1) { - const email = user.emails[0].address; - return Gravatar.imageUrl(email, options); - } -}); - -/** - * registerHelper displayName - */ -Template.registerHelper("displayName", function (displayUser) { - i18nextDep.depend(); - - const user = displayUser || Accounts.user(); - if (user) { - if (user.profile && user.profile.name) { - return user.profile.name; - } else if (user.username) { - return user.username; - } - - // todo: previous check was user.services !== "anonymous", "resume". Is this - // new check covers previous check? - if (Roles.userIsInRole(user._id || user.userId, "account/profile", - Reaction.getShopId())) { - return i18next.t("accountsUI.guest", { defaultValue: "Guest" }); - } - } -}); - -/** - * registerHelper fName - */ - -Template.registerHelper("fName", function (displayUser) { - const user = displayUser || Meteor.user(); - if (user && user.profile && user.profile.name) { - return user.profile.name.split(" ")[0]; - } else if (user && user.username) { - return user.username.name.split(" ")[0]; - } - if (user && user.services) { - const username = (function () { - switch (false) { - case !user.services.twitter: - return user.services.twitter.first_name; - case !user.services.google: - return user.services.google.given_name; - case !user.services.facebook: - return user.services.facebook.first_name; - case !user.services.instagram: - return user.services.instagram.first_name; - case !user.services.pinterest: - return user.services.pinterest.first_name; - default: - return i18next.t("accountsUI.guest", { defaultValue: "Guest" }); - } - })(); - return username; - } - return i18next.t("accountsUI.signIn", { defaultValue: "Sign in" }); -}); diff --git a/client/modules/accounts/templates/dropdown/helpers/util.js b/client/modules/accounts/templates/dropdown/helpers/util.js deleted file mode 100644 index fb47aab4fa5..00000000000 --- a/client/modules/accounts/templates/dropdown/helpers/util.js +++ /dev/null @@ -1,97 +0,0 @@ -import _ from "lodash"; -import { Accounts } from "meteor/accounts-base"; -import { ServiceConfiguration } from "meteor/service-configuration"; - -function capitalize(str) { - const finalString = str === null ? "" : String(str); - return finalString.charAt(0).toUpperCase() + str.slice(1); -} - -const providers = { - Facebook: {}, - Google: {}, - Twitter: {} -}; - -providers.Facebook.fields = function () { - return [ - { property: "appId", label: "App ID" }, - { property: "secret", label: "App Secret" } - ]; -}; - -providers.Google.fields = function () { - return [ - { property: "clientId", label: "Client ID" }, - { property: "secret", label: "Client secret" } - ]; -}; - -providers.Twitter.fields = function () { - return [ - { property: "consumerKey", label: "API key" }, - { property: "secret", label: "API secret" } - ]; -}; - -export class ServiceConfigHelper { - availableServices() { - const services = Package["accounts-oauth"] ? Accounts.oauth.serviceNames() : []; - services.sort(); - - return services; - } - - capitalizedServiceName(name) { - if (name === "meteor-developer") { - return "MeteorDeveloperAccount"; - } - - return capitalize(name); - } - - configFieldsForService(name) { - const capitalizedName = this.capitalizedServiceName(name); - const template = providers[capitalizedName]; - - if (template) { - const fields = template.fields(); - - return _.map(fields, (field) => { - if (!field.type) { - field.type = field.property === "secret" ? "password" : "text"; - } - - return _.extend(field, { - type: field.type - }); - }); - } - - return []; - } - - services(extendEach) { - const availableServices = this.availableServices(); - const configurations = ServiceConfiguration.configurations.find().fetch(); - - return _.map(availableServices, (name) => { - const matchingConfigurations = _.filter(configurations, { service: name }); - let service = { - name, - label: this.capitalizedServiceName(name), - fields: this.configFieldsForService(name) - }; - - if (matchingConfigurations.length) { - service = _.extend(service, matchingConfigurations[0]); - } - - if (_.isFunction(extendEach)) { - service = _.extend(service, extendEach(service) || {}); - } - - return service; - }); - } -} diff --git a/client/modules/accounts/templates/login/loginForm.js b/client/modules/accounts/templates/login/loginForm.js index 7a6a0adf73f..36ca3d61f0d 100644 --- a/client/modules/accounts/templates/login/loginForm.js +++ b/client/modules/accounts/templates/login/loginForm.js @@ -1,5 +1,5 @@ +import { Components } from "@reactioncommerce/reaction-components"; import { Template } from "meteor/templating"; -import { LoginContainer, MessagesContainer } from "../../containers"; Template.loginForm.helpers({ component() { @@ -7,7 +7,7 @@ Template.loginForm.helpers({ return { ...currentData, - component: LoginContainer + component: Components.Login }; } }); @@ -17,7 +17,7 @@ Template.loginFormMessages.helpers({ const currentData = Template.currentData() || {}; return { ...currentData, - component: MessagesContainer + component: Components.LoginFormMessages }; } }); diff --git a/client/modules/core/main.js b/client/modules/core/main.js index 25d06bf6c7d..55ab7cfcca7 100644 --- a/client/modules/core/main.js +++ b/client/modules/core/main.js @@ -91,7 +91,7 @@ export default { let group = this.getShopId(); let permissions = ["owner"]; let id = ""; - const userId = checkUserId || this.userId || Meteor.userId(); + const userId = checkUserId || Meteor.userId(); // // local roleCheck function // is the bulk of the logic diff --git a/client/modules/i18n/templates/currency/containers/currencyContainer.js b/client/modules/i18n/templates/currency/containers/currencyContainer.js index 05d822be261..83e8d4d71d7 100644 --- a/client/modules/i18n/templates/currency/containers/currencyContainer.js +++ b/client/modules/i18n/templates/currency/containers/currencyContainer.js @@ -2,7 +2,7 @@ import React, { Component } from "react"; import { Meteor } from "meteor/meteor"; import { Match } from "meteor/check"; import { Reaction } from "/client/api"; -import { composeWithTracker } from "/lib/api/compose"; +import { composeWithTracker } from "@reactioncommerce/reaction-components"; import { Cart, Shops } from "/lib/collections"; import Currency from "../components/currency"; diff --git a/client/modules/i18n/templates/header/containers/i18nContainer.js b/client/modules/i18n/templates/header/containers/i18nContainer.js index ac4e31c812a..c4c340c9b52 100644 --- a/client/modules/i18n/templates/header/containers/i18nContainer.js +++ b/client/modules/i18n/templates/header/containers/i18nContainer.js @@ -2,7 +2,7 @@ import React, { Component } from "react"; import { Reaction } from "/client/api"; import { Meteor } from "meteor/meteor"; import { Shops } from "/lib/collections"; -import { composeWithTracker } from "/lib/api/compose"; +import { composeWithTracker } from "@reactioncommerce/reaction-components"; import Language from "../components/i18n"; class LanguageDropdownContainer extends Component { diff --git a/imports/plugins/core/accounts/client/index.js b/imports/plugins/core/accounts/client/index.js deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/imports/plugins/core/checkout/client/components/cartDrawer.js b/imports/plugins/core/checkout/client/components/cartDrawer.js index 3edbf03a8c5..c9677f99ab1 100644 --- a/imports/plugins/core/checkout/client/components/cartDrawer.js +++ b/imports/plugins/core/checkout/client/components/cartDrawer.js @@ -1,43 +1,40 @@ import React from "react"; import PropTypes from "prop-types"; -import CartSubTotals from "../container/cartSubTotalContainer"; -import CartItems from "./cartItems"; +import { Components } from "@reactioncommerce/reaction-components"; -const cartDrawer = ({ productItems, pdpPath, handleRemoveItem, handleCheckout, handleImage, handleLowInventory, handleShowProduct }) => { - return ( -
-
-
-
- -
- {productItems.map(item => { - return ( -
- -
- ); - })} +const CartDrawer = ({ productItems, pdpPath, handleRemoveItem, handleCheckout, handleImage, handleLowInventory, handleShowProduct }) => ( +
+
+
+
+
-
-
-
- - Checkout now - + {productItems.map(item => { + return ( +
+ +
+ ); + })}
- ); -}; +
+
+ + Checkout now + +
+
+); -cartDrawer.propTypes = { +CartDrawer.propTypes = { handleCheckout: PropTypes.func, handleImage: PropTypes.func, handleLowInventory: PropTypes.func, @@ -47,4 +44,4 @@ cartDrawer.propTypes = { productItems: PropTypes.array }; -export default cartDrawer; +export default CartDrawer; diff --git a/imports/plugins/core/checkout/client/components/cartIcon.js b/imports/plugins/core/checkout/client/components/cartIcon.js index a9cd220922c..51d8e559fb6 100644 --- a/imports/plugins/core/checkout/client/components/cartIcon.js +++ b/imports/plugins/core/checkout/client/components/cartIcon.js @@ -1,31 +1,18 @@ -import React, { Component } from "react"; +import React from "react"; import PropTypes from "prop-types"; -import Velocity from "velocity-animate"; -import { Reaction } from "/client/api"; -class CartIcon extends Component { - static propTypes = { - cart: PropTypes.object - } +const CartIcon = ({ handleClick, cart }) => ( +
+ + + +
{cart ? cart.cartCount() : 0}
+
+); - handleClick = (event) => { - event.preventDefault(); - const cartDrawer = document.querySelector("#cart-drawer-container"); - Velocity(cartDrawer, { opacity: 1 }, 300, () => { - Reaction.toggleSession("displayCart"); - }); - } - - render() { - return ( -
- - - -
{this.props.cart ? this.props.cart.cartCount() : 0}
-
- ); - } -} +CartIcon.propTypes = { + cart: PropTypes.object, + handleClick: PropTypes.func.isRequired +}; export default CartIcon; diff --git a/imports/plugins/core/checkout/client/components/cartItems.js b/imports/plugins/core/checkout/client/components/cartItems.js index 0c42ed2daf6..374176d0ded 100644 --- a/imports/plugins/core/checkout/client/components/cartItems.js +++ b/imports/plugins/core/checkout/client/components/cartItems.js @@ -1,5 +1,6 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; +import { registerComponent } from "@reactioncommerce/reaction-components"; class CartItems extends Component { static propTypes = { @@ -69,4 +70,6 @@ class CartItems extends Component { } } +registerComponent("CartItems", CartItems); + export default CartItems; diff --git a/imports/plugins/core/checkout/client/components/cartPanel.js b/imports/plugins/core/checkout/client/components/cartPanel.js new file mode 100644 index 00000000000..3c9b58b280f --- /dev/null +++ b/imports/plugins/core/checkout/client/components/cartPanel.js @@ -0,0 +1,32 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { Components } from "@reactioncommerce/reaction-components"; + +const CartPanel = (props) => ( +
+ + + +
{}
+
+ +
+
+); + +CartPanel.propTypes = { + checkout: PropTypes.func, + onClick: PropTypes.func +}; + +export default CartPanel; diff --git a/imports/plugins/core/checkout/client/components/cartSubTotal.js b/imports/plugins/core/checkout/client/components/cartSubTotal.js index d9ec8d9f6ee..bfa17618ede 100644 --- a/imports/plugins/core/checkout/client/components/cartSubTotal.js +++ b/imports/plugins/core/checkout/client/components/cartSubTotal.js @@ -1,6 +1,6 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; -import { Currency, Translation } from "/imports/plugins/core/ui/client/components/"; +import { Components } from "@reactioncommerce/reaction-components"; class CartSubTotal extends Component { static propTypes = { @@ -16,8 +16,8 @@ class CartSubTotal extends Component { if (Number(this.props.cartDiscount) > 0) { return ( - - + + ); } @@ -26,8 +26,8 @@ class CartSubTotal extends Component { if (Number(this.props.cartShipping) > 0) { return ( - - + + ); } @@ -36,8 +36,8 @@ class CartSubTotal extends Component { if (Number(this.props.cartTaxes) > 0) { return ( - - + + ); } @@ -48,23 +48,23 @@ class CartSubTotal extends Component {
- + - + - - + + {this.validateDiscount()} {this.validateShipping()} {this.validateTaxes()} - - + +
{this.props.cartCount}
diff --git a/imports/plugins/core/checkout/client/components/emptyCartDrawer.js b/imports/plugins/core/checkout/client/components/emptyCartDrawer.js index a0e87fa4425..e5a70d35e39 100644 --- a/imports/plugins/core/checkout/client/components/emptyCartDrawer.js +++ b/imports/plugins/core/checkout/client/components/emptyCartDrawer.js @@ -1,9 +1,18 @@ import React from "react"; import PropTypes from "prop-types"; -import { Button, Translation } from "/imports/plugins/core/ui/client/components"; +import { $ } from "meteor/jquery"; +import { Components, registerComponent } from "@reactioncommerce/reaction-components"; +import { Reaction } from "/client/api"; +function handleKeepShopping(event) { + event.stopPropagation(); + event.preventDefault(); + return $("#cart-drawer-container").fadeOut(300, function () { + return Reaction.toggleSession("displayCart"); + }); +} -const EmptyCartDrawer = ({ keepShopping }) => { +const EmptyCartDrawer = () => { return (
@@ -12,17 +21,17 @@ const EmptyCartDrawer = ({ keepShopping }) => {

- +

-
@@ -35,4 +44,6 @@ EmptyCartDrawer.propTypes = { keepShopping: PropTypes.func }; +registerComponent("EmptyCartDrawer", EmptyCartDrawer); + export default EmptyCartDrawer; diff --git a/imports/plugins/core/checkout/client/container/cartIconContainer.js b/imports/plugins/core/checkout/client/container/cartIconContainer.js deleted file mode 100644 index 39bed4dc6c5..00000000000 --- a/imports/plugins/core/checkout/client/container/cartIconContainer.js +++ /dev/null @@ -1,29 +0,0 @@ -import React, { Component } from "react"; -import { Cart } from "/lib/collections"; -import { composeWithTracker } from "/lib/api/compose"; -import { Reaction } from "/client/api"; -import CartIcon from "../components/cartIcon"; - -class CartIconContainer extends Component { - render() { - return ( -
- -
- ); - } -} - -const composer = (props, onData) => { - const subscription = Reaction.Subscriptions.Cart; - - if (subscription.ready()) { - const cart = Cart.findOne(); - - onData(null, { - cart: cart - }); - } -}; - -export default composeWithTracker(composer)(CartIconContainer); diff --git a/imports/plugins/core/checkout/client/container/emptyCartContainer.js b/imports/plugins/core/checkout/client/container/emptyCartContainer.js deleted file mode 100644 index 1bfb59bb719..00000000000 --- a/imports/plugins/core/checkout/client/container/emptyCartContainer.js +++ /dev/null @@ -1,23 +0,0 @@ -import React, { Component } from "react"; -import { $ } from "meteor/jquery"; -import { Reaction } from "/client/api"; -import EmptyCartDrawer from "../components/emptyCartDrawer"; - -class EmptyCartContainer extends Component { - handleKeepShopping(event) { - event.stopPropagation(); - event.preventDefault(); - return $("#cart-drawer-container").fadeOut(300, function () { - return Reaction.toggleSession("displayCart"); - }); - } - render() { - return ( -
- -
- ); - } -} - -export default EmptyCartContainer; diff --git a/imports/plugins/core/checkout/client/container/cartDrawerContainer.js b/imports/plugins/core/checkout/client/containers/cartDrawerContainer.js similarity index 68% rename from imports/plugins/core/checkout/client/container/cartDrawerContainer.js rename to imports/plugins/core/checkout/client/containers/cartDrawerContainer.js index 3ebc9754e5e..3a2efa7d906 100644 --- a/imports/plugins/core/checkout/client/container/cartDrawerContainer.js +++ b/imports/plugins/core/checkout/client/containers/cartDrawerContainer.js @@ -1,34 +1,28 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; +import { compose, withProps } from "recompose"; +import { registerComponent, composeWithTracker } from "@reactioncommerce/reaction-components"; import { $ } from "meteor/jquery"; import { Session } from "meteor/session"; import { Meteor } from "meteor/meteor"; import { Cart, Media } from "/lib/collections"; import { Reaction } from "/client/api"; -import { composeWithTracker } from "/lib/api/compose"; -import { Loading } from "/imports/plugins/core/ui/client/components"; import CartDrawer from "../components/cartDrawer"; -class CartDrawerContainer extends Component { - static propTypes = { - defaultImage: PropTypes.object, - lowInventory: PropTypes.bool, - productItems: PropTypes.array - } - +// event handlers to pass in as props +const handlers = { handleImage(item) { const { defaultImage } = item; if (defaultImage && defaultImage.url({ store: "small" })) { return defaultImage; } return false; - } + }, + /** * showLowInventoryWarning * @param {Object} productItem - product item object * @return {Boolean} return true if low inventory on variant */ - showItemLowInventoryWarning(productItem) { + handleLowInventory(productItem) { const { variants } = productItem; if (variants && variants.inventoryPolicy && variants.lowInventoryWarningThreshold) { @@ -36,20 +30,16 @@ class CartDrawerContainer extends Component { variants.lowInventoryWarningThreshold; } return false; - } - - handleLowInventory = (productItem) => { - return this.showItemLowInventoryWarning(productItem); - } + }, - handleShowProduct = (productItem) => { + handleShowProduct(productItem) { if (productItem) { Reaction.Router.go("product", { handle: productItem.productId, variantId: productItem.variants._id }); } - } + }, pdpPath(productItem) { if (productItem) { @@ -61,7 +51,7 @@ class CartDrawerContainer extends Component { } }); } - } + }, handleRemoveItem(event) { event.stopPropagation(); @@ -70,28 +60,16 @@ class CartDrawerContainer extends Component { $(`#${currentCartItemId}`).fadeOut(500, () => { return Meteor.call("cart/removeFromCart", currentCartItemId); }); - } + }, + handleCheckout() { $("#cart-drawer-container").fadeOut(); Session.set("displayCart", false); return Reaction.Router.go("cart/checkout"); } - render() { - const { productItems } = this.props; - return ( - - ); - } -} +}; +// reactive Tracker wrapped function function composer(props, onData) { const userId = Meteor.userId(); const shopId = Reaction.getShopId(); @@ -116,4 +94,13 @@ function composer(props, onData) { }); } -export default composeWithTracker(composer, Loading)(CartDrawerContainer); +// register the containers +registerComponent("CartDrawer", CartDrawer, [ + withProps(handlers), + composeWithTracker(composer) +]); + +export default compose( + withProps(handlers), + composeWithTracker(composer) +)(CartDrawer); diff --git a/imports/plugins/core/checkout/client/containers/cartIconContainer.js b/imports/plugins/core/checkout/client/containers/cartIconContainer.js new file mode 100644 index 00000000000..c46d8d4c54e --- /dev/null +++ b/imports/plugins/core/checkout/client/containers/cartIconContainer.js @@ -0,0 +1,35 @@ +import Velocity from "velocity-animate"; +import { compose, withProps } from "recompose"; +import { registerComponent, composeWithTracker } from "@reactioncommerce/reaction-components"; +import { Cart } from "/lib/collections"; +import { Reaction } from "/client/api"; +import CartIcon from "../components/cartIcon"; + +const handlers = { + handleClick(e) { + e.preventDefault(); + const cartDrawer = document.querySelector("#cart-drawer-container"); + Velocity(cartDrawer, { opacity: 1 }, 300, () => { + Reaction.toggleSession("displayCart"); + }); + } +}; + +const composer = (props, onData) => { + const subscription = Reaction.Subscriptions.Cart; + + if (subscription.ready()) { + const cart = Cart.findOne(); + onData(null, { cart }); + } +}; + +registerComponent("CartIcon", CartIcon, [ + withProps(handlers), + composeWithTracker(composer) +]); + +export default compose( + withProps(handlers), + composeWithTracker(composer) +)(CartIcon); diff --git a/imports/plugins/core/checkout/client/containers/cartPanelContainer.js b/imports/plugins/core/checkout/client/containers/cartPanelContainer.js new file mode 100644 index 00000000000..a6b717fdc22 --- /dev/null +++ b/imports/plugins/core/checkout/client/containers/cartPanelContainer.js @@ -0,0 +1,18 @@ +import { withProps } from "recompose"; +import { registerComponent } from "@reactioncommerce/reaction-components"; +import { $ } from "meteor/jquery"; +import { Session } from "meteor/session"; +import { Reaction } from "/client/api"; +import CartPanel from "../components/cartPanel"; + +const handlers = { + checkout() { + $("#cart-drawer-container").fadeOut(); + Session.set("displayCart", false); + return Reaction.Router.go("cart/checkout"); + } +}; + +registerComponent("CartPanel", CartPanel, withProps(handlers)); + +export default withProps(handlers)(CartPanel); diff --git a/imports/plugins/core/checkout/client/container/cartSubTotalContainer.js b/imports/plugins/core/checkout/client/containers/cartSubTotalContainer.js similarity index 51% rename from imports/plugins/core/checkout/client/container/cartSubTotalContainer.js rename to imports/plugins/core/checkout/client/containers/cartSubTotalContainer.js index 8e694cf207f..16ba9bc3eb3 100644 --- a/imports/plugins/core/checkout/client/container/cartSubTotalContainer.js +++ b/imports/plugins/core/checkout/client/containers/cartSubTotalContainer.js @@ -1,19 +1,7 @@ -import React, { Component } from "react"; +import { registerComponent, composeWithTracker } from "@reactioncommerce/reaction-components"; import { Cart } from "/lib/collections"; -import { composeWithTracker } from "/lib/api/compose"; -import { Loading } from "/imports/plugins/core/ui/client/components"; import CartSubTotal from "../components/cartSubTotal"; -class CartSubTotalContainer extends Component { - render() { - return ( - - ); - } -} - function composer(props, onData) { const cart = Cart.findOne(); if (cart) { @@ -25,9 +13,9 @@ function composer(props, onData) { cartTaxes: cart.cartTaxes(), cartTotal: cart.cartTotal() }); - } else { - onData(null, {}); } } -export default composeWithTracker(composer, Loading)(CartSubTotalContainer); +registerComponent("CartSubTotal", CartSubTotal, composeWithTracker(composer)); + +export default composeWithTracker(composer)(CartSubTotal); diff --git a/imports/plugins/core/checkout/client/index.js b/imports/plugins/core/checkout/client/index.js index 788200366b5..d851c168016 100644 --- a/imports/plugins/core/checkout/client/index.js +++ b/imports/plugins/core/checkout/client/index.js @@ -19,3 +19,15 @@ import "./templates/checkout/review/review.html"; import "./templates/checkout/review/review.js"; import "./templates/checkout/checkout.html"; import "./templates/checkout/checkout.js"; + +export { default as CartDrawer } from "./components/cartDrawer"; +export { default as CartIcon } from "./components/cartIcon"; +export { default as CartItems } from "./components/cartItems"; +export { default as CartPanel } from "./components/cartPanel"; +export { default as CartSubTotal } from "./components/cartSubTotal"; +export { default as EmptyCartDrawer } from "./components/emptyCartDrawer"; + +export { default as CartDrawerContainer } from "./containers/cartDrawerContainer"; +export { default as CartIconContainer } from "./containers/cartIconContainer"; +export { default as CartPanelContainer } from "./containers/cartPanelContainer"; +export { default as CartSubTotalContainer } from "./containers/cartSubTotalContainer"; diff --git a/imports/plugins/core/checkout/client/templates/cartDrawer/cartDrawer.html b/imports/plugins/core/checkout/client/templates/cartDrawer/cartDrawer.html index 94bf474624d..6e4d9af34ed 100644 --- a/imports/plugins/core/checkout/client/templates/cartDrawer/cartDrawer.html +++ b/imports/plugins/core/checkout/client/templates/cartDrawer/cartDrawer.html @@ -1,7 +1,7 @@ @@ -9,12 +9,11 @@
{{> React component=EmptyCartDrawer}}
- diff --git a/imports/plugins/core/checkout/client/templates/cartDrawer/cartDrawer.js b/imports/plugins/core/checkout/client/templates/cartDrawer/cartDrawer.js index 2fffad7bc97..16741a4cafd 100644 --- a/imports/plugins/core/checkout/client/templates/cartDrawer/cartDrawer.js +++ b/imports/plugins/core/checkout/client/templates/cartDrawer/cartDrawer.js @@ -1,10 +1,10 @@ +import Swiper from "swiper"; +import { Components } from "@reactioncommerce/reaction-components"; import { $ } from "meteor/jquery"; import { Cart } from "/lib/collections"; import { Session } from "meteor/session"; import { Template } from "meteor/templating"; -import Swiper from "swiper"; -import CartDrawerContainer from "../../container/cartDrawerContainer"; -import EmptyCartDrawer from "../../container/emptyCartContainer"; + /** * cartDrawer helpers * @@ -66,7 +66,7 @@ Template.openCartDrawer.onRendered(function () { Template.openCartDrawer.helpers({ CartDrawerContainer() { - return CartDrawerContainer; + return Components.CartDrawer; } }); @@ -76,6 +76,6 @@ Template.emptyCartDrawer.onRendered(function () { Template.emptyCartDrawer.helpers({ EmptyCartDrawer() { - return EmptyCartDrawer; + return Components.EmptyCartDrawer; } }); diff --git a/imports/plugins/core/checkout/client/templates/cartPanel/component/cartPanel.js b/imports/plugins/core/checkout/client/templates/cartPanel/component/cartPanel.js deleted file mode 100644 index ae994452463..00000000000 --- a/imports/plugins/core/checkout/client/templates/cartPanel/component/cartPanel.js +++ /dev/null @@ -1,36 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import { Button } from "/imports/plugins/core/ui/client/components"; - -class CartPanel extends Component { - render() { - return ( -
- - - -
{}
-
-
-
- ); - } -} - -CartPanel.propTypes = { - checkout: PropTypes.func, - onClick: PropTypes.func -}; - -export default CartPanel; diff --git a/imports/plugins/core/checkout/client/templates/cartPanel/container/cartPanelContainer.js b/imports/plugins/core/checkout/client/templates/cartPanel/container/cartPanelContainer.js deleted file mode 100644 index 5e4d16001b3..00000000000 --- a/imports/plugins/core/checkout/client/templates/cartPanel/container/cartPanelContainer.js +++ /dev/null @@ -1,22 +0,0 @@ -import React, { Component } from "react"; -import { $ } from "meteor/jquery"; -import { Session } from "meteor/session"; -import { Reaction } from "/client/api"; -import CartPanel from "../component/cartPanel"; - -class CartPanelContainer extends Component { - handleCheckout() { - $("#cart-drawer-container").fadeOut(); - Session.set("displayCart", false); - return Reaction.Router.go("cart/checkout"); - } - render() { - return ( - - ); - } -} - -export default CartPanelContainer; diff --git a/imports/plugins/core/checkout/client/templates/checkout/review/review.js b/imports/plugins/core/checkout/client/templates/checkout/review/review.js index fb832cececa..4de20aa386f 100644 --- a/imports/plugins/core/checkout/client/templates/checkout/review/review.js +++ b/imports/plugins/core/checkout/client/templates/checkout/review/review.js @@ -1,7 +1,7 @@ import "./review.html"; import { Meteor } from "meteor/meteor"; import { Template } from "meteor/templating"; -import CartSubTotals from "../../../container/cartSubTotalContainer"; +import CartSubTotals from "../../../containers/cartSubTotalContainer"; /** * review status diff --git a/imports/plugins/core/components/lib/components.js b/imports/plugins/core/components/lib/components.js new file mode 100644 index 00000000000..3a0b6c22b97 --- /dev/null +++ b/imports/plugins/core/components/lib/components.js @@ -0,0 +1,156 @@ +import { compose, setDisplayName } from "recompose"; + + +export const Components = {}; // populated with final wrapped components +export const ComponentsTable = {}; // storage for separate elements of each component + + +/** + * Register a component and container(s) with a name. + * The raw component can then be extended or replaced. + * + * Structure of a component in the list: + * + * ComponentsTable.MyComponent = { + * name: 'MyComponent', + * hocs: [fn1, fn2], + * rawComponent: React.Component + * } + * + * @param {String} name The name of the component to register. + * @param {React.Component} rawComponent Interchangeable/extendable component. + * @param {Function|[Function]} hocs The HOCs to wrap around the raw component. + * + * @returns {React.Component} returns the final wrapped component + */ +export function registerComponent(name, rawComponent, hocs = []) { + if (!name || !rawComponent) { + throw new Error("A name and component are required for registerComponent"); + } + + // store the component in the table + ComponentsTable[name] = { + name, + rawComponent, + hocs: Array.isArray(hocs) ? hocs : [hocs] + }; +} + + +/** + * Register containers (HOC) with a name. + * If some containers already exist for the component, they will be extended. + * @param {String} name The name of the component to register. + * @param {Function|[Function]} hocs The HOCs to wrap around the raw component. + * + * @returns {undefined} + */ +export function registerHOC(name, hocs = []) { + if (!name || !hocs) { + throw new Error("A name and HOC(s) are required for registerHOC"); + } + + const newHOCs = Array.isArray(hocs) ? hocs : [hocs]; + + const existingComponent = ComponentsTable[name]; + + // Check to see if this component has already been registered and whether it has + // HOC's to merge with our new ones. If not, just register it like a new component. + // This allows us to register HOCs _before_ registering the UI component. + // Just keep in mind that the resulting component will definitely throw an error + // if a UI component doesn't eventually get registered. + if (!!existingComponent && !!existingComponent.hocs) { + const existingHOCs = existingComponent.hocs; + + ComponentsTable[name] = { + name, + hocs: [...newHOCs, ...existingHOCs] + }; + } else { + ComponentsTable[name] = { + name, + hocs: newHOCs + }; + } +} + + +/** + * Get a component registered with registerComponent(name, component, ...hocs). + * @param {String} name The name of the component to get. + * @return {Function|React.Component} A (wrapped) React component + */ +export function getComponent(name) { + const component = ComponentsTable[name]; + + if (!component) { + throw new Error(`Component ${name} not registered.`); + } + + const hocs = component.hocs.map((hoc) => Array.isArray(hoc) ? hoc[0](hoc[1]) : hoc); + + return compose(...hocs, setDisplayName(`Reaction(${name})`))(component.rawComponent); +} + + +/** + * Replace a Reaction component with a new component and optionally add one or more higher order components. + * This function keeps track of the previous HOCs and wraps the new HOCs around previous ones + * @param {String} name The name of the component to register. + * @param {React.Component} newComponent Interchangeable/extendable component. + * @param {Function|[Function]} hocs The HOCs to compose with the raw component. + * @returns {Function|React.Component} A component callable with Components[name] + */ +export function replaceComponent(name, newComponent, hocs = []) { + const previousComponent = ComponentsTable[name]; + + if (!previousComponent) { + throw new Error(`Component '${name}' not found. Use registerComponent to create it.`); + } + + const newHocs = Array.isArray(hocs) ? hocs : [hocs]; + + return registerComponent(name, newComponent, [...newHocs, ...previousComponent.hocs]); +} + + +/** + * Get the raw UI component without any possible HOCs wrapping it. + * @param {String} name The name of the component to get. + * @returns {Function|React.Component} A React component + */ +export const getRawComponent = (name) => ComponentsTable[name].rawComponent; + + +/** + * Get the raw UI component without any possible HOCs wrapping it. + * @param {String} name The name of the component to get. + * @returns {Function|React.Component} Array of HOCs + */ +export const getHOCs = (name) => ComponentsTable[name].hocs; + + +/** + * Wrap a new component with the HOCs from a different component + * @param {String} sourceComponentName The name of the component to get the HOCs from + * @param {Function|React.Component} targetComponent Component to wrap + * @returns {Function|React.Component} A new component wrapped with the HOCs of the source component + */ +export function copyHOCs(sourceComponentName, targetComponent) { + const sourceComponent = ComponentsTable[sourceComponentName]; + return compose(...sourceComponent.hocs)(targetComponent); +} + + +/** + * Populate the final Components object with the contents of the lookup table. + * This should only be called once on app startup. + * @returns {Object} An object containing all of the registered components + **/ +export function loadRegisteredComponents() { + Object.keys(ComponentsTable).map((name) => { + Components[name] = getComponent(name); + }); + + return Components; +} diff --git a/imports/plugins/core/components/lib/composer/compose.js b/imports/plugins/core/components/lib/composer/compose.js new file mode 100644 index 00000000000..90925f765c0 --- /dev/null +++ b/imports/plugins/core/components/lib/composer/compose.js @@ -0,0 +1,143 @@ +import React from "react"; +import shallowEqual from "shallowequal"; +import hoistStatics from "hoist-non-react-statics"; +import _ from "lodash"; +import { getDisplayName } from "recompose"; + +export default function compose(dataLoader, options = {}) { + return function (Child) { + const { + errorHandler = (err) => { throw err; }, + loadingHandler = () => null, + env = {}, + pure = false, + propsToWatch = null, // Watch all the props. + shouldSubscribe = null, + shouldUpdate = null + } = options; + + class Container extends React.Component { + constructor(props, ...args) { + super(props, ...args); + this.state = {}; + this.propsCache = {}; + + this._subscribe(props); + } + + componentDidMount() { + this._mounted = true; + } + + componentWillReceiveProps(props) { + this._subscribe(props); + } + + shouldComponentUpdate(nextProps, nextState) { + if (shouldUpdate) { + return shouldUpdate(this.props, nextProps); + } + + if (!pure) { + return true; + } + + return ( + !shallowEqual(this.props, nextProps) || + this.state.error !== nextState.error || + !shallowEqual(this.state.data, nextState.data) + ); + } + + componentWillUnmount() { + this._unmounted = true; + this._unsubscribe(); + } + + _shouldSubscribe(props) { + const firstRun = !this._cachedWatchingProps; + const nextProps = _.pick(props, propsToWatch); + const currentProps = this._cachedWatchingProps || {}; + this._cachedWatchingProps = nextProps; + + if (firstRun) return true; + if (typeof shouldSubscribe === "function") { + return shouldSubscribe(currentProps, nextProps); + } + + if (propsToWatch === null) return true; + if (propsToWatch.length === 0) return false; + return !shallowEqual(currentProps, nextProps); + } + + _subscribe(props) { + if (!this._shouldSubscribe(props)) return; + + const onData = (error, data) => { + if (this._unmounted) { + throw new Error(`Trying to set data after component(${Container.displayName}) has unmounted.`); + } + + const payload = { error, data }; + + if (!this._mounted) { + this.state = { + ...this.state, + ...payload + }; + return; + } + + this.setState(payload); + }; + + // We need to do this before subscribing again. + this._unsubscribe(); + this._stop = dataLoader(props, onData, env); + } + + _unsubscribe() { + if (this._stop) { + this._stop(); + } + } + + render() { + const props = this.props; + const { data, error } = this.state; + + if (error) { + return errorHandler(error); + } + + if (!data) { + return loadingHandler(); + } + + const finalProps = { + ...props, + ...data + }; + + const setChildRef = (c) => { + this.child = c; + }; + + return ( + + ); + } + } + + Container.__composerData = { + dataLoader, + options + }; + + Container.displayName = `Tracker(${getDisplayName(Child)})`; + + hoistStatics(Container, Child); + + return Container; + }; +} diff --git a/imports/plugins/core/components/lib/composer/index.js b/imports/plugins/core/components/lib/composer/index.js new file mode 100644 index 00000000000..cb742ac787e --- /dev/null +++ b/imports/plugins/core/components/lib/composer/index.js @@ -0,0 +1,2 @@ +export { default as compose } from "./compose"; +export * from "./tracker"; diff --git a/imports/plugins/core/components/lib/composer/tracker.js b/imports/plugins/core/components/lib/composer/tracker.js new file mode 100644 index 00000000000..ba890ef8e83 --- /dev/null +++ b/imports/plugins/core/components/lib/composer/tracker.js @@ -0,0 +1,56 @@ +import React from "react"; +import { Tracker } from "meteor/tracker"; +import { Components } from "../components"; +import compose from "./compose"; + + +/** + * getTrackerLoader creates a Meteor Tracker to watch dep updates from + * the passed in reactiveMapper function + * @param {Function} reactiveMapper data fetching function to bind to a tracker + * @return {Function} composed function + */ +function getTrackerLoader(reactiveMapper) { + return (props, onData, env) => { + let trackerCleanup = null; + const handler = Tracker.nonreactive(() => { + return Tracker.autorun(() => { + // assign the custom clean-up function. + trackerCleanup = reactiveMapper(props, onData, env); + }); + }); + + return () => { + if (typeof trackerCleanup === "function") trackerCleanup(); + return handler.stop(); + }; + }; +} + + +/** + * A higher order component to wrap a reactive function with Meteor's Tracker + * @param {Function} reactiveMapper data fetching function to bind to a tracker + * @param {React.Component|Boolean|Object} options can be a custom loader, false (to disable), or a full options object + * @return {Function} composed function + */ +export function composeWithTracker(reactiveMapper, options) { + let composeOptions = {}; + + if (typeof options === "undefined") { + // eslint-disable-next-line react/display-name + composeOptions.loadingHandler = () => ; + } + + if (typeof options === "function") { + const CustomLoader = options; + // eslint-disable-next-line + composeOptions.loadingHandler = () => ; + } + + if (typeof options === "object") { + composeOptions = options; + } + + return compose(getTrackerLoader(reactiveMapper), composeOptions); +} diff --git a/imports/plugins/core/components/lib/hoc.js b/imports/plugins/core/components/lib/hoc.js new file mode 100644 index 00000000000..77161c7669e --- /dev/null +++ b/imports/plugins/core/components/lib/hoc.js @@ -0,0 +1,77 @@ +import { composeWithTracker } from "./composer"; +import { Meteor } from "meteor/meteor"; +import { Roles } from "meteor/alanning:roles"; +import { Accounts } from "/lib/collections"; + +let Reaction; + +if (Meteor.isClient) { + Reaction = require("/client/api").Reaction; +} else { + Reaction = require("/server/api").Reaction; +} + + +/** + * A wrapper to reactively inject the current user into a component + * @param {Function|React.Component} component - the component to wrap + * @return {Function} the new wrapped component with a "currentUser" prop + */ +export function withCurrentUser(component) { + return composeWithTracker((props, onData) => { + onData(null, { currentUser: Meteor.user() }); + })(component); +} + + +/** + * A wrapper to reactively inject the current account into a component. + * This assumes you have signed up and are not an anonymous user. + * @param {Function|React.Component} component - the component to wrap + * @return {Function} the new wrapped component with a "currentAccount" prop + */ +export function withCurrentAccount(component) { + return composeWithTracker((props, onData) => { + const shopId = Reaction.getShopId(); + const user = Meteor.user(); + + if (!shopId || !user) { + return null; + } + + // shoppers should always be guests + const isGuest = Roles.userIsInRole(user, "guest", shopId); + // but if a user has never logged in then they are anonymous + const isAnonymous = Roles.userIsInRole(user, "anonymous", shopId); + + const account = Accounts.findOne(user._id); + + onData(null, { currentAccount: isGuest && !isAnonymous && account }); + })(component); +} + + +/** + * A wrapper to reactively inject the current user's admin status. + * Sets a boolean 'isAdmin' prop on the wrapped component. + * @param {Function|React.Component} component - the component to wrap + * @return {Function} the new wrapped component with an "isAdmin" prop + */ +export function withIsAdmin(component) { + return composeWithTracker((props, onData) => { + onData(null, { isAdmin: Reaction.hasAdminAccess() }); + })(component); +} + + +/** + * A wrapper to reactively inject the current user's owner status. + * Sets a boolean 'isOwner' prop on the wrapped component. + * @param {Function|React.Component} component - the component to wrap + * @return {Function} the new wrapped component with an "isOwner" prop + */ +export function withIsOwner(component) { + return composeWithTracker((props, onData) => { + onData(null, { isOwner: Reaction.hasOwnerAccess() }); + })(component); +} diff --git a/imports/plugins/core/components/lib/index.js b/imports/plugins/core/components/lib/index.js new file mode 100644 index 00000000000..604b36276ad --- /dev/null +++ b/imports/plugins/core/components/lib/index.js @@ -0,0 +1,3 @@ +export { composeWithTracker } from "./composer"; +export * from "./components"; +export * from "./hoc"; diff --git a/imports/plugins/core/dashboard/client/containers/actionViewContainer.js b/imports/plugins/core/dashboard/client/containers/actionViewContainer.js index 607a31d523a..7a27cb1e188 100644 --- a/imports/plugins/core/dashboard/client/containers/actionViewContainer.js +++ b/imports/plugins/core/dashboard/client/containers/actionViewContainer.js @@ -1,10 +1,9 @@ import React from "react"; import { StyleRoot } from "radium"; import _ from "lodash"; -import { composeWithTracker } from "/lib/api/compose"; +import { composeWithTracker } from "@reactioncommerce/reaction-components"; import { Reaction } from "/client/api"; import { TranslationProvider, AdminContextProvider } from "/imports/plugins/core/ui/client/providers"; -import { Loading } from "/imports/plugins/core/ui/client/components"; function handleActionViewBack() { @@ -86,5 +85,5 @@ export default function ActionViewContainer(Comp) { ); } - return composeWithTracker(composer, Loading)(CompositeComponent); + return composeWithTracker(composer)(CompositeComponent); } diff --git a/imports/plugins/core/dashboard/client/containers/packageListContainer.js b/imports/plugins/core/dashboard/client/containers/packageListContainer.js index ffe4975484d..9fef3207dc7 100644 --- a/imports/plugins/core/dashboard/client/containers/packageListContainer.js +++ b/imports/plugins/core/dashboard/client/containers/packageListContainer.js @@ -1,10 +1,9 @@ import React from "react"; +import { composeWithTracker } from "@reactioncommerce/reaction-components"; import { Meteor } from "meteor/meteor"; import { Template } from "meteor/templating"; import { Roles } from "meteor/alanning:roles"; -import { composeWithTracker } from "/lib/api/compose"; import { Reaction } from "/client/api"; -import { Loading } from "/imports/plugins/core/ui/client/components"; import { TranslationProvider } from "/imports/plugins/core/ui/client/providers"; /** @@ -78,5 +77,5 @@ export default function PackageListContainer(Comp) { ); } - return composeWithTracker(composer, Loading)(CompositeComponent); + return composeWithTracker(composer)(CompositeComponent); } diff --git a/imports/plugins/core/dashboard/client/containers/toolbarContainer.js b/imports/plugins/core/dashboard/client/containers/toolbarContainer.js index 196b42055b2..640b755e88c 100644 --- a/imports/plugins/core/dashboard/client/containers/toolbarContainer.js +++ b/imports/plugins/core/dashboard/client/containers/toolbarContainer.js @@ -1,7 +1,7 @@ import React from "react"; import { Meteor } from "meteor/meteor"; import { Session } from "meteor/session"; -import { composeWithTracker } from "/lib/api/compose"; +import { composeWithTracker } from "@reactioncommerce/reaction-components"; import { Reaction, i18next } from "/client/api"; import { Tags } from "/lib/collections"; import { TranslationProvider, AdminContextProvider } from "/imports/plugins/core/ui/client/providers"; @@ -108,5 +108,5 @@ export default function ToolbarContainer(Comp) { ); } - return composeWithTracker(composer, null)(CompositeComponent); + return composeWithTracker(composer)(CompositeComponent); } diff --git a/imports/plugins/core/discounts/client/components/list.js b/imports/plugins/core/discounts/client/components/list.js index 5b4b830bad5..68af525e189 100644 --- a/imports/plugins/core/discounts/client/components/list.js +++ b/imports/plugins/core/discounts/client/components/list.js @@ -3,7 +3,7 @@ import PropTypes from "prop-types"; import { Meteor } from "meteor/meteor"; import { Translation, IconButton } from "/imports/plugins/core/ui/client/components"; import DiscountForm from "./form"; -import { composeWithTracker } from "/lib/api/compose"; +import { composeWithTracker } from "@reactioncommerce/reaction-components"; import { Reaction } from "/client/api"; class DiscountList extends Component { diff --git a/imports/plugins/core/email/client/actions/index.js b/imports/plugins/core/email/client/actions/index.js index b73f2e27c01..fd818eab033 100644 --- a/imports/plugins/core/email/client/actions/index.js +++ b/imports/plugins/core/email/client/actions/index.js @@ -1,2 +1 @@ -export { default as logs } from "./logs"; export { default as settings } from "./settings"; diff --git a/imports/plugins/core/email/client/actions/logs.js b/imports/plugins/core/email/client/actions/logs.js deleted file mode 100644 index 0e47f19ea54..00000000000 --- a/imports/plugins/core/email/client/actions/logs.js +++ /dev/null @@ -1,18 +0,0 @@ -import { Meteor } from "meteor/meteor"; -import { i18next } from "/client/api"; - -export default { - /** - * Restart a failed or cancelled email job - * @param {Object} email - the email job object - * @return {null} triggers an alert - */ - resend(email) { - Meteor.call("emails/retryFailed", email._id, (err) => { - if (err) { - return Alerts.toast(i18next.t("app.error", { error: err.reason }), "error"); - } - return Alerts.toast(i18next.t("mail.alerts.resendSuccess", { email: email.data.to }), "success"); - }); - } -}; diff --git a/imports/plugins/core/email/client/components/emailConfig.js b/imports/plugins/core/email/client/components/emailConfig.js index b6bb2999caf..48247ed7ede 100644 --- a/imports/plugins/core/email/client/components/emailConfig.js +++ b/imports/plugins/core/email/client/components/emailConfig.js @@ -1,8 +1,7 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; -import { Card, CardHeader, CardBody, CardGroup, Icon, Translation } from "/imports/plugins/core/ui/client/components"; -import EmailSettings from "../containers/emailSettings"; - +import { Components } from "@reactioncommerce/reaction-components"; +import { Translation } from "/imports/plugins/core/ui/client/components"; class EmailConfig extends Component { constructor(props) { @@ -40,7 +39,7 @@ class EmailConfig extends Component { return (
- +
: {status ? @@ -92,7 +91,7 @@ class EmailConfig extends Component { i18nKey={"admin.settings.editSettings"} /> - +
); } @@ -102,21 +101,19 @@ class EmailConfig extends Component { render() { return ( - - - + + - + {this.renderSettingsDisplay()} {this.renderSettingsUpdate()} - - - + + + ); } } diff --git a/imports/plugins/core/email/client/components/emailLogs.js b/imports/plugins/core/email/client/components/emailLogs.js index 0476c7ac7ac..cea2ce77811 100644 --- a/imports/plugins/core/email/client/components/emailLogs.js +++ b/imports/plugins/core/email/client/components/emailLogs.js @@ -1,7 +1,7 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; -import { Card, CardHeader, CardBody, CardGroup, Loading, SortableTable } from "/imports/plugins/core/ui/client/components"; -import EmailTableColumn from "./emailTableColumn"; +import { Components } from "@reactioncommerce/reaction-components"; +import { SortableTable } from "/imports/plugins/core/ui/client/components"; import { Jobs } from "/lib/collections"; import { i18next } from "/client/api"; @@ -35,8 +35,8 @@ class EmailLogs extends Component { const columnMeta = { accessor: field, Header: i18next.t(`admin.logs.headers.${field}`), - Cell: row => ( - + Cell: (row) => ( + ), className: colClassName, width: colWidth, @@ -56,27 +56,27 @@ class EmailLogs extends Component { filteredFields={filteredFields} noDataMessage={noDataMessage} columnMetadata={customColumnMetadata} - externalLoadingComponent={Loading} + externalLoadingComponent={Components.Loading} /> ); } render() { return ( - - + - - + {this.renderEmailsTable()} - - - + + + ); } } diff --git a/imports/plugins/core/email/client/components/emailSettings.js b/imports/plugins/core/email/client/components/emailSettings.js index 01328d280e2..ea7a8a78797 100644 --- a/imports/plugins/core/email/client/components/emailSettings.js +++ b/imports/plugins/core/email/client/components/emailSettings.js @@ -1,6 +1,6 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; -import { Button, Select, TextField } from "/imports/plugins/core/ui/client/components"; +import { Components } from "@reactioncommerce/reaction-components"; class EmailSettings extends Component { constructor(props) { @@ -48,7 +48,7 @@ class EmailSettings extends Component { return (
- + - - - - - - - - - + - {this.renderArchivedLabel()} - + - - - + + + ); } } diff --git a/imports/plugins/included/sms/client/containers/smsSettingsContainer.js b/imports/plugins/included/sms/client/containers/smsSettingsContainer.js index ec118053e0e..d2dea1d97e5 100644 --- a/imports/plugins/included/sms/client/containers/smsSettingsContainer.js +++ b/imports/plugins/included/sms/client/containers/smsSettingsContainer.js @@ -1,7 +1,6 @@ -import { composeWithTracker, merge } from "/lib/api/compose"; -import { useDeps } from "react-simple-di"; +import { compose, withProps } from "recompose"; +import { registerComponent, composeWithTracker } from "@reactioncommerce/reaction-components"; import { Meteor } from "meteor/meteor"; -import { Loading } from "/imports/plugins/core/ui/client/components"; import { Sms } from "/lib/collections"; import actions from "../actions"; import SmsSettings from "../components/smsSettings"; @@ -10,15 +9,20 @@ import SmsSettings from "../components/smsSettings"; const composer = ({}, onData) => { if (Meteor.subscribe("SmsSettings").ready()) { const settings = Sms.findOne(); - onData(null, { settings: settings }); + onData(null, { settings }); } }; -const depsMapper = () => ({ +const handlers = { saveSettings: actions.settings.saveSettings -}); +}; + +registerComponent("SmsSettings", SmsSettings, [ + composeWithTracker(composer), + withProps(handlers) +]); -export default merge( - composeWithTracker(composer, Loading), - useDeps(depsMapper) +export default compose( + composeWithTracker(composer), + withProps(handlers) )(SmsSettings); diff --git a/imports/plugins/included/sms/client/index.js b/imports/plugins/included/sms/client/index.js index dc85c609c2d..78cf0c92faa 100644 --- a/imports/plugins/included/sms/client/index.js +++ b/imports/plugins/included/sms/client/index.js @@ -1,2 +1,4 @@ import "./templates/smsSettings.html"; import "./templates/smsSettings.js"; + +export { default as SmsSettings } from "./containers/smsSettingsContainer"; diff --git a/imports/plugins/included/sms/client/templates/smsSettings.js b/imports/plugins/included/sms/client/templates/smsSettings.js index 66c1cba4b57..3119a1d8037 100644 --- a/imports/plugins/included/sms/client/templates/smsSettings.js +++ b/imports/plugins/included/sms/client/templates/smsSettings.js @@ -1,10 +1,10 @@ import { Template } from "meteor/templating"; -import SmsSettings from "../containers/smsSettingsContainer"; +import { Components } from "@reactioncommerce/reaction-components"; Template.smsSettings.helpers({ SmsSettings() { return { - component: SmsSettings + component: Components.SmsSettings }; } }); diff --git a/imports/plugins/included/social/client/containers/socialContainer.js b/imports/plugins/included/social/client/containers/socialContainer.js index 5cbd33f4917..bd077371e21 100644 --- a/imports/plugins/included/social/client/containers/socialContainer.js +++ b/imports/plugins/included/social/client/containers/socialContainer.js @@ -1,5 +1,5 @@ import React, { Component } from "react"; -import { composeWithTracker } from "/lib/api/compose"; +import { composeWithTracker } from "@reactioncommerce/reaction-components"; import { Reaction } from "/client/api"; import { SocialButtons } from "../components"; import { createSocialSettings } from "../../lib/helpers"; diff --git a/imports/plugins/included/social/client/containers/socialSettingsContainer.js b/imports/plugins/included/social/client/containers/socialSettingsContainer.js index 99a2878d136..c4a27604b88 100644 --- a/imports/plugins/included/social/client/containers/socialSettingsContainer.js +++ b/imports/plugins/included/social/client/containers/socialSettingsContainer.js @@ -1,8 +1,8 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; import { isEqual } from "lodash"; +import { composeWithTracker } from "@reactioncommerce/reaction-components"; import { Meteor } from "meteor/meteor"; -import { composeWithTracker } from "/lib/api/compose"; import { Reaction, i18next } from "/client/api"; import { Packages } from "/lib/collections"; import { SocialSettings } from "../components"; diff --git a/imports/plugins/included/ui-search/lib/containers/searchModalContainer.js b/imports/plugins/included/ui-search/lib/containers/searchModalContainer.js index 8fd5239da33..a7082090877 100644 --- a/imports/plugins/included/ui-search/lib/containers/searchModalContainer.js +++ b/imports/plugins/included/ui-search/lib/containers/searchModalContainer.js @@ -1,10 +1,10 @@ import React, { Component } from "react"; +import { composeWithTracker } from "@reactioncommerce/reaction-components"; import { Meteor } from "meteor/meteor"; import { Tracker } from "meteor/tracker"; import { Roles } from "meteor/alanning:roles"; import _ from "lodash"; import { Reaction } from "/client/api"; -import { composeWithTracker } from "/lib/api/compose"; import * as Collections from "/lib/collections"; import SearchModal from "../components/searchModal"; diff --git a/lib/api/compose.js b/lib/api/compose.js deleted file mode 100644 index cd085a33868..00000000000 --- a/lib/api/compose.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Wrapper around react-komposer v2 to provide some backwars compatability - * for features from v1. - */ -import { compose } from "react-komposer"; -import React from "react"; -export * from "react-komposer"; -import { Tracker } from "meteor/tracker"; - -/** - * getTrackerLoader creates a Meteor Tracker to watch dep updates from - * passed in reactiveMapper funtion - * @param {Function} reactiveMapper data fetching function to bind to a tracker - * @return {Function} composed function - */ -function getTrackerLoader(reactiveMapper) { - return (props, onData, env) => { - let trackerCleanup = null; - const handler = Tracker.nonreactive(() => { - return Tracker.autorun(() => { - // assign the custom clean-up function. - trackerCleanup = reactiveMapper(props, onData, env); - }); - }); - - return () => { - if (typeof trackerCleanup === "function") trackerCleanup(); - return handler.stop(); - }; - }; -} - -/** - * Re-implementation of composeWithTracker from v1.x - * @param {Function} reactiveMapper data fetching function to bind to a tracker - * @param {React.Component} LoadingComponent react component for a custom loading screen - * @return {Function} composed function - */ -export function composeWithTracker(reactiveMapper, LoadingComponent) { - const options = {}; - - if (typeof LoadingComponent !== "undefined") { - options.loadingHandler = () => { // eslint-disable-line react/display-name - return ( - - ); - }; - } - - return compose(getTrackerLoader(reactiveMapper), options); -} diff --git a/package.json b/package.json index 72438fb9189..106e5e4f7ad 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "reaction", "description": "Reaction is a modern reactive, real-time event driven ecommerce platform.", - "version": "1.4.1", + "version": "1.5.0", "main": "main.js", "directories": { "test": "tests" @@ -20,14 +20,14 @@ }, "dependencies": { "@reactioncommerce/authorize-net": "^1.0.8", - "@reactioncommerce/nodemailer": "^5.0.4", + "@reactioncommerce/nodemailer": "^5.0.5", "accounting-js": "^1.1.1", "amplify-store": "^0.1.0", - "autoprefixer": "^7.1.1", - "autosize": "^3.0.20", + "autoprefixer": "^7.1.2", + "autosize": "^4.0.0", "babel-runtime": "^6.23.0", "bootstrap": "^3.3.7", - "braintree": "^2.0.2", + "braintree": "^2.2.0", "bunyan": "^2.0.0", "bunyan-format": "^0.2.1", "classnames": "^2.2.5", @@ -35,17 +35,18 @@ "country-data": "^0.0.31", "create-react-class": "^15.6.0", "css-annotation": "^0.6.2", - "deep-diff": "^0.3.4", - "dnd-core": "^2.3.0", + "deep-diff": "^0.3.8", + "dnd-core": "^2.4.0", "faker": "^4.1.0", - "fibers": "^1.0.15", + "fibers": "^2.0.0", "flatten-obj": "^3.1.0", "font-awesome": "^4.7.0", - "handlebars": "^4.0.6", - "history": "^4.6.1", - "i18next": "8.4.2", + "handlebars": "^4.0.10", + "history": "^4.6.3", + "hoist-non-react-statics": "^2.2.0", + "i18next": "8.4.3", "i18next-browser-languagedetector": "^2.0.0", - "i18next-localstorage-cache": "^1.1.0", + "i18next-localstorage-cache": "^1.1.1", "i18next-sprintf-postprocessor": "^0.2.2", "immutable": "^3.8.1", "jquery": "^3.2.1", @@ -56,75 +57,75 @@ "match-sorter": "^1.8.0", "meteor-node-stubs": "^0.2.11", "moment": "^2.18.1", - "moment-timezone": "^0.5.11", + "moment-timezone": "^0.5.13", "nexmo": "^2.0.2", - "node-geocoder": "^3.16.0", + "node-geocoder": "^3.18.0", "node-loggly-bulk": "^2.0.0", - "nodemailer-wellknown": "^0.2.1", + "nodemailer-wellknown": "^0.2.3", "nouislider-algolia-fork": "^10.0.0", "npm-shrinkwrap": "^6.0.2", "path-to-regexp": "^1.7.0", "paypal-rest-sdk": "^1.7.1", - "postcss": "^6.0.2", + "postcss": "^6.0.8", "postcss-js": "^1.0.0", - "prerender-node": "^2.7.0", - "prop-types": "https://registry.npmjs.org/prop-types/-/prop-types-15.5.8.tgz", + "prerender-node": "^2.7.2", + "prop-types": "^15.5.10", "query-parse": "^1.0.0", - "radium": "^0.19.1", - "react": "15.4.2", - "react-addons-create-fragment": "15.4.2", - "react-addons-pure-render-mixin": "15.4.2", - "react-autosuggest": "^9.0.0", + "radium": "^0.19.4", + "react": "15.6.1", + "react-addons-create-fragment": "15.6.0", + "react-addons-pure-render-mixin": "15.6.0", + "react-autosuggest": "^9.3.1", "react-avatar": "^2.3.0", - "react-color": "^2.11.4", - "react-copy-to-clipboard": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.0.tgz", - "react-dnd": "^2.3.0", - "react-dnd-html5-backend": "^2.3.0", - "react-dom": "15.4.2", - "react-dropzone": "^3.12.3", - "react-helmet": "^5.0.1", - "react-komposer": "^2.0.0", - "react-measure": "^1.4.6", + "react-color": "^2.13.3", + "react-copy-to-clipboard": "^5.0.0", + "react-dnd": "^2.4.0", + "react-dnd-html5-backend": "^2.4.1", + "react-dom": "15.6.1", + "react-dropzone": "^3.13.3", + "react-helmet": "^5.1.3", + "react-measure": "^1.4.7", "react-nouislider": "^2.0.0", - "react-onclickoutside": "^5.10.0", - "react-router": "^4.1.1", - "react-router-dom": "^4.1.1", - "react-select": "^1.0.0-rc.3", - "react-simple-di": "^1.2.0", - "react-table": "^6.3.0", - "react-taco-table": "^0.5.0", - "react-tether": "^0.5.6", - "react-textarea-autosize": "^4.0.5", - "shippo": "^1.2.0", - "sortablejs": "^1.5.1", - "stripe": "^4.16.0", - "sweetalert2": "^6.4.4", + "react-onclickoutside": "^6.4.0", + "react-router": "^4.1.2", + "react-router-dom": "^4.1.2", + "react-select": "^1.0.0-rc.5", + "react-table": "^6.5.1", + "react-taco-table": "^0.5.1", + "react-tether": "^0.5.7", + "react-textarea-autosize": "^5.0.7", + "recompose": "^0.24.0", + "shallowequal": "^1.0.2", + "shippo": "^1.3.0", + "sortablejs": "^1.6.0", + "stripe": "^4.23.1", + "sweetalert2": "^6.6.6", "swiper": "^3.4.2", "tether-drop": "^1.4.2", "tether-tooltip": "^1.2.0", "transliteration": "github:reactioncommerce/transliteration", - "twilio": "^3.4.0", + "twilio": "^3.5.0", "url": "^0.11.0", "velocity-animate": "^1.5.0", - "velocity-react": "^1.2.1" + "velocity-react": "^1.3.3" }, "devDependencies": { - "babel-eslint": "^7.2.1", + "babel-eslint": "^7.2.3", "babel-jest": "^20.0.3", "babel-plugin-lodash": "^3.2.11", "babel-plugin-module-resolver": "^2.7.1", - "babel-preset-es2015": "^6.22.0", + "babel-preset-es2015": "^6.24.1", "babel-preset-react": "^6.24.1", - "babel-preset-stage-2": "^6.22.0", + "babel-preset-stage-2": "^6.24.1", "browserstack-local": "^1.3.0", - "chai": "^4.0.2", - "enzyme": "^2.8.2", + "chai": "^4.1.0", + "enzyme": "^2.9.1", "enzyme-to-json": "^1.5.1", - "eslint": "^4.2.0", + "eslint": "^4.3.0", "eslint-plugin-react": "^7.1.0", "jest": "^20.0.4", - "js-yaml": "^3.8.2", - "react-addons-test-utils": "15.4.2" + "js-yaml": "^3.9.0", + "react-addons-test-utils": "15.6.0" }, "postcss": { "plugins": { @@ -138,38 +139,39 @@ "scripts": { "test": "jest" }, - "jest": { - "moduleNameMapper": { - "^/lib(.*)": "/lib/$1", - "^/imports/plugins(.*)": "/imports/plugins/$1", - "^/client/api(.*)": "/imports/test-utils/__mocks__/client/api$1", - "^meteor/aldeed:simple-schema": "/imports/test-utils/__mocks__/meteor/aldeed-simple-schema", - "^meteor/tmeasday:publish-counts": "/imports/test-utils/__mocks__/meteor/aldeed-simple-schema", - "^meteor/(.*)": "/imports/test-utils/__mocks__/meteor/$1" - } - }, "babel": { "plugins": [ - "lodash", - [ - "module-resolver", - { - "root": [ - "./" - ], - "alias": { - "underscore": "lodash", - "@reactioncommerce/reaction-ui": "./imports/plugins/core/ui/client/components", - "@reactioncommerce/reaction-router": "./imports/plugins/core/router/lib", - "@reactioncommerce/reaction-collections": "./imports/plugins/core/collections" - } + [ "lodash", { + "id": [ + "lodash", + "recompose" + ] + }], + [ "module-resolver", { + "root": ["./"], + "alias": { + "@reactioncommerce/reaction-collections": "./imports/plugins/core/collections", + "@reactioncommerce/reaction-components": "/imports/plugins/core/components/lib", + "@reactioncommerce/reaction-router": "./imports/plugins/core/router/lib", + "@reactioncommerce/reaction-ui": "./imports/plugins/core/ui/client/components", + "underscore": "lodash" } - ] + }] ], "presets": [ "es2015", "react", "stage-2" ] + }, + "jest": { + "moduleNameMapper": { + "^/lib(.*)": "/lib/$1", + "^/imports/plugins(.*)": "/imports/plugins/$1", + "^/client/api(.*)": "/imports/test-utils/__mocks__/client/api$1", + "^meteor/aldeed:simple-schema": "/imports/test-utils/__mocks__/meteor/aldeed-simple-schema", + "^meteor/tmeasday:publish-counts": "/imports/test-utils/__mocks__/meteor/aldeed-simple-schema", + "^meteor/(.*)": "/imports/test-utils/__mocks__/meteor/$1" + } } }