Skip to content

Commit

Permalink
Merge pull request #3121 from reactioncommerce/folusho-convert-taxclo…
Browse files Browse the repository at this point in the history
…ud-panel-to-react-3093

Convert TaxCloud panel to React
  • Loading branch information
spencern authored Nov 20, 2017
2 parents 391b5ff + bba0eb5 commit 983fffa
Show file tree
Hide file tree
Showing 11 changed files with 285 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as TaxCloudSettingsForm } from "./taxCloudSettingsForm";
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from "react";
import PropTypes from "prop-types";
import { Form } from "/imports/plugins/core/ui/client/components";
import { Components } from "@reactioncommerce/reaction-components";
import { TaxCloudPackageConfig } from "../../lib/collections/schemas";

/**
* @file TaxCloudSettingsForm is a React Component used to change TaxCloud
* settings.
* @module TaxCloudSettingsForm
*/

/**
* @method TaxCloudSettingsForm
* @summary renders a form for updating TaxCloud settings.
* @param {Object} props - some data for use by this component.
* @property {Function} handleSubmit - a function for saving new TaxCloud settings.
* @property {Array} hiddenFields - the fields (of the TaxCloud Package) to hide from the form.
* @property {Object} settings - the value of the "settings" field in the TaxCloud Package.
* @property {Object} shownFields - info about the fields the form is to show.
* @since 1.5.2
* @return {Node} - a React node containing the TaxCloud settings form.
*/
const TaxCloudSettingsForm = (props) => {
const { handleSubmit, hiddenFields, settings, shownFields } = props;
return (
<div className="rui taxcloud-settings-form">
{!settings.taxcloud.apiLoginId &&
<div className="alert alert-info">
<Components.Translation defaultValue="Add API Login ID to enable" i18nkey="admin.taxSettings.taxcloudCredentials" />
<a href="https://www.taxcloud.com/" target="_blank">TaxCloud</a>
</div>
}
<Form
schema={TaxCloudPackageConfig}
doc={{ settings }}
docPath={"settings.taxcloud"}
name={"settings.taxcloud"}
fields={shownFields}
hideFields={hiddenFields}
onSubmit={handleSubmit}
/>
</div>
);
};

/**
* @name TaxCloudSettingsForm propTypes
* @type {propTypes}
* @param {Object} props - React PropTypes
* @property {Function} handleSubmit - a function that saves new TaxCloud settings.
* @property {Array} hiddenFields - an array of the TaxCloud Package's fields
* to hide from the settings form.
* @property {Object} settings - the value of the "settings" field in the TaxCloud Package.
* @property {Object} shownFields - info about the fields of the TaxCloud Package
* that the settings form will allow users to change.
* @return {Array} React propTypes
*/
TaxCloudSettingsForm.propTypes = {
handleSubmit: PropTypes.func,
hiddenFields: PropTypes.arrayOf(PropTypes.string),
settings: PropTypes.object,
shownFields: PropTypes.object
};

export default TaxCloudSettingsForm;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as TaxCloudSettingsFormContainer } from "./taxCloudSettingsFormContainer";
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { compose, withProps } from "recompose";
import { composeWithTracker, registerComponent } from "@reactioncommerce/reaction-components";
import { Meteor } from "meteor/meteor";
import { Reaction, i18next } from "/client/api";
import { TaxCloudPackageConfig } from "../../lib/collections/schemas";
import { TaxCloudSettingsForm } from "../components";

/**
* @file This is a container for TaxCloudSettingsForm.
* @module taxCloudSettingsFormContainer
*/

const handlers = {
/**
* handleSubmit
* @method
* @summary event handler for when new TaxCloud settings are submitted.
* @param {Object} event - event info.
* @param {Object} changedInfo - info about the new TaxCloud settings.
* @param {String} targetField - where to save the new settings in the TaxCloud Package.
* @since 1.5.2
* @return {null} - returns nothing
*/
handleSubmit(event, changedInfo, targetField) {
if (!changedInfo.isValid) {
return;
}
Meteor.call("package/update", "taxes-taxcloud", targetField, changedInfo.doc.settings.taxcloud, (error) => {
if (error) {
Alerts.toast(
i18next.t("admin.update.updateFailed", { defaultValue: "Failed to update TaxCloud settings." }),
"error"
);
return;
}
Alerts.toast(
i18next.t("admin.update.updateSucceeded", { defaultValue: "TaxCloud settings updated." }),
"success"
);
});
}
};

const composer = (props, onData) => {
const shownFields = {
["settings.taxcloud.apiKey"]: TaxCloudPackageConfig._schema["settings.taxcloud.apiKey"],
["settings.taxcloud.apiLoginId"]: TaxCloudPackageConfig._schema["settings.taxcloud.apiLoginId"]
};
const hiddenFields = [
"settings.taxcloud.enabled",
"settings.taxcloud.refreshPeriod",
"settings.taxcloud.taxCodeUrl"
];

const shopId = Reaction.getShopId();
const packageSub = Meteor.subscribe("Packages", shopId);
if (packageSub.ready()) {
const packageData = Reaction.getPackageSettings("taxes-taxcloud");
onData(null, { settings: packageData.settings, shownFields, hiddenFields });
}
};

registerComponent("TaxCloudSettingsForm", TaxCloudSettingsForm, [
withProps(handlers), composeWithTracker(composer)
]);

export default compose(withProps(handlers), composeWithTracker(composer))(TaxCloudSettingsForm);
Original file line number Diff line number Diff line change
@@ -1,21 +1,5 @@
<template name="taxCloudSettings">

{{#unless packageData.settings.taxcloud.apiLoginId}}
<div class="alert alert-info">
<span data-i18n="admin.taxSettings.taxcloudCredentials">Add API Login ID to enable</span>
<a href="https://www.taxcloud.com/" target="_blank">TaxCloud</a>
</div>
{{/unless}}

<div>
{{#autoForm collection=Collections.Packages schema=packageConfigSchema doc=packageData type="update" id="taxcloud-update-form"}}
<div class="panel-body">
{{> afQuickField name='settings.taxcloud.apiLoginId' class='form-control'}}
{{> afQuickField name='settings.taxcloud.apiKey' class='form-control'}}
<!-- {{> afQuickField name='settings.taxcloud.refreshPeriod' class='form-control'}}
{{> afQuickField name='settings.taxcloud.taxCodeUrl' class='form-control'}} -->
</div>
{{> shopSettingsSubmitButton}}
{{/autoForm}}
{{> React taxCloudForm}}
</div>
</template>
52 changes: 10 additions & 42 deletions imports/plugins/included/taxes-taxcloud/client/settings/taxcloud.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,15 @@
import { Meteor } from "meteor/meteor";
import { Template } from "meteor/templating";
import { AutoForm } from "meteor/aldeed:autoform";
import { Packages } from "/lib/collections";
import { TaxCodes } from "/imports/plugins/core/taxes/lib/collections";
import { Reaction, i18next } from "/client/api";
import { TaxCloudPackageConfig } from "../../lib/collections/schemas";
import { TaxCloudSettingsFormContainer } from "../containers";

Template.taxCloudSettings.helpers({
packageConfigSchema() {
return TaxCloudPackageConfig;
},
packageData() {
return Packages.findOne({
name: "taxes-taxcloud",
shopId: Reaction.getShopId()
});
}
});


AutoForm.hooks({
"taxcloud-update-form": {
onSuccess: function () {
if (!TaxCodes.findOne({ taxCodeProvider: "taxes-taxcloud" })) {
Meteor.call("taxcloud/getTaxCodes", (err, res) => {
if (res && Array.isArray(res)) {
Alerts.toast(i18next.t("admin.taxSettings.shopTaxMethodsSaved"),
"success");
res.forEach((code) => {
Meteor.call("taxes/insertTaxCodes", Reaction.getShopId(), code,
"taxes-taxcloud");
});
}
});
} else {
Alerts.toast(i18next.t("admin.taxSettings.shopTaxMethodsAlreadySaved"),
"success");
}
},
onError: function (operation, error) {
return Alerts.toast(
`${i18next.t("admin.taxSettings.shopTaxMethodsFailed")} ${error}`,
"error");
}
/**
* @method taxCloudForm
* @summary returns a component for updating the TaxCloud settings for
* this app.
* @since 1.5.2
* @return {Object} - an object containing the component to render.
*/
taxCloudForm() {
return { component: TaxCloudSettingsFormContainer };
}
});
4 changes: 4 additions & 0 deletions imports/plugins/included/taxes-taxcloud/server/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
"taxcloudSettingsLabel": "TaxCloud",
"taxcloudCredentials": "Add credentials to enable",
"taxcloudGetCredentialsURL": "Get them here"
},
"update": {
"updateSucceeded": "TaxCloud settings updated.",
"updateFailed": "Failed to update TaxCloud settings."
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion server/imports/fixtures/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import accounts from "./accounts";
import cart from "./cart";
import orders from "./orders";
import products from "./products";
import examplePaymentMethod from "./packages";
import { examplePaymentMethod, examplePackage } from "./packages";
// import shipping from "./shipping";
import shops from "./shops";
import users from "./users";
Expand All @@ -12,6 +12,7 @@ export default function () {
shops();
users();
examplePaymentMethod();
examplePackage();
accounts();
products();
cart();
Expand Down
20 changes: 19 additions & 1 deletion server/imports/fixtures/packages.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const getPkgData = (pkgName) => {
};


export default function () {
export function examplePaymentMethod() {
const examplePaymentMethodPackage = {
name: "example-paymentmethod",
icon: "fa fa-credit-card-alt",
Expand All @@ -34,3 +34,21 @@ export default function () {
Factory.define("examplePaymentPackage", Packages, Object.assign({}, examplePaymentMethodPackage));
}

/**
* @method examplePackage
* @summary creates a new fixture based off of the Packages collection.
* @since 1.5.5
* @return {undefined} - returns nothing.
*/
export function examplePackage() {
const examplePkg = {
name: "example-package",
settings: {
enabled: false,
apiUrl: "http://example.com/api"
},
shopId: "random-shop-101"
};

Factory.define("examplePackage", Packages, examplePkg);
}
70 changes: 70 additions & 0 deletions server/methods/core/packages-update.app-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Meteor } from "meteor/meteor";
import { Match } from "meteor/check";
import { Factory } from "meteor/dburles:factory";
import { expect } from "meteor/practicalmeteor:chai";
import { sinon } from "meteor/practicalmeteor:sinon";
import { Packages } from "/lib/collections";
import { Reaction } from "/server/api";

describe("Update Package", function () {
let sandbox;

beforeEach(function () {
sandbox = sinon.sandbox.create();
});

afterEach(function () {
sandbox.restore();
});

describe("package/update", function () {
it("should throw an 'Access Denied' error for non-admins", function (done) {
const pkgUpdateSpy = sandbox.spy(Packages, "update");
const examplePackage = Factory.create("examplePackage");

function updatePackage() {
return Meteor.call("package/update", examplePackage.name, "settings", {});
}
expect(updatePackage).to.throw(Meteor.Error, /Access Denied/);
expect(pkgUpdateSpy).to.not.have.been.called;

return done();
});

it("should throw an error when supplied with an argument of the wrong type", function (done) {
const pkgUpdateSpy = sandbox.spy(Packages, "update");
sandbox.stub(Reaction, "getShopId", () => "randomId");
sandbox.stub(Reaction, "hasPermission", () => true);

function updatePackage(packageName, field, value) {
return Meteor.call("package/update", packageName, field, value);
}
expect(() => updatePackage([], "someField", { foo: "bar" })).to.throw(Match.Error, /Match error: Expected string, got object/);
expect(() => updatePackage("somePackage", [], { foo: "bar" })).to.throw(Match.Error, /Match error: Expected string, got object/);
expect(() => updatePackage("somePackage", "someField", "")).to.throw(Match.Error, /Match error: Expected object, got string/);
expect(pkgUpdateSpy).to.not.have.been.called;

return done();
});

it("should be able to update any Package", function (done) {
const packageUpdateSpy = sandbox.spy(Packages, "update");
const oldPackage = Factory.create("examplePackage");

sandbox.stub(Reaction, "getShopId", () => oldPackage.shopId);
sandbox.stub(Reaction, "hasPermission", () => true);
const packageName = oldPackage.name;
const newValues = {
enabled: true,
apiUrl: "http://foo-bar.com/api/v1"
};
Meteor.call("package/update", packageName, "settings", newValues);
expect(packageUpdateSpy).to.have.been.called;
const updatedPackage = Packages.findOne({ name: packageName });
expect(oldPackage.settings.enabled).to.not.equal(updatedPackage.settings.enabled);
expect(oldPackage.settings.apiUrl).to.not.equal(updatedPackage.settings.apiUrl);

return done();
});
});
});
44 changes: 44 additions & 0 deletions server/methods/core/packages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Meteor } from "meteor/meteor";
import { check } from "meteor/check";
import { Packages } from "/lib/collections";
import { Reaction } from "/server/api";

/**
* @method updatePackage
* @summary updates the data stored for a certain Package.
* @param {String} packageName - the name of the Package to update.
* @param {String} field - the part of the Package's data that is to
* be updated.
* @param {Object} value - the new data that's to be stored for the said
* Package.
* @since 1.5.1
* @return {Object} - returns an object with info about the update operation.
*/
export function updatePackage(packageName, field, value) {
check(packageName, String);
check(field, String);
check(value, Object);

const userId = Meteor.userId();
const shopId = Reaction.getShopId();
if (!Reaction.hasPermission([packageName], userId, shopId)) {
throw new Meteor.Error("access-denied", `Access Denied. You don't have permissions for the ${packageName} package.`);
}

const updateResult = Packages.update({
name: packageName,
shopId: shopId
}, {
$set: {
[field]: value
}
});
if (updateResult !== 1) {
throw new Meteor.Error("server-error", `An error occurred while updating the package ${packageName}.`);
}
return updateResult;
}

Meteor.methods({
"package/update": updatePackage
});

0 comments on commit 983fffa

Please sign in to comment.