diff --git a/client/modules/core/main.js b/client/modules/core/main.js index 3c9c4c8eafd..31f384e7370 100644 --- a/client/modules/core/main.js +++ b/client/modules/core/main.js @@ -215,6 +215,14 @@ export default { return false; }, + updateUserPreferences(packageName, preference, values) { + const currentPreference = this.getUserPreferences(packageName, preference, {}); + return this.setUserPreferences(packageName, preference, { + ...currentPreference, + ...values + }); + }, + getShopId() { return this.shopId; }, diff --git a/client/modules/router/main.js b/client/modules/router/main.js index 8ea4eaff9d4..e011180a959 100644 --- a/client/modules/router/main.js +++ b/client/modules/router/main.js @@ -192,14 +192,13 @@ Router.initPackageRoutes = () => { // registryItems if (registryItem.route) { const { + meta, route, template, layout, workflow } = registryItem; - // console.log(registryItem); - // get registry route name const name = getRegistryRouteName(pkg.name, registryItem); @@ -208,6 +207,7 @@ Router.initPackageRoutes = () => { const newRouteConfig = { route, options: { + meta, name, template, layout, @@ -218,6 +218,7 @@ Router.initPackageRoutes = () => { } } }; + // push new routes newRoutes.push(newRouteConfig); } // end registryItems diff --git a/imports/plugins/core/ui/client/components/cards/card.js b/imports/plugins/core/ui/client/components/cards/card.js index 77029ef13a3..d39fef1f35c 100644 --- a/imports/plugins/core/ui/client/components/cards/card.js +++ b/imports/plugins/core/ui/client/components/cards/card.js @@ -20,11 +20,11 @@ class Card extends Component { handleExpanderClick = (event) => { this.setState({ expanded: !this.state.expanded + }, () => { + if (typeof this.props.onExpand === "function") { + this.props.onExpand(event, this, this.props.name, this.state.expanded); + } }); - - if (typeof this.props.onExpand === "function") { - this.props.onExpand(event, this); - } } render() { @@ -67,6 +67,7 @@ Card.propTypes = { children: PropTypes.node, expandable: PropTypes.bool, expanded: PropTypes.bool, + name: PropTypes.string, onExpand: PropTypes.func, style: PropTypes.object }; diff --git a/imports/plugins/core/ui/client/components/cards/cardHeader.js b/imports/plugins/core/ui/client/components/cards/cardHeader.js index 9323972c798..adc89fd1a81 100644 --- a/imports/plugins/core/ui/client/components/cards/cardHeader.js +++ b/imports/plugins/core/ui/client/components/cards/cardHeader.js @@ -2,6 +2,7 @@ import React, { Component, PropTypes } from "react"; import classnames from "classnames"; import CardTitle from "./cardTitle"; import IconButton from "../button/iconButton"; +import Icon from "../icon/icon"; import Switch from "../switch/switch"; class CardHeader extends Component { @@ -16,10 +17,13 @@ class CardHeader extends Component { expandOnSwitchOn: PropTypes.bool, expanded: PropTypes.bool, i18nKeyTitle: PropTypes.string, + icon: PropTypes.string, + imageView: PropTypes.node, onClick: PropTypes.func, onSwitchChange: PropTypes.func, showSwitch: PropTypes.bool, - switchChecked: PropTypes.bool, + switchName: PropTypes.string, + switchOn: PropTypes.bool, title: PropTypes.string }; @@ -53,6 +57,26 @@ class CardHeader extends Component { return null; } + renderImage() { + if (this.props.icon) { + return ( +
+ +
+ ); + } + + if (this.props.imageView) { + return ( +
+ {this.props.imageView} +
+ ); + } + + return null; + } + renderDisclsoureArrow() { const expanderClassName = classnames({ rui: true, @@ -76,7 +100,8 @@ class CardHeader extends Component { if (this.props.showSwitch) { return ( ); @@ -97,6 +122,7 @@ class CardHeader extends Component { return (
+ {this.renderImage()} {this.renderTitle()}
@@ -109,8 +135,12 @@ class CardHeader extends Component { return (
- {this.renderTitle()} - {this.props.children} +
+ {this.renderTitle()} +
+
+ {this.props.children} +
); } diff --git a/imports/plugins/core/ui/client/components/cards/index.js b/imports/plugins/core/ui/client/components/cards/index.js index 2f7708ed970..4e0220909be 100644 --- a/imports/plugins/core/ui/client/components/cards/index.js +++ b/imports/plugins/core/ui/client/components/cards/index.js @@ -3,3 +3,4 @@ export { default as CardHeader } from "./cardHeader"; export { default as CardTitle } from "./cardTitle"; export { default as CardBody } from "./cardBody"; export { default as CardGroup } from "./cardGroup"; +export { default as SettingsCard } from "./settingsCard"; diff --git a/imports/plugins/core/ui/client/components/cards/settingsCard.js b/imports/plugins/core/ui/client/components/cards/settingsCard.js new file mode 100644 index 00000000000..2e41371f5f6 --- /dev/null +++ b/imports/plugins/core/ui/client/components/cards/settingsCard.js @@ -0,0 +1,67 @@ +/** + * Settings Card is a composite component to standardize the + * creation settings cards (panels) in the dashboard. + */ + +import React, { Component, PropTypes } from "react"; +import Blaze from "meteor/gadicc:blaze-react-component"; +import { Card, CardHeader, CardBody } from "/imports/plugins/core/ui/client/components"; + +class SettingsCard extends Component { + static propTypes = { + children: PropTypes.node, + enabled: PropTypes.bool, + expanded: PropTypes.bool, + i18nKeyTitle: PropTypes.string, + icon: PropTypes.string, + name: PropTypes.string, + onExpand: PropTypes.func, + onSwitchChange: PropTypes.func, + template: PropTypes.any, + title: PropTypes.string + } + + handleSwitchChange = (event, isChecked) => { + if (typeof this.props.onSwitchChange === "function") { + this.props.onSwitchChange(event, isChecked, this.props.name, this); + } + } + + renderCardBody() { + if (this.props.template) { + return ( + + ); + } + + return this.props.children; + } + + render() { + return ( + + + + {this.renderCardBody()} + + + ); + } +} + +export default SettingsCard; diff --git a/imports/plugins/core/ui/client/components/forms/form.js b/imports/plugins/core/ui/client/components/forms/form.js new file mode 100644 index 00000000000..9e2b4ab00dd --- /dev/null +++ b/imports/plugins/core/ui/client/components/forms/form.js @@ -0,0 +1,255 @@ +import React, { Component, PropTypes } from "react"; +import { map, update, set, at, isEqual } from "lodash"; +import classnames from "classnames"; +import { toCamelCase } from "/lib/api"; +import { Switch, Button, TextField, FormActions } from "../"; + +class Form extends Component { + static propTypes = { + doc: PropTypes.object, + docPath: PropTypes.string, + hideFields: PropTypes.arrayOf(PropTypes.string), + name: PropTypes.string, + onSubmit: PropTypes.func, + schema: PropTypes.object + } + + constructor(props) { + super(props); + + this.state = { + doc: props.doc, + schema: this.validationSchema(), + isValid: undefined + }; + } + + componentWillReceiveProps(nextProps) { + if (isEqual(nextProps.doc, this.props.doc) === false) { + this.setState({ + doc: nextProps.doc, + schema: this.validationSchema() + }); + } + } + + + validationSchema() { + const { docPath } = this.props; + + if (docPath) { + const objectKeys = this.objectKeys[docPath + "."]; + if (Array.isArray(objectKeys)) { + // Use the objectKeys from parent fieldset to generate + // actual form fields + const fieldNames = objectKeys.map((fieldName) => { + return `${docPath}.${fieldName}`; + }); + + return this.props.schema.pick(fieldNames).newContext(); + } + } + + return this.props.schema.namedContext(); + } + + get objectKeys() { + return this.props.schema._objectKeys; + } + + get schema() { + return this.props.schema._schema; + } + + valueForField(fieldName) { + const picked = at(this.state.doc, fieldName); + + if (Array.isArray(picked) && picked.length) { + return picked[0]; + } + + return undefined; + } + + validate() { + const { docPath } = this.props; + + // Create a smaller document in order to validate without extra fields + const docToValidate = set( + {}, + docPath, + at(this.state.doc, this.props.docPath)[0] + ); + + // Clean any fields not in schame to avoid needless validation errors + const cleanedObject = this.state.schema._simpleSchema.clean(docToValidate); + + // Finally validate the document + this.setState({ + isValid: this.state.schema.validate(cleanedObject) + }); + } + + isFieldHidden(fieldName) { + if (Array.isArray(this.props.hideFields) && this.props.hideFields.indexOf(fieldName) >= 0) { + return true; + } + + return false; + } + + handleChange = (event, value, name) => { + const newdoc = update(this.state.doc, name, () => { + return value; + }); + + this.setState({ + doc: newdoc + }, () => { + this.validate(); + }); + } + + handleSubmit = (event) => { + event.preventDefault(); + + this.validate(); + + if (this.props.onSubmit) { + this.props.onSubmit(event, { + doc: this.state.doc, + isValid: this.state.isValid + }, this.props.name); + } + } + + renderFormField(field) { + const sharedProps = { + i18nKeyLabel: `settings.${toCamelCase(field.name)}`, + key: field.name, + label: field.label, + name: field.name + }; + + let fieldElement; + let helpText; + + switch (field.type) { + case "boolean": + fieldElement = ( + + ); + break; + case "string": + fieldElement = ( + + ); + break; + default: + return null; + } + + let fieldHasError = false; + + if (this.state.isValid === false) { + this.state.schema._invalidKeys + .filter((v) => v.name === field.name) + .map((validationError) => { + const message = this.state.schema.keyErrorMessage(validationError.name); + fieldHasError = true; + + helpText = ( +
+ {message} +
+ ); + }); + } + + const formGroupClassName = classnames({ + "rui": true, + "form-group": true, + "has-error": fieldHasError + }); + + return ( +
+ {fieldElement} + {helpText} +
+ ); + } + + renderField(field) { + const { fieldName } = field; + + if (this.isFieldHidden(fieldName) === false) { + const fieldSchema = this.schema[fieldName]; + const fieldProps = { + ...fieldSchema, + name: fieldName, + type: typeof fieldSchema.type() + }; + + return this.renderFormField(fieldProps); + } + + return null; + } + + renderWithSchema() { + const { docPath } = this.props; + + if (this.props.schema) { + if (docPath) { + return map(this.schema, (field, key) => { // eslint-disable-line consistent-return + if (key.endsWith(docPath)) { + const objectKeys = this.objectKeys[docPath + "."]; + if (Array.isArray(objectKeys)) { + // Use the objectKeys from parent fieldset to generate + // actual form fields + return objectKeys.map((fieldName) => { + const fullFieldName = docPath ? `${docPath}.${fieldName}` : fieldName; + return this.renderField({ fieldName: fullFieldName }); + }); + } + + return this.renderField({ fieldName: key }); + } + }); + } + + return map(this.schema, (field, key) => { // eslint-disable-line consistent-return + return this.renderField({ fieldName: key }); + }); + } + + return null; + } + + render() { + return ( +
+ {this.renderWithSchema()} + + + + +
+ ); + } +} + +ExampleSettingsForm.propTypes = { + onChange: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + packageData: PropTypes.object +}; + +export default ExampleSettingsForm; + diff --git a/imports/plugins/included/payments-example/client/settings/components/index.js b/imports/plugins/included/payments-example/client/settings/components/index.js new file mode 100644 index 00000000000..b49222b0c7f --- /dev/null +++ b/imports/plugins/included/payments-example/client/settings/components/index.js @@ -0,0 +1 @@ +export { default as ExampleSettingsForm } from "./exampleSettingsForm.js"; diff --git a/imports/plugins/included/payments-example/client/settings/containers/exampleSettingsFormContainer.js b/imports/plugins/included/payments-example/client/settings/containers/exampleSettingsFormContainer.js new file mode 100644 index 00000000000..5da59806e94 --- /dev/null +++ b/imports/plugins/included/payments-example/client/settings/containers/exampleSettingsFormContainer.js @@ -0,0 +1,80 @@ +import React, { Component, PropTypes } from "react"; +import { Meteor } from "meteor/meteor"; +import { composeWithTracker } from "/lib/api/compose"; +import { Packages } from "/lib/collections"; +import { Loading } from "/imports/plugins/core/ui/client/components"; +import { TranslationProvider } from "/imports/plugins/core/ui/client/providers"; +import { Reaction, i18next } from "/client/api"; +import { ExampleSettingsForm } from "../components"; + +class ExampleSettingsFormContainer extends Component { + constructor(props) { + super(props); + + this.state = { + apiKey: "" + }; + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.saveUpdate = this.saveUpdate.bind(this); + } + + handleChange(e) { + e.preventDefault(); + this.setState({ apiKey: e.target.value }); + } + + handleSubmit(e) { + e.preventDefault(); + + const packageId = this.props.packageData._id; + const settingsKey = this.props.packageData.registry[0].settingsKey; + const apiKey = this.state.apiKey; + + const fields = [{ + property: "apiKey", + value: apiKey + }]; + + this.saveUpdate(fields, packageId, settingsKey); + } + + saveUpdate(fields, id, settingsKey) { + Meteor.call("registry/update", id, settingsKey, fields, (err) => { + if (err) { + return Alerts.toast(i18next.t("admin.settings.saveFailed"), "error"); + } + return Alerts.toast(i18next.t("admin.settings.saveSuccess"), "success"); + }); + } + + render() { + return ( + + + + ); + } +} + +ExampleSettingsFormContainer.propTypes = { + packageData: PropTypes.object +}; + +const composer = ({}, onData) => { + const subscription = Meteor.subscribe("Packages"); + if (subscription.ready()) { + const packageData = Packages.findOne({ + name: "example-paymentmethod", + shopId: Reaction.getShopId() + }); + onData(null, { packageData }); + } +}; + +export default composeWithTracker(composer, Loading)(ExampleSettingsFormContainer); diff --git a/imports/plugins/included/payments-example/client/settings/containers/index.js b/imports/plugins/included/payments-example/client/settings/containers/index.js new file mode 100644 index 00000000000..a04ab5d1421 --- /dev/null +++ b/imports/plugins/included/payments-example/client/settings/containers/index.js @@ -0,0 +1 @@ +export { default as ExampleSettingsFormContainer } from "./exampleSettingsFormContainer"; diff --git a/imports/plugins/included/payments-example/client/settings/example.html b/imports/plugins/included/payments-example/client/settings/example.html deleted file mode 100644 index 7b123e41d47..00000000000 --- a/imports/plugins/included/payments-example/client/settings/example.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - diff --git a/imports/plugins/included/payments-example/client/settings/example.js b/imports/plugins/included/payments-example/client/settings/example.js deleted file mode 100644 index ce9cdd7ba4f..00000000000 --- a/imports/plugins/included/payments-example/client/settings/example.js +++ /dev/null @@ -1,46 +0,0 @@ -import { Template } from "meteor/templating"; -import { Reaction, i18next } from "/client/api"; -import { Packages } from "/lib/collections"; -import { ExamplePackageConfig } from "../../lib/collections/schemas"; - -import "./example.html"; - - -Template.exampleSettings.helpers({ - ExamplePackageConfig() { - return ExamplePackageConfig; - }, - packageData() { - return Packages.findOne({ - name: "example-paymentmethod", - shopId: Reaction.getShopId() - }); - } -}); - - -Template.example.helpers({ - packageData: function () { - return Packages.findOne({ - name: "example-paymentmethod", - shopId: Reaction.getShopId() - }); - } -}); - -Template.example.events({ - "click [data-event-action=showExampleSettings]": function () { - Reaction.showActionView(); - } -}); - -AutoForm.hooks({ - "example-update-form": { - onSuccess: function () { - return Alerts.toast(i18next.t("admin.settings.saveSuccess"), "success"); - }, - onError: function () { - return Alerts.toast(`${i18next.t("admin.settings.saveFailed")} ${error}`, "error"); - } - } -}); diff --git a/imports/plugins/included/payments-example/client/settings/templates/example.html b/imports/plugins/included/payments-example/client/settings/templates/example.html new file mode 100644 index 00000000000..f70d1753254 --- /dev/null +++ b/imports/plugins/included/payments-example/client/settings/templates/example.html @@ -0,0 +1,5 @@ + diff --git a/imports/plugins/included/payments-example/client/settings/templates/example.js b/imports/plugins/included/payments-example/client/settings/templates/example.js new file mode 100644 index 00000000000..f0793d9c9f1 --- /dev/null +++ b/imports/plugins/included/payments-example/client/settings/templates/example.js @@ -0,0 +1,11 @@ +import { ExampleSettingsFormContainer } from "../containers"; +import { Template } from "meteor/templating"; +import "./example.html"; + +Template.exampleSettings.helpers({ + ExampleSettings() { + return { + component: ExampleSettingsFormContainer + }; + } +}); diff --git a/imports/plugins/included/social/client/components/index.js b/imports/plugins/included/social/client/components/index.js index 228114006ab..6eef7e64ad6 100644 --- a/imports/plugins/included/social/client/components/index.js +++ b/imports/plugins/included/social/client/components/index.js @@ -3,3 +3,4 @@ export { default as Facebook } from "./facebook"; export { default as Twitter } from "./twitter"; export { default as GooglePlus } from "./googleplus"; export { default as Pinterest } from "./pinterest"; +export { default as SocialSettings } from "./settings"; diff --git a/imports/plugins/included/social/client/components/settings.js b/imports/plugins/included/social/client/components/settings.js new file mode 100644 index 00000000000..37c106212fe --- /dev/null +++ b/imports/plugins/included/social/client/components/settings.js @@ -0,0 +1,110 @@ +import React, { Component, PropTypes } from "react"; +import { + CardGroup, + SettingsCard, + Form +} from "/imports/plugins/core/ui/client/components"; +import { SocialPackageConfig } from "/lib/collections/schemas/social"; + +const socialProviders = [ + { + name: "facebook", + icon: "fa fa-facebook", + fields: ["appId", "appSecret", "profilePage"] + }, + { + name: "twitter", + icon: "fa fa-twitter", + fields: ["username", "profilePage"] + }, + { + name: "pinterest", + icon: "fa fa-pinterest", + fields: ["profilePage"] + }, + { + name: "googleplus", + icon: "fa fa-google-plus", + fields: ["profilePage"] + } +]; + +class SocialSettings extends Component { + static propTypes = { + onSettingChange: PropTypes.func, + onSettingEnableChange: PropTypes.func, + onSettingExpand: PropTypes.func, + onSettingsSave: PropTypes.func, + packageData: PropTypes.object, + preferences: PropTypes.object, + providers: PropTypes.arrayOf(PropTypes.string), + socialSettings: PropTypes.object + } + + getSchemaForField(provider, field) { + return SocialPackageConfig._schema[`settings.public.apps.${provider}.${field}`]; + } + + handleSettingChange = (event, value, name) => { + if (typeof this.props.onSettingChange === "function") { + const parts = name.split("."); + this.props.onSettingChange(parts[0], parts[1], value); + } + } + + handleSubmit = (event, data, formName) => { + if (typeof this.props.onSettingsSave === "function") { + this.props.onSettingsSave(formName, data.doc); + } + } + + renderCards() { + if (Array.isArray(socialProviders)) { + return socialProviders.map((provider, index) => { + const doc = { + settings: { + ...this.props.packageData.settings + } + }; + + return ( + +
+ + ); + }); + } + + return null; + } + + render() { + return ( + + {this.renderCards()} + + ); + } +} + +export default SocialSettings; diff --git a/imports/plugins/included/social/client/containers/socialContainer.js b/imports/plugins/included/social/client/containers/socialContainer.js index 17e0856449e..5cbd33f4917 100644 --- a/imports/plugins/included/social/client/containers/socialContainer.js +++ b/imports/plugins/included/social/client/containers/socialContainer.js @@ -2,8 +2,7 @@ import React, { Component } from "react"; import { composeWithTracker } from "/lib/api/compose"; import { Reaction } from "/client/api"; import { SocialButtons } from "../components"; -import { createSocialSettings } from "../lib/helpers"; - +import { createSocialSettings } from "../../lib/helpers"; class SocialContainer extends Component { render() { diff --git a/imports/plugins/included/social/client/containers/socialSettingsContainer.js b/imports/plugins/included/social/client/containers/socialSettingsContainer.js new file mode 100644 index 00000000000..de2439ac5cb --- /dev/null +++ b/imports/plugins/included/social/client/containers/socialSettingsContainer.js @@ -0,0 +1,86 @@ +import React, { Component, PropTypes } from "react"; +import { isEqual } from "lodash"; +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"; +import { createSocialSettings } from "../../lib/helpers"; + +class SocialSettingsContainer extends Component { + static propTypes = { + settings: PropTypes.object + } + + constructor(props) { + super(props); + + this.state = { + settings: props.settings + }; + } + + componentWillReceiveProps(nextProps) { + if (isEqual(nextProps.settings, this.props.settings) === false) { + this.setState({ + settings: nextProps.settings + }); + } + } + + handleSettingEnable = (event, isChecked, name) => { + Meteor.call("reaction-social/updateSocialSetting", name, "enabled", isChecked); + } + + handleSettingExpand = (event, card, name, isExpanded) => { + Reaction.updateUserPreferences("reaction-social", "settingsCards", { + [name]: isExpanded + }); + } + + handleSettingsSave = (settingName, values) => { + Meteor.call("reaction-social/updateSocialSettings", values.settings, (error) => { + if (!error) { + Alerts.toast( + i18next.t("admin.settings.socialSettingsSaved", { defaultValue: "Social settings saved" }), + "success" + ); + } + }); + } + + render() { + return ( + + ); + } +} + +function composer(props, onData) { + const subscription = Reaction.Subscriptions.Packages; + const preferences = Reaction.getUserPreferences("reaction-social", "settingsCards", {}); + + const socialPackage = Packages.findOne({ + name: "reaction-social" + }); + + if (subscription.ready()) { + onData(null, { + preferences: preferences, + packageData: socialPackage, + socialSettings: createSocialSettings(props) + }); + } else { + onData(null, {}); + } +} + +const decoratedComponent = composeWithTracker(composer)(SocialSettingsContainer); + +export default decoratedComponent; diff --git a/imports/plugins/included/social/client/templates/dashboard/social.html b/imports/plugins/included/social/client/templates/dashboard/social.html index 82e393300e2..3516fde3fea 100644 --- a/imports/plugins/included/social/client/templates/dashboard/social.html +++ b/imports/plugins/included/social/client/templates/dashboard/social.html @@ -1,73 +1,7 @@