Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert TaxCloud panel to React #3121

Merged
merged 29 commits into from
Nov 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2a0e841
create component for changing taxcloud settings
foladipo Oct 12, 2017
25e8e71
replace autoform with react component for updating taxcloud settings
foladipo Oct 12, 2017
3ac4445
improve the name of a certain template helper
foladipo Oct 13, 2017
62b4e8a
add jsdocs
foladipo Oct 13, 2017
7e207c1
create a Meteor method for updating the data for a certain Package
foladipo Oct 17, 2017
ad232c2
- enable the new TaxCloud settings component to update DB values
foladipo Oct 17, 2017
f896862
add jsdocs
foladipo Oct 18, 2017
95022d8
remove obsolete autoform code
foladipo Oct 18, 2017
e07e2d0
add a missing return statement
foladipo Oct 18, 2017
5771d1a
code linting
foladipo Oct 18, 2017
f765915
improve the handling errors from trying to update the TaxCloud package
foladipo Oct 18, 2017
6de1358
improve error handling
foladipo Oct 19, 2017
336a7f0
add jsdocs for a component's proptypes and its source file's module
foladipo Oct 22, 2017
bbda8bf
replace some functions with Reaction.getPackageSettings()
foladipo Oct 22, 2017
cf4e711
create TaxCloudSettingsFormContainer
foladipo Oct 23, 2017
a75cdb6
replace some code with the use of TaxCloudSettingsFormContainer
foladipo Oct 23, 2017
cb310f4
add/update jsdocs
foladipo Oct 23, 2017
1cd18ee
improve jsdoc
foladipo Oct 24, 2017
10a12e3
add missing prop
foladipo Oct 24, 2017
31de683
correct i18n values
foladipo Oct 24, 2017
820a58a
use Reaction.getPackageSettings() instead of Packages.findOne()
foladipo Oct 24, 2017
31a313f
remove JSX code from TaxCloudSettingsFormContainer thus limiting its …
foladipo Oct 26, 2017
839f32c
improve the names of some methods
foladipo Oct 26, 2017
863139e
remove the use of a variable that never varies
foladipo Oct 26, 2017
9617a46
add a class that makes it easier to identify TaxCloudSettingsForm
foladipo Oct 26, 2017
8b218fe
write tests for the package/update Meteor method
foladipo Nov 1, 2017
a8aa5c7
add tests for when package/update is called with arguments of the wro…
foladipo Nov 7, 2017
ab24292
Remove only from tests so all tests run as normal
brent-hoover Nov 8, 2017
bba0eb5
Remove data-i18n and replace with Components.Translation
brent-hoover Nov 8, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Server methods require a test

* @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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for properly documenting this method :)

It looks really similar to this method registry/update:

* @name registry/update

In our new docs: http://api.docs.reactioncommerce.com/Methods_Registry.html

Could you use that method instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take a look at using that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@machikoyasuda I have considered using the method you mentioned, but I don't think it's appropriate to use it here because how it works (e.g Collection.upsert, name.split("/").splice(-1)) is different from how package/update works (Collection.update). Also, in summary, I'd have to change too many things that are beyond the scope of this ticket. In detail:

  • I am using the Form component created by @mikemurray (link). That component expects a handleSubmit function in its props, which it calls with the arguments event, changedInfo, targetField. However, if that handleSubmit calls registry/update, it won't be able to supply the parameters expected by registry/update.
  • I could try to change the arguments passed to handleSubmit by the Form component. But then, that would also mean that I have to change the behavior of the handleSubmit supplied by the SocialSettingsContainer component, which is another client of the Form component (link).
  • I could try to change the registry/update too, but it has many clients, and those would require updating too.

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
});