Skip to content

Commit

Permalink
[Marketplace] Owner permission should be transferable #2188 (#2609)
Browse files Browse the repository at this point in the history
* Setup up version for migrating from old permissions style to groups

* Update accounts that match on migrate and add down method

* Add shop manager as part of default groups on start and add to migration file

* Show no dropdown if shop has only one group

* If account with no name, use first part of email as name

* Switch logger to debug

* add sort to display higher permission groups at the top

* Setup searching for and creating custom group and setting users to custom

* Add back initial setup from previous commits - inviteOwner method, mail template

* fix failing test

* Add form to call invite user form marketplace settings dashgs

* Set invited user as owner of new shop and set preferences

* Fix typo, and set default option

* Add comments to document flow of migration

* Prevent error on user without roles field in tests

* Change info logs to debug

* Add comment

* make dropdown more groups aware; and init owner swap

* wip

* Resolve lint import error

* Fix error in createShop; and update publication to limit to shop

* Merge seun-fix-passwordoverlay-comp-2605 into seun-invite-owner-2186

* Updating sortable for pre-passed data

* wip

* Convert comp to react; and use registerComponent

* Use correct Id on shopcreate

* Fix Reset password link and overlays broken (#2612)

* Fix missing component errors and component name reference

* Fix missing component imports and references

* Return use Component. for core comp

* Change componenent ref in resetPassword

* Update Match check to prevent error in cart after password reset

* Add todo comment on the check on cart/merge

* Update to only comment and fix test

* Alert before swap owner; Add i18n and event update

* Add load on long method calls

* Move loader to HOC

* Update to loader

* Separating marketplace owner change from shop owner change

* Remove log error

* Separate warning for Marketplace change

* Show dropdown options based on owner perm

* Update comment

* Set correct shopId when user updates password for the first time

* Check if addressBook exist before using

* Make shopIdAutoValueForCart separate from shopIdAutoValue
  • Loading branch information
impactmass authored and spencern committed Aug 4, 2017
1 parent 31adcf1 commit b99eebc
Show file tree
Hide file tree
Showing 13 changed files with 217 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ class AccountsDashboard extends Component {
this.setState({ selectedGroup: group });
};

handleMethodLoad = () => {
this.setState({ loading: true });
};

handleMethodDone = () => {
this.setState({ loading: false });
};

renderGroupDetail = () => {
const { groups, accounts } = this.state;
return (
Expand All @@ -47,11 +55,23 @@ class AccountsDashboard extends Component {

renderGroupsTable(groups) {
if (Array.isArray(groups)) {
return groups.map((group, index) => {
return (
<Components.GroupsTable key={index} group={group} onGroupSelect={this.handleGroupSelect} {...this.props} />
);
});
return (
<div className="group-container">
{this.state.loading && <Components.Loading />}
{groups.map((group, index) => {
return (
<Components.GroupsTable
key={index}
group={group}
onMethodLoad={this.handleMethodLoad}
onMethodDone={this.handleMethodDone}
onGroupSelect={this.handleGroupSelect}
{...this.props}
/>
);
})}
</div>
);
}

return null;
Expand Down
30 changes: 22 additions & 8 deletions imports/plugins/core/accounts/client/components/adminInviteForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ class AdminInviteForm extends Component {
constructor(props) {
super(props);
const { defaultInviteGroup, groups } = props;
const groupsInvitable = groups.filter((grp) => grp.slug !== "owner");

this.state = {
groups,
groups: groupsInvitable,
defaultInviteGroup,
name: "",
email: "",
Expand All @@ -30,7 +31,8 @@ class AdminInviteForm extends Component {

componentWillReceiveProps(nextProps) {
const { groups, defaultInviteGroup } = nextProps;
this.setState({ groups, defaultInviteGroup });
const groupsInvitable = groups.filter((grp) => grp.slug !== "owner");
this.setState({ groups: groupsInvitable, defaultInviteGroup });
}

onChange(event) {
Expand Down Expand Up @@ -99,15 +101,27 @@ class AdminInviteForm extends Component {
if (!buttonGroup._id) {
return null;
}
const buttonElement = (
const buttonElement = (opt) => (
<Components.Button bezelStyle="solid" label={buttonGroup.name && _.startCase(buttonGroup.name)} >
&nbsp;<i className="fa fa-chevron-down" />
&nbsp;
{opt && opt.length && // add icon only if there's a list of options
<i className="fa fa-chevron-down" />
}
</Components.Button>
);

// current selected option and "owner" should not show in list options
const dropOptions = this.state.groups.filter((grp) => grp._id !== buttonGroup._id);
if (!dropOptions.length) { return buttonElement(); } // do not use dropdown if only one option

return (
<Components.DropDownMenu buttonElement={buttonElement} attachment="bottom center" onChange={this.handleGroupSelect}>
{this.state.groups
.filter((grp) => grp._id !== buttonGroup._id)
<Components.DropDownMenu
buttonElement={buttonElement(dropOptions)}
onChange={this.handleGroupSelect}
attachment="bottom right"
targetAttachment="top right"
>
{dropOptions
.map((grp, index) => (
<Components.MenuItem
key={index}
Expand All @@ -120,7 +134,6 @@ class AdminInviteForm extends Component {
);
}


renderForm() {
return (
<div className="panel panel-default admin-invite-form">
Expand Down Expand Up @@ -156,6 +169,7 @@ class AdminInviteForm extends Component {
<div className="form-btns add-admin justify">
<Components.Button
status="primary"
buttonType="submit"
onClick={this.handleSubmit}
bezelStyle="solid"
i18nKeyLabel="accountsUI.info.sendInvitation"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class GroupForm extends Component {
<div className="justify">
<Components.Button
status="primary"
buttonType="submit"
onClick={this.handleSubmit}
bezelStyle="solid"
i18nKeyLabel={this.props.i18nKeyLabel}
Expand Down
74 changes: 50 additions & 24 deletions imports/plugins/core/accounts/client/components/groupsTableCell.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Meteor } from "meteor/meteor";
import React from "react";
import _ from "lodash";
import PropTypes from "prop-types";
import { Components, registerComponent } from "@reactioncommerce/reaction-components";
import { Reaction } from "/client/api";
import { getGravatar } from "../helpers/accountsHelper";

const GroupsTableCell = ({ account, columnName, group, groups, handleRemoveUserFromGroup, handleUserGroupChange }) => {
const GroupsTableCell = ({ account, columnName, group, groups, handleRemoveUserFromGroup, handleUserGroupChange, ...props }) => {
const email = _.get(account, "emails[0].address");

if (columnName === "name") {
Expand Down Expand Up @@ -37,40 +39,62 @@ const GroupsTableCell = ({ account, columnName, group, groups, handleRemoveUserF
}

if (columnName === "dropdown") {
const groupName = <p>{_.startCase(groups[0].name)}</p>;
const ownerGroup = groups.find((grp) => grp.slug === "owner") || {};
const hasOwnerAccess = Reaction.hasPermission("owner", Meteor.userId(), Reaction.getShopId());

if (groups.length === 1) {
return (
<p>{_.startCase(groups[0].name)}</p>
);
return groupName;
}

if (group.slug === "owner") {
return groupName;
}
const dropDownButton = (

const { onMethodDone, onMethodLoad } = props;
const dropDownButton = (opt) => ( // eslint-disable-line
<div className="group-dropdown">
<Components.Button label={group.name && _.startCase(group.name)}>
&nbsp;<i className="fa fa-chevron-down" />
<Components.Button bezelStyle="solid" label={group.name && _.startCase(group.name)}>
&nbsp;
{opt && opt.length > 1 && // add icon only if there's more than the current group
<i className="fa fa-chevron-down" />
}
</Components.Button>
</div>
);

// Permission check. Remove owner option, if user is not current owner
const dropOptions = groups.filter(grp => (grp.slug === "owner" && !hasOwnerAccess) ? false : true) || [];
if (dropOptions.length < 2) { return dropDownButton(); } // do not use dropdown if only one option

return (
<Components.DropDownMenu
buttonElement={dropDownButton}
attachment="bottom center"
onChange={handleUserGroupChange(account)}
>
{groups
.filter((grp) => grp._id !== group._id)
.map((grp, index) => (
<Components.MenuItem
key={index}
label={_.startCase(grp.name)}
selectLabel={_.startCase(grp.name)}
value={grp._id}
/>
))}
</Components.DropDownMenu>
<div className="group-dropdown">
<Components.DropDownMenu
className="dropdown-item"
buttonElement={dropDownButton(groups)}
attachment="bottom right"
targetAttachment="top right"
onChange={handleUserGroupChange({ account, ownerGrpId: ownerGroup._id, onMethodDone, onMethodLoad })}
>
{dropOptions
.filter(grp => grp._id !== group._id)
.map((grp, index) => (
<Components.MenuItem
key={index}
label={_.startCase(grp.name)}
selectLabel={_.startCase(grp.name)}
value={grp._id}
/>
))}
</Components.DropDownMenu>
</div>
);
}

if (columnName === "button") {
if (group.slug === "owner") {
return null;
}
return (
<div className="group-table-button">
<Components.Button
Expand All @@ -93,7 +117,9 @@ GroupsTableCell.propTypes = {
group: PropTypes.object, // current group in interation
groups: PropTypes.array, // all available groups
handleRemoveUserFromGroup: PropTypes.func,
handleUserGroupChange: PropTypes.func
handleUserGroupChange: PropTypes.func,
onMethodDone: PropTypes.func,
onMethodLoad: PropTypes.func
};

registerComponent("GroupsTableCell", GroupsTableCell);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,55 @@
import { compose, withProps } from "recompose";
import Alert from "sweetalert2";
import { registerComponent, composeWithTracker } from "@reactioncommerce/reaction-components";
import { Meteor } from "meteor/meteor";
import { Accounts, Groups } from "/lib/collections";
import { Reaction, i18next } from "/client/api";
import AccountsDashboard from "../components/accountsDashboard";

const handlers = {
handleUserGroupChange(account) {
handleUserGroupChange({ account, ownerGrpId, onMethodLoad, onMethodDone }) {
return (event, groupId) => {
if (onMethodLoad) { onMethodLoad(); }

if (groupId === ownerGrpId) {
return alertConfirm()
.then(() => {
return updateMethodCall(groupId);
})
.catch(() => {
if (onMethodDone) { onMethodDone(); }
});
}

return updateMethodCall(groupId);
};

function updateMethodCall(groupId) {
Meteor.call("group/addUser", account._id, groupId, (err) => {
if (err) {
return Alerts.toast(i18next.t("admin.groups.addUserError", { err: err.message }), "error");
Alerts.toast(i18next.t("admin.groups.addUserError", { err: err.message }), "error");
}
return Alerts.toast(i18next.t("admin.groups.addUserSuccess"), "success");
if (!err) {
Alerts.toast(i18next.t("admin.groups.addUserSuccess"), "success");
}
if (onMethodDone) { onMethodDone(); }
});
};
}

function alertConfirm() {
let changeOwnerWarn = "changeShopOwnerWarn";
if (Reaction.getShopId() === Reaction.getPrimaryShopId()) {
changeOwnerWarn = "changeMktOwnerWarn";
}
return Alert({
title: i18next.t("admin.settings.changeOwner"),
text: i18next.t(`admin.settings.${changeOwnerWarn}`),
type: "warning",
showCancelButton: true,
cancelButtonText: i18next.t("admin.settings.cancel"),
confirmButtonText: i18next.t("admin.settings.continue")
});
}
},

handleRemoveUserFromGroup(account, groupId) {
Expand All @@ -30,6 +65,7 @@ const handlers = {
};

const composer = (props, onData) => {
const shopId = Reaction.getShopId();
const adminUserSub = Meteor.subscribe("Accounts", null);
const grpSub = Meteor.subscribe("Groups");

Expand All @@ -39,14 +75,14 @@ const composer = (props, onData) => {
shopId: Reaction.getShopId()
}).fetch();
const adminQuery = {
[`roles.${Reaction.getShopId()}`]: {
[`roles.${shopId}`]: {
$in: ["dashboard"]
}
};

const adminUsers = Meteor.users.find(adminQuery, { fields: { _id: 1 } }).fetch();
const ids = adminUsers.map((user) => user._id);
const accounts = Accounts.find({ _id: { $in: ids }, shopId: Reaction.getShopId() }).fetch();
const accounts = Accounts.find({ _id: { $in: ids } }).fetch();

onData(null, { accounts, groups });
}
Expand Down
5 changes: 5 additions & 0 deletions imports/plugins/core/accounts/server/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
"accountsDescription": "Account Settings"
},
"settings": {
"continue": "Continue",
"cancel": "Cancel",
"changeOwner": "Change Owner",
"changeShopOwnerWarn": "You are about to change the Shop owner. The current owner will be replaced",
"changeMktOwnerWarn": "You are about to change the Marketplace owner. The current owner will be replaced",
"accountsSettingsLabel": "Accounts",
"accountsSettingsTitle": "Accounts",
"accountsSettingsDescription": "Account Settings"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@
}
border-left: 1px solid #ccc;
}
.group-container {
position: relative;
.spinner-container {
position: absolute;
top: 50%;
left: 50%;
z-index: 9999;
}
}
}

.accounts-group-table {
Expand Down
6 changes: 3 additions & 3 deletions lib/collections/schemas/cart.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SimpleSchema } from "meteor/aldeed:simple-schema";
import { shopIdAutoValue } from "./helpers";
import { shopIdAutoValueForCart } from "./helpers";
import { Payment } from "./payments";
import { Product, ProductVariant } from "./products";
import { Shipment, ShippingParcel } from "./shipping";
Expand All @@ -19,7 +19,7 @@ export const CartItem = new SimpleSchema({
},
shopId: {
type: String,
autoValue: shopIdAutoValue,
autoValue: shopIdAutoValueForCart,
index: 1,
label: "Cart Item shopId",
optional: true
Expand Down Expand Up @@ -87,7 +87,7 @@ export const Cart = new SimpleSchema({
},
shopId: {
type: String,
autoValue: shopIdAutoValue,
autoValue: shopIdAutoValueForCart,
index: 1,
label: "Cart ShopId"
},
Expand Down
16 changes: 16 additions & 0 deletions lib/collections/schemas/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ import { Reaction } from "/lib/api";
* @return {String} returns current shopId
*/
export function shopIdAutoValue() {
// we should always have a shopId
if (this.isSet && Meteor.isServer) {
return this.value;
} else if (Meteor.isServer && !this.isUpdate || Meteor.isClient && this.isInsert) {
return Reaction.getShopId();
}
return this.unset();
}

/**
* shopIdAutoValueForCart
* @summary copy of shopIdAutoValue with modification for Cart
* @example autoValue: shopIdAutoValue
* @return {String} shopId
*/
export function shopIdAutoValueForCart() {
// we should always have a shopId
if (this.isSet && Meteor.isServer) {
return this.value;
Expand Down
Loading

0 comments on commit b99eebc

Please sign in to comment.