From 43ca38946ec84c5508b9f4fc2faf0ed40c1efd6a Mon Sep 17 00:00:00 2001 From: Loan Laux Date: Wed, 15 Apr 2020 19:41:12 +0200 Subject: [PATCH 01/38] refactor: add withAccounts and withGroups HOCs Signed-off-by: Loan Laux --- .../client/components/accountsDashboard.js | 50 ++++++---- .../containers/accountsDashboardContainer.js | 43 +++----- .../accounts/client/helpers/accountsHelper.js | 9 +- .../core/accounts/client/hocs/withAccounts.js | 98 +++++++++++++++++++ .../core/accounts/client/hocs/withGroups.js | 61 ++++++++++++ 5 files changed, 208 insertions(+), 53 deletions(-) create mode 100644 imports/plugins/core/accounts/client/hocs/withAccounts.js create mode 100644 imports/plugins/core/accounts/client/hocs/withGroups.js diff --git a/imports/plugins/core/accounts/client/components/accountsDashboard.js b/imports/plugins/core/accounts/client/components/accountsDashboard.js index 7cadef9f5f..30f30f5edf 100644 --- a/imports/plugins/core/accounts/client/components/accountsDashboard.js +++ b/imports/plugins/core/accounts/client/components/accountsDashboard.js @@ -15,29 +15,37 @@ class AccountsDashboard extends Component { constructor(props) { super(props); - const { accounts, adminGroups, groups } = this.props; - const sortedGroups = sortUsersIntoGroups({ groups: sortGroups(adminGroups), accounts }) || []; - const defaultSelectedGroup = sortedGroups[0]; + const { accounts, adminGroups, groups, isLoadingAccounts, isLoadingGroups } = this.props; - this.state = { - accounts, - groups: sortGroups(groups), - adminGroups: sortedGroups, - selectedGroup: defaultSelectedGroup - }; + if (isLoadingAccounts === false && isLoadingGroups === false && Array.isArray(accounts) && Array.isArray(groups)) { + const sortedGroups = sortUsersIntoGroups({ groups: sortGroups(adminGroups), accounts }) || []; + const defaultSelectedGroup = sortedGroups[0]; + + this.state = { + accounts, + groups: sortGroups(groups), + adminGroups: sortedGroups, + selectedGroup: defaultSelectedGroup + }; + } else { + this.state = {}; + } } // eslint-disable-next-line camelcase UNSAFE_componentWillReceiveProps(nextProps) { - const { adminGroups, accounts, groups } = nextProps; - const sortedGroups = sortUsersIntoGroups({ groups: sortGroups(adminGroups), accounts }); - const selectedGroup = adminGroups.find((grp) => grp._id === (this.state.selectedGroup || {})._id); - this.setState({ - adminGroups: sortedGroups, - groups: sortGroups(groups), - accounts, - selectedGroup - }); + const { adminGroups, accounts, groups, isLoadingAccounts, isLoadingGroups } = nextProps; + + if (isLoadingAccounts === false && isLoadingGroups === false && Array.isArray(accounts) && Array.isArray(groups)) { + const sortedGroups = sortUsersIntoGroups({ groups: sortGroups(adminGroups), accounts }); + const selectedGroup = adminGroups.find((grp) => grp._id === (this.state.selectedGroup || {})._id); + this.setState({ + adminGroups: sortedGroups, + groups: sortGroups(groups), + accounts, + selectedGroup + }); + } } handleGroupSelect = (group) => { @@ -89,14 +97,16 @@ class AccountsDashboard extends Component { } render() { + const { isLoadingAccounts, isLoadingGroups } = this.props; + return (
{i18next.t("admin.dashboard.manageGroups")}
- {this.renderGroupsTable(this.state.adminGroups)} + {isLoadingAccounts || isLoadingGroups ? "" : this.renderGroupsTable(this.state.adminGroups)} - {this.renderGroupDetail()} + {isLoadingAccounts || isLoadingGroups ? "Loading..." : this.renderGroupDetail()}
); diff --git a/imports/plugins/core/accounts/client/containers/accountsDashboardContainer.js b/imports/plugins/core/accounts/client/containers/accountsDashboardContainer.js index 9adcd16ce7..847f835162 100644 --- a/imports/plugins/core/accounts/client/containers/accountsDashboardContainer.js +++ b/imports/plugins/core/accounts/client/containers/accountsDashboardContainer.js @@ -1,14 +1,17 @@ import { compose, withProps } from "recompose"; import Alert from "sweetalert2"; -import { registerComponent, composeWithTracker, withIsAdmin } from "@reactioncommerce/reaction-components"; +import { registerComponent, withIsAdmin } from "@reactioncommerce/reaction-components"; import { Meteor } from "meteor/meteor"; import getOpaqueIds from "/imports/plugins/core/core/client/util/getOpaqueIds"; import simpleGraphQLClient from "/imports/plugins/core/graphql/lib/helpers/simpleClient"; -import { Accounts, Groups } from "/lib/collections"; -import { Reaction, i18next } from "/client/api"; +import withOpaqueShopId from "/imports/plugins/core/graphql/lib/hocs/withOpaqueShopId"; +import { Accounts } from "/lib/collections"; +import { i18next } from "/client/api"; import AccountsDashboard from "../components/accountsDashboard"; import addAccountToGroupMutation from "./addAccountToGroup.graphql"; import removeAccountFromGroupMutation from "./removeAccountFromGroup.graphql"; +import withAccounts from "../hocs/withAccounts"; +import withGroups from "../hocs/withGroups"; const addAccountToGroupMutate = simpleGraphQLClient.createMutationFunction(addAccountToGroupMutation); const removeAccountFromGroupMutate = simpleGraphQLClient.createMutationFunction(removeAccountFromGroupMutation); @@ -103,40 +106,18 @@ const handlers = { } }; -const composer = (props, onData) => { - const shopId = Reaction.getShopId(); - if (!shopId) return; - - const grpSub = Meteor.subscribe("Groups", { shopId }); - const accountSub = Meteor.subscribe("Accounts"); - - if (accountSub.ready() && grpSub.ready()) { - const groups = Groups.find({ shopId }).fetch(); - const adminGroups = groups.filter((group) => group.slug !== "customer" && group.slug !== "guest"); - - // Find all accounts that are in any of the admin groups - // or not in any group - const adminGroupIds = adminGroups.map((group) => group._id); - const accounts = Accounts.find({ - $or: [ - { groups: { $in: adminGroupIds } }, - { groups: null }, - { groups: { $size: 0 } } - ] - }).fetch(); - - onData(null, { accounts, adminGroups, groups }); - } -}; - registerComponent("AccountsDashboard", AccountsDashboard, [ withIsAdmin, - composeWithTracker(composer), + withOpaqueShopId, + withGroups, + withAccounts, withProps(handlers) ]); export default compose( withIsAdmin, - composeWithTracker(composer), + withOpaqueShopId, + withGroups, + withAccounts, withProps(handlers) )(AccountsDashboard); diff --git a/imports/plugins/core/accounts/client/helpers/accountsHelper.js b/imports/plugins/core/accounts/client/helpers/accountsHelper.js index 71b6d5ba3d..7050153046 100644 --- a/imports/plugins/core/accounts/client/helpers/accountsHelper.js +++ b/imports/plugins/core/accounts/client/helpers/accountsHelper.js @@ -12,7 +12,12 @@ import * as Collections from "/lib/collections"; */ export default function sortUsersIntoGroups({ accounts, groups }) { const newGroups = groups.map((group) => { - const matchingAccounts = accounts.filter((acc) => acc.groups && acc.groups.indexOf(group._id) > -1); + const matchingAccounts = accounts.filter((acc) => { + const accountGroups = acc.groups && acc.groups.nodes; + const accountGroupIds = accountGroups.map((accountGroup) => accountGroup._id); + + return accountGroupIds.includes(group._id); + }); group.users = _.compact(matchingAccounts); return group; }); @@ -35,7 +40,7 @@ export default function sortUsersIntoGroups({ accounts, groups }) { * @returns {Array} [description] */ export function sortGroups(groups) { - return groups.sort((prev, next) => { + return groups && groups.sort((prev, next) => { if (next.slug === "owner") { return 1; } // owner tops return next.permissions.length - prev.permissions.length; }); diff --git a/imports/plugins/core/accounts/client/hocs/withAccounts.js b/imports/plugins/core/accounts/client/hocs/withAccounts.js new file mode 100644 index 0000000000..43f8e5e941 --- /dev/null +++ b/imports/plugins/core/accounts/client/hocs/withAccounts.js @@ -0,0 +1,98 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { Query } from "react-apollo"; +import gql from "graphql-tag"; + +const getAccounts = gql` + query accounts( + $groupIds: [ID], + $first: ConnectionLimitInt, + $last: ConnectionLimitInt, + $before: ConnectionCursor, + $after: ConnectionCursor, + $offset: Int, + $sortBy: AccountSortByField, + $sortOrder: SortOrder + ) { + accounts( + groupIds: $groupIds, + first: $first, + last: $last, + before: $before, + after: $after, + offset: $offset, + sortBy: $sortBy, + sortOrder: $sortOrder + ) { + nodes { + _id + addressBook { + nodes { + address1 + } + } + createdAt + currency { + code + } + emailRecords { + address + verified + } + groups { + nodes { + _id + description + name + permissions + } + } + metafields { + key + scope + namespace + description + value + valueType + } + name + note + preferences + updatedAt + } + } + } +`; + +export default (Component) => ( + class AccountsQuery extends React.Component { + static propTypes = { + groups: PropTypes.array + }; + + render() { + const { groups } = this.props; + + const groupIds = groups && groups.map((group) => group._id); + + return ( + + {({ loading, data }) => { + const props = { + ...this.props, + isLoadingAccounts: loading + }; + + if (!loading && data) { + props.accounts = data.accounts.nodes; + } + + return ( + + ); + }} + + ); + } + } +); diff --git a/imports/plugins/core/accounts/client/hocs/withGroups.js b/imports/plugins/core/accounts/client/hocs/withGroups.js new file mode 100644 index 0000000000..7b8c4c5e92 --- /dev/null +++ b/imports/plugins/core/accounts/client/hocs/withGroups.js @@ -0,0 +1,61 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { Query } from "react-apollo"; +import gql from "graphql-tag"; + +const getGroups = gql` + query getGroups($shopId: ID!) { + groups(shopId: $shopId) { + nodes { + _id + createdAt + createdBy { + _id + } + description + name + permissions + slug + updatedAt + } + } + } +`; + +export default (Component) => ( + class GroupsQuery extends React.Component { + static propTypes = { + shopId: PropTypes.string + }; + + render() { + const { shopId } = this.props; + + if (!shopId) { + return ( + + ); + } + + return ( + + {({ loading, data }) => { + const props = { + ...this.props, + isLoadingGroups: loading + }; + + if (!loading && data) { + props.groups = data.groups.nodes; + props.adminGroups = props.groups.filter((group) => group.slug !== "customer" && group.slug !== "guest"); + } + + return ( + + ); + }} + + ); + } + } +); From 02a9a1b4437213f5c4819bb8e8e82c17978a6d20 Mon Sep 17 00:00:00 2001 From: Loan Laux Date: Wed, 15 Apr 2020 19:42:09 +0200 Subject: [PATCH 02/38] feat: use loading spinner when necessary Signed-off-by: Loan Laux --- .../core/accounts/client/components/accountsDashboard.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imports/plugins/core/accounts/client/components/accountsDashboard.js b/imports/plugins/core/accounts/client/components/accountsDashboard.js index 30f30f5edf..2051365df4 100644 --- a/imports/plugins/core/accounts/client/components/accountsDashboard.js +++ b/imports/plugins/core/accounts/client/components/accountsDashboard.js @@ -104,9 +104,9 @@ class AccountsDashboard extends Component {
{i18next.t("admin.dashboard.manageGroups")}
- {isLoadingAccounts || isLoadingGroups ? "" : this.renderGroupsTable(this.state.adminGroups)} + {isLoadingAccounts || isLoadingGroups ? : this.renderGroupsTable(this.state.adminGroups)} - {isLoadingAccounts || isLoadingGroups ? "Loading..." : this.renderGroupDetail()} + {isLoadingAccounts || isLoadingGroups ? : this.renderGroupDetail()} ); From 0a22a8e10882db776a4657e77a101e9fb31fcd86 Mon Sep 17 00:00:00 2001 From: Loan Laux Date: Wed, 15 Apr 2020 20:26:42 +0200 Subject: [PATCH 03/38] fix: wire fields to table Signed-off-by: Loan Laux --- .../plugins/core/accounts/client/components/groupHeader.js | 2 +- .../plugins/core/accounts/client/components/groupsTable.js | 7 ++++--- .../core/accounts/client/components/groupsTableCell.js | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/imports/plugins/core/accounts/client/components/groupHeader.js b/imports/plugins/core/accounts/client/components/groupHeader.js index 981ce1f101..d8616cadfc 100644 --- a/imports/plugins/core/accounts/client/components/groupHeader.js +++ b/imports/plugins/core/accounts/client/components/groupHeader.js @@ -14,7 +14,7 @@ const GroupHeader = ({ columnName }) => { ); } - if (columnName === "email") { + if (columnName === "emailRecords") { return (
diff --git a/imports/plugins/core/accounts/client/components/groupsTable.js b/imports/plugins/core/accounts/client/components/groupsTable.js index 5ae447715a..02eba08284 100644 --- a/imports/plugins/core/accounts/client/components/groupsTable.js +++ b/imports/plugins/core/accounts/client/components/groupsTable.js @@ -6,7 +6,7 @@ import { SortableTable } from "/imports/plugins/core/ui/client/components"; const GroupsTable = (props) => { const { group } = props; - const fields = ["name", "email", "createdAt", "dropdown", "button"]; + const fields = ["name", "emailRecords", "createdAt", "dropdown", "button"]; const tableClass = (length) => classnames({ "accounts-group-table": true, @@ -14,14 +14,15 @@ const GroupsTable = (props) => { }); const columnMetadata = fields.map((columnName) => ({ - Header: , + Header: , accessor: "", - // TODO: Review this line - copied disable line from shippo carriers.js Cell: (data) => { // eslint-disable-line return ; } })); + console.log(group); + return ( diff --git a/imports/plugins/core/accounts/client/components/groupsTableCell.js b/imports/plugins/core/accounts/client/components/groupsTableCell.js index 0d70b1386e..0adc0d87ac 100644 --- a/imports/plugins/core/accounts/client/components/groupsTableCell.js +++ b/imports/plugins/core/accounts/client/components/groupsTableCell.js @@ -15,7 +15,7 @@ const GroupsTableCell = (props) => { moment } = props; - const email = _.get(account, "emails[0].address", ""); + const email = account.emailRecords[0].address; const groups = adminGroups; const userAvatar = getUserAvatar(account); const createdAt = (moment && moment(account.createdAt).format("MMM Do")) || account.createdAt.toLocaleString(); @@ -31,7 +31,7 @@ const GroupsTableCell = (props) => { ); } - if (columnName === "email") { + if (columnName === "emailRecords") { return (
{email} From 2c884e9ca7f8c68d17de7a242a8dcb19eab06e68 Mon Sep 17 00:00:00 2001 From: Loan Laux Date: Wed, 15 Apr 2020 20:31:00 +0200 Subject: [PATCH 04/38] chore: remove console.log usage Signed-off-by: Loan Laux --- imports/plugins/core/accounts/client/components/groupsTable.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/imports/plugins/core/accounts/client/components/groupsTable.js b/imports/plugins/core/accounts/client/components/groupsTable.js index 02eba08284..afd3a53ad7 100644 --- a/imports/plugins/core/accounts/client/components/groupsTable.js +++ b/imports/plugins/core/accounts/client/components/groupsTable.js @@ -21,8 +21,6 @@ const GroupsTable = (props) => { } })); - console.log(group); - return ( From 568767210a61bce7697ac9ca5554d316ed808e18 Mon Sep 17 00:00:00 2001 From: Loan Laux Date: Thu, 16 Apr 2020 00:46:36 +0200 Subject: [PATCH 05/38] fix: use correct IDs for inviteShopMember input Signed-off-by: Loan Laux --- .../client/components/adminInviteForm.js | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/imports/plugins/core/accounts/client/components/adminInviteForm.js b/imports/plugins/core/accounts/client/components/adminInviteForm.js index 2792d16059..4477625394 100644 --- a/imports/plugins/core/accounts/client/components/adminInviteForm.js +++ b/imports/plugins/core/accounts/client/components/adminInviteForm.js @@ -11,7 +11,7 @@ import InlineAlert from "@reactioncommerce/components/InlineAlert/v1"; import { getDefaultUserInviteGroup, getUserByEmail } from "../helpers/accountsHelper"; import gql from "graphql-tag"; import { Mutation } from "react-apollo"; -import getOpaqueIds from "/imports/plugins/core/core/client/util/getOpaqueIds"; +import withOpaqueShopId from "/imports/plugins/core/graphql/lib/hocs/withOpaqueShopId"; const iconComponents = { iconDismiss: @@ -66,23 +66,10 @@ class AdminInviteForm extends Component { alertArray: this.state.alertArray.filter((alert) => !_.isEqual(alert, oldAlert)) }); - sendInvitation = async (options, mutation) => { - const [ - opaqueGroupId, - opaqueShopId - ] = await getOpaqueIds([ - { namespace: "Group", id: options.groupId }, - { namespace: "Shop", id: options.shopId } - ]); - + sendInvitation = async (input, mutation) => { await mutation({ variables: { - input: { - email: options.email, - groupId: opaqueGroupId, - name: options.name, - shopId: opaqueShopId - } + input } }); } @@ -108,7 +95,8 @@ class AdminInviteForm extends Component { const isEmailVerified = matchingEmail && matchingEmail.verified; - const options = { email, name, shopId: Reaction.getShopId(), groupId: group._id }; + const { shopId } = this.props; + const options = { email, name, shopId, groupId: group._id }; if (matchingAccount) { return Alerts.alert({ @@ -258,6 +246,8 @@ class AdminInviteForm extends Component { } } -registerComponent("AdminInviteForm", AdminInviteForm); +registerComponent("AdminInviteForm", AdminInviteForm, [ + withOpaqueShopId +]); -export default AdminInviteForm; +export default withOpaqueShopId(AdminInviteForm); From e674374e150e9deaf197f5f7c1e2374a420e9480 Mon Sep 17 00:00:00 2001 From: Loan Laux Date: Thu, 16 Apr 2020 00:55:35 +0200 Subject: [PATCH 06/38] fix: use correct IDs in accounts dashboard handlers Signed-off-by: Loan Laux --- .../containers/accountsDashboardContainer.js | 27 ++++++------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/imports/plugins/core/accounts/client/containers/accountsDashboardContainer.js b/imports/plugins/core/accounts/client/containers/accountsDashboardContainer.js index 847f835162..631d03919f 100644 --- a/imports/plugins/core/accounts/client/containers/accountsDashboardContainer.js +++ b/imports/plugins/core/accounts/client/containers/accountsDashboardContainer.js @@ -2,7 +2,6 @@ import { compose, withProps } from "recompose"; import Alert from "sweetalert2"; import { registerComponent, withIsAdmin } from "@reactioncommerce/reaction-components"; import { Meteor } from "meteor/meteor"; -import getOpaqueIds from "/imports/plugins/core/core/client/util/getOpaqueIds"; import simpleGraphQLClient from "/imports/plugins/core/graphql/lib/helpers/simpleClient"; import withOpaqueShopId from "/imports/plugins/core/graphql/lib/hocs/withOpaqueShopId"; import { Accounts } from "/lib/collections"; @@ -64,15 +63,10 @@ const handlers = { } try { - const [ - opaqueAccountId, - opaqueGroupId - ] = await getOpaqueIds([ - { namespace: "Account", id: account._id }, - { namespace: "Group", id: groupId } - ]); - - await addAccountToGroupMutate({ accountId: opaqueAccountId, groupId: opaqueGroupId }); + await addAccountToGroupMutate({ + accountId: account._id, + groupId + }); } catch (error) { Alerts.toast(i18next.t("admin.groups.addUserError", { err: error.message }), "error"); } @@ -88,15 +82,10 @@ const handlers = { if (!value) return null; try { - const [ - opaqueAccountId, - opaqueGroupId - ] = await getOpaqueIds([ - { namespace: "Account", id: account._id }, - { namespace: "Group", id: groupId } - ]); - - await removeAccountFromGroupMutate({ accountId: opaqueAccountId, groupId: opaqueGroupId }); + await removeAccountFromGroupMutate({ + accountId: account._id, + groupId + }); } catch (error) { Alerts.toast(i18next.t("admin.groups.removeUserError", { err: error.message }), "error"); } From 4399204c540c02888bb11e4c2092b2c9809d812d Mon Sep 17 00:00:00 2001 From: Loan Laux Date: Thu, 16 Apr 2020 12:27:45 +0200 Subject: [PATCH 07/38] feat: use viewer query instead of MiniMongo Signed-off-by: Loan Laux --- .../containers/accountsDashboardContainer.js | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/imports/plugins/core/accounts/client/containers/accountsDashboardContainer.js b/imports/plugins/core/accounts/client/containers/accountsDashboardContainer.js index 631d03919f..e73dbd5c89 100644 --- a/imports/plugins/core/accounts/client/containers/accountsDashboardContainer.js +++ b/imports/plugins/core/accounts/client/containers/accountsDashboardContainer.js @@ -1,4 +1,5 @@ -import { compose, withProps } from "recompose"; +import React from "react"; +import { compose } from "recompose"; import Alert from "sweetalert2"; import { registerComponent, withIsAdmin } from "@reactioncommerce/reaction-components"; import { Meteor } from "meteor/meteor"; @@ -11,6 +12,7 @@ import addAccountToGroupMutation from "./addAccountToGroup.graphql"; import removeAccountFromGroupMutation from "./removeAccountFromGroup.graphql"; import withAccounts from "../hocs/withAccounts"; import withGroups from "../hocs/withGroups"; +import useAuth from "../../../../../client/ui/hooks/useAuth"; const addAccountToGroupMutate = simpleGraphQLClient.createMutationFunction(addAccountToGroupMutation); const removeAccountFromGroupMutate = simpleGraphQLClient.createMutationFunction(removeAccountFromGroupMutation); @@ -45,14 +47,15 @@ function alertConfirmChangeOwner() { }); } -const handlers = { - handleUserGroupChange({ account, currentGroupId, ownerGrpId, onMethodLoad, onMethodDone }) { +function AccountsDashboardContainer(props) { + const { viewer: loggedInAccount } = useAuth(); + + const handleUserGroupChange = ({ account, currentGroupId, ownerGrpId, onMethodLoad, onMethodDone }) => { return async (event, groupId) => { if (onMethodLoad) onMethodLoad(); // Confirm if removing owner role from myself if (currentGroupId === ownerGrpId) { - const loggedInAccount = Accounts.findOne({ userId: Meteor.userId() }); if (loggedInAccount && loggedInAccount._id === account._id) { const { value } = await alertConfirmChangeOwner(); if (!value) { @@ -74,9 +77,9 @@ const handlers = { if (onMethodDone) onMethodDone(); return null; }; - }, + }; - handleRemoveUserFromGroup(account, groupId) { + const handleRemoveUserFromGroup = (account, groupId) => { return async () => { const { value } = await alertConfirmRemoveUser(); if (!value) return null; @@ -92,21 +95,27 @@ const handlers = { return null; }; - } -}; + }; + + return ( + + ); +} -registerComponent("AccountsDashboard", AccountsDashboard, [ +registerComponent("AccountsDashboard", AccountsDashboardContainer, [ withIsAdmin, withOpaqueShopId, withGroups, - withAccounts, - withProps(handlers) + withAccounts ]); export default compose( withIsAdmin, withOpaqueShopId, withGroups, - withAccounts, - withProps(handlers) -)(AccountsDashboard); + withAccounts +)(AccountsDashboardContainer); From 786c34b302b8dcefc589294cc9c83565a72463eb Mon Sep 17 00:00:00 2001 From: Loan Laux Date: Sun, 19 Apr 2020 13:03:55 +0200 Subject: [PATCH 08/38] feat: fetch roles with GraphQL Signed-off-by: Loan Laux --- .../accounts/client/components/editGroup.js | 224 +----------------- .../client/components/manageGroups.js | 17 +- .../client/components/permissionsList.js | 51 +--- .../containers/accountsDashboardContainer.js | 11 +- .../core/accounts/client/hocs/withRoles.js | 51 ++++ 5 files changed, 79 insertions(+), 275 deletions(-) create mode 100644 imports/plugins/core/accounts/client/hocs/withRoles.js diff --git a/imports/plugins/core/accounts/client/components/editGroup.js b/imports/plugins/core/accounts/client/components/editGroup.js index 5091a04b67..2fd536d629 100644 --- a/imports/plugins/core/accounts/client/components/editGroup.js +++ b/imports/plugins/core/accounts/client/components/editGroup.js @@ -10,228 +10,6 @@ import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import CardHeader from "@material-ui/core/CardHeader"; -const permissionList = [ - { - name: "reaction-dashboard", - label: "Dashboard", - permissions: [ - { - permission: "dashboard", - label: "Dashboard" - }, - { - permission: "shopSettings", - label: "Shop Settings" - } - ] - }, - { - name: "reaction-taxes-rates", - label: "Taxes Rates", - permissions: [] - }, - { - name: "reaction-file-collections", - label: "File Collections", - permissions: [ - { - permission: "media/create", - label: "Create Media" - }, - { - permission: "media/update", - label: "Update Media" - }, - { - permission: "media/delete", - label: "Delete Media" - } - ] - }, - { - name: "reaction-taxes", - label: "Taxes", - permissions: [] - }, - { - name: "reaction-i18n", - label: "I18n", - permissions: [] - }, - { - name: "reaction-product-admin", - label: "Product Admin", - permissions: [ - { - permission: "product/admin", - label: "Product Admin" - }, - { - permission: "product/archive", - label: "Archive Product" - }, - { - permission: "product/clone", - label: "Clone Product" - }, - { - permission: "product/create", - label: "Create Product" - }, - { - permission: "product/publish", - label: "Publish Product" - }, - { - permission: "product/update", - label: "Update Product" - } - ] - }, - { - name: "reaction-email", - label: "Email", - permissions: [] - }, - { - label: "Address", - permissions: [] - }, - { - name: "reaction-orders", - label: "Orders", - permissions: [ - { - permission: "order/fulfillment", - label: "Order Fulfillment" - }, - { - permission: "order/view", - label: "Order View" - } - ] - }, - { - name: "reaction-product-variant", - label: "Product Variant", - permissions: [ - { - permission: "tag", - label: "/tag/:slug?" - }, - { - permission: "createProduct", - label: "Add Product" - } - ] - }, - { - name: "reaction-tags", - label: "Tags", - permissions: [ - { - permission: "tag/admin", - label: "Tag Admin" - }, - { - permission: "tag/edit", - label: "Edit Tag" - } - ] - }, - { - name: "reaction-accounts", - label: "Accounts", - permissions: [ - { - permission: "accounts", - label: "Accounts" - }, - { - permission: "account/verify", - label: "Account Verify" - }, - { - permission: "reaction-accounts/accountsSettings", - label: "Account Settings" - }, - { - permission: "dashboard/accounts", - label: "Accounts" - }, - { - permission: "account/profile", - label: "Profile" - }, - { - permission: "reset-password", - label: "reset-password" - }, - { - permission: "account/enroll", - label: "Account Enroll" - }, - { - permission: "account/invite", - label: "Account Invite" - } - ] - }, - { - name: "discount-codes", - label: "discount Codes", - permissions: [ - { - permission: "discounts/apply", - label: "Apply Discounts" - } - ] - }, - { - name: "reaction-shipping", - label: "Shipping", - permissions: [ - { - permission: "shipping", - label: "Shipping" - } - ] - }, - { - name: "reaction-notification", - label: "Notification", - permissions: [ - { - permission: "notifications", - label: "Notifications" - } - ] - }, - { - name: "reaction-templates", - label: "Templates", - permissions: [] - }, - { - name: "reaction-discounts", - label: "Discounts", - permissions: [] - }, - { - label: "Hydra Oauth", - permissions: [ - { - permission: "account/login", - label: "OAuth Login" - }, - { - permission: "not-found", - label: "not-found" - } - ] - } -]; - class EditGroup extends Component { static propTypes = { accounts: PropTypes.array, @@ -383,7 +161,7 @@ class EditGroup extends Component { } return ( diff --git a/imports/plugins/core/accounts/client/components/manageGroups.js b/imports/plugins/core/accounts/client/components/manageGroups.js index a9a3edae75..b131f91637 100644 --- a/imports/plugins/core/accounts/client/components/manageGroups.js +++ b/imports/plugins/core/accounts/client/components/manageGroups.js @@ -51,15 +51,14 @@ class ManageGroups extends Component { groups={groupsInvitable} /> } - {this.props.isAdmin && -
- -
- } +
+ +
); } diff --git a/imports/plugins/core/accounts/client/components/permissionsList.js b/imports/plugins/core/accounts/client/components/permissionsList.js index 6d282afad0..794bf8489e 100644 --- a/imports/plugins/core/accounts/client/components/permissionsList.js +++ b/imports/plugins/core/accounts/client/components/permissionsList.js @@ -87,47 +87,22 @@ class PermissionsList extends Component { return false; }; - renderSubPermissions(permission) { - if (permission.permissions.length) { - return ( -
- {permission.permissions.map((childPermission, index) => ( - - ))} -
- ); - } - return null; - } - - renderPermissions(permissions) { - const jsx = []; - permissions.forEach((permission, key) => { - jsx.push(
- - {this.renderSubPermissions(permission)} -
); - }); - return jsx; - } - render() { + const { permissions } = this.props; + return (
- {this.renderPermissions(_.compact(this.props.permissions))} + {permissions && permissions.map((permission) => ( +
+ +
+ ))}
); } diff --git a/imports/plugins/core/accounts/client/containers/accountsDashboardContainer.js b/imports/plugins/core/accounts/client/containers/accountsDashboardContainer.js index e73dbd5c89..f9088701f9 100644 --- a/imports/plugins/core/accounts/client/containers/accountsDashboardContainer.js +++ b/imports/plugins/core/accounts/client/containers/accountsDashboardContainer.js @@ -2,17 +2,16 @@ import React from "react"; import { compose } from "recompose"; import Alert from "sweetalert2"; import { registerComponent, withIsAdmin } from "@reactioncommerce/reaction-components"; -import { Meteor } from "meteor/meteor"; import simpleGraphQLClient from "/imports/plugins/core/graphql/lib/helpers/simpleClient"; import withOpaqueShopId from "/imports/plugins/core/graphql/lib/hocs/withOpaqueShopId"; -import { Accounts } from "/lib/collections"; import { i18next } from "/client/api"; import AccountsDashboard from "../components/accountsDashboard"; import addAccountToGroupMutation from "./addAccountToGroup.graphql"; import removeAccountFromGroupMutation from "./removeAccountFromGroup.graphql"; import withAccounts from "../hocs/withAccounts"; import withGroups from "../hocs/withGroups"; -import useAuth from "../../../../../client/ui/hooks/useAuth"; +import withRoles from "../hocs/withRoles"; +import useAuth from "/imports/client/ui/hooks/useAuth"; const addAccountToGroupMutate = simpleGraphQLClient.createMutationFunction(addAccountToGroupMutation); const removeAccountFromGroupMutate = simpleGraphQLClient.createMutationFunction(removeAccountFromGroupMutation); @@ -110,12 +109,14 @@ registerComponent("AccountsDashboard", AccountsDashboardContainer, [ withIsAdmin, withOpaqueShopId, withGroups, - withAccounts + withAccounts, + withRoles ]); export default compose( withIsAdmin, withOpaqueShopId, withGroups, - withAccounts + withAccounts, + withRoles )(AccountsDashboardContainer); diff --git a/imports/plugins/core/accounts/client/hocs/withRoles.js b/imports/plugins/core/accounts/client/hocs/withRoles.js new file mode 100644 index 0000000000..0f8d7a8141 --- /dev/null +++ b/imports/plugins/core/accounts/client/hocs/withRoles.js @@ -0,0 +1,51 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { Query } from "react-apollo"; +import gql from "graphql-tag"; + +const roles = gql` + query ($shopId: ID!) { + roles(shopId: $shopId){ + nodes { + name + } + } + } +`; + +export default (Component) => ( + class RolesQuery extends React.Component { + static propTypes = { + shopId: PropTypes.array + }; + + render() { + const { shopId } = this.props; + + if (!shopId) { + return ( + + ); + } + + return ( + + {({ loading, data }) => { + const props = { + ...this.props, + isLoadingRoles: loading + }; + + if (!loading && data) { + props.roles = data.roles.nodes; + } + + return ( + + ); + }} + + ); + } + } +); From 17ce629e669d8e1138213161039c9a626b9cc0f8 Mon Sep 17 00:00:00 2001 From: Loan Laux Date: Mon, 20 Apr 2020 02:21:55 +0200 Subject: [PATCH 09/38] refactor: use hooks and stateless components for cards and tables Signed-off-by: Loan Laux --- .../client/components/AccountsTable.js | 160 ++++++++++++++++++ .../accounts/client/components/GroupCards.js | 86 ++++++++++ .../client/graphql/mutations/archiveGroups.js | 9 + .../client/graphql/mutations/cloneGroups.js | 9 + .../client/graphql/mutations/createGroup.js | 9 + .../client/graphql/mutations/updateGroup.js | 9 + .../client/graphql/queries/accounts.js | 35 ++++ .../accounts/client/graphql/queries/groups.js | 20 +++ .../core/accounts/client/hooks/useGroups.js | 59 +++++++ imports/plugins/core/accounts/client/index.js | 4 +- 10 files changed, 398 insertions(+), 2 deletions(-) create mode 100644 imports/plugins/core/accounts/client/components/AccountsTable.js create mode 100644 imports/plugins/core/accounts/client/components/GroupCards.js create mode 100644 imports/plugins/core/accounts/client/graphql/mutations/archiveGroups.js create mode 100644 imports/plugins/core/accounts/client/graphql/mutations/cloneGroups.js create mode 100644 imports/plugins/core/accounts/client/graphql/mutations/createGroup.js create mode 100644 imports/plugins/core/accounts/client/graphql/mutations/updateGroup.js create mode 100644 imports/plugins/core/accounts/client/graphql/queries/accounts.js create mode 100644 imports/plugins/core/accounts/client/graphql/queries/groups.js create mode 100644 imports/plugins/core/accounts/client/hooks/useGroups.js diff --git a/imports/plugins/core/accounts/client/components/AccountsTable.js b/imports/plugins/core/accounts/client/components/AccountsTable.js new file mode 100644 index 0000000000..f68897de80 --- /dev/null +++ b/imports/plugins/core/accounts/client/components/AccountsTable.js @@ -0,0 +1,160 @@ +import React, { Fragment, useState, useMemo, useCallback } from "react"; +import { useApolloClient, useMutation } from "@apollo/react-hooks"; +import i18next from "i18next"; +import { useHistory } from "react-router-dom"; +import DataTable, { useDataTable } from "@reactioncommerce/catalyst/DataTable"; +import { useSnackbar } from "notistack"; +import { makeStyles } from "@material-ui/core"; +import useCurrentShopId from "/imports/client/ui/hooks/useCurrentShopId"; +import accountsQuery from "../graphql/queries/accounts"; +import archiveGroups from "../graphql/mutations/archiveGroups"; +import updateGroup from "../graphql/mutations/updateGroup"; +import cloneGroups from "../graphql/mutations/cloneGroups"; +import createGroupMutation from "../graphql/mutations/createGroup"; + +const useStyles = makeStyles((theme) => ({ + card: { + overflow: "visible" + }, + cardHeader: { + paddingBottom: 0 + }, + selectedAccount: { + fontWeight: 400, + marginLeft: theme.spacing(1) + } +})); + +/** + * @summary Main products view + * @name ProductsTable + * @returns {React.Component} A React component + */ +function AccountsTable(props) { + const apolloClient = useApolloClient(); + const { enqueueSnackbar } = useSnackbar(); + const history = useHistory(); + const [shopId] = useCurrentShopId(); + const [createGroup, { error: createProductError }] = useMutation(createGroupMutation); + + // React-Table state + const [isLoading, setIsLoading] = useState(false); + const [pageCount, setPageCount] = useState(1); + const [selectedRows, setSelectedRows] = useState([]); + const [tableData, setTableData] = useState([]); + + // Tag selector state + const [isTagSelectorVisible, setTagSelectorVisibility] = useState(false); + + // Create and memoize the column data + const columns = useMemo(() => [ + { + Header: "", + accessor: (row) => { + const email = row.emailRecords[0].address; + + return email; + }, + id: "profilePicture" + }, { + Header: i18next.t("admin.accountsTable.header.email"), + accessor: (row) => { + const email = row.emailRecords[0].address; + + return email; + }, + id: "email" + }, { + Header: i18next.t("admin.accountsTable.header.name"), + accessor: "name" + } + ], []); + + + const onFetchData = useCallback(async ({ globalFilter, manualFilters, pageIndex, pageSize }) => { + // Wait for shop id to be available before fetching products. + setIsLoading(true); + if (!shopId) { + return; + } + + const { data } = await apolloClient.query({ + query: accountsQuery, + variables: { + shopId, + groupIds: [props.group._id], + query: globalFilter, + first: pageSize, + limit: (pageIndex + 1) * pageSize, + offset: pageIndex * pageSize + }, + fetchPolicy: "network-only" + }); + + // Update the state with the fetched data as an array of objects and the calculated page count + setTableData(data.accounts.nodes); + setPageCount(Math.ceil(data.accounts.totalCount / pageSize)); + + setIsLoading(false); + }, [apolloClient, shopId, props.group._id]); + + const onRowSelect = useCallback(async ({ selectedRows: rows }) => { + setSelectedRows(rows || []); + }, []); + + const labels = useMemo(() => ({ + globalFilterPlaceholder: i18next.t("admin.productTable.filters.placeholder") + }), []); + + const dataTableProps = useDataTable({ + columns, + data: tableData, + labels, + pageCount, + onFetchData, + onRowSelect, + getRowId: (row) => row._id + }); + + const { refetch } = dataTableProps; + + const handleCreateGroup = async () => { + const { data } = await createGroup({ variables: { input: { shopId } } }); + + if (data) { + const { createGroup: { product } } = data; + history.push(`/products/${product._id}`); + } + + if (createProductError) { + enqueueSnackbar(i18next.t("admin.productTable.bulkActions.error", { variant: "error" })); + } + }; + + // Create options for the built-in ActionMenu in the DataTable + const options = useMemo(() => [{ + label: i18next.t("admin.productTable.bulkActions.addRemoveTags"), + isDisabled: selectedRows.length === 0, + onClick: () => { + setTagSelectorVisibility(true); + } + }], [apolloClient, enqueueSnackbar, refetch, selectedRows, shopId]); + + const classes = useStyles(); + const selectedAccount = selectedRows.length ? `${selectedRows.length} selected` : ""; + const cardTitle = ( + + {i18next.t("admin.accounts")}{selectedAccount} + + ); + + return ( + + ); +} + +export default AccountsTable; diff --git a/imports/plugins/core/accounts/client/components/GroupCards.js b/imports/plugins/core/accounts/client/components/GroupCards.js new file mode 100644 index 0000000000..599b01457b --- /dev/null +++ b/imports/plugins/core/accounts/client/components/GroupCards.js @@ -0,0 +1,86 @@ +import React, { useState } from "react"; +import { useMutation, useQuery } from "@apollo/react-hooks"; +import i18next from "i18next"; +import Button from "@reactioncommerce/catalyst/Button"; +import { useSnackbar } from "notistack"; +import { Card, CardHeader, CardContent, Grid, makeStyles } from "@material-ui/core"; +import { Components } from "@reactioncommerce/reaction-components"; +import useCurrentShopId from "/imports/client/ui/hooks/useCurrentShopId"; +import groupsQuery from "../graphql/queries/groups"; +import archiveGroups from "../graphql/mutations/archiveGroups"; +import updateGroup from "../graphql/mutations/updateGroup"; +import cloneGroups from "../graphql/mutations/cloneGroups"; +import createGroupMutation from "../graphql/mutations/createGroup"; +import AccountsTable from "./AccountsTable"; + +const useStyles = makeStyles((theme) => ({ + card: { + overflow: "visible" + }, + cardHeader: { + paddingBottom: 0 + }, + selectedProducts: { + fontWeight: 400, + marginLeft: theme.spacing(1) + } +})); + +/** + * @summary Main products view + * @name ProductsTable + * @returns {React.Component} A React component + */ +function GroupCards() { + const { enqueueSnackbar } = useSnackbar(); + const [shopId] = useCurrentShopId(); + const [createGroup, { error: createGroupError }] = useMutation(createGroupMutation); + const classes = useStyles(); + + const { data: groupData, loading, refetch: refetchGroups } = useQuery(groupsQuery, { + variables: { + shopId + }, + fetchPolicy: "network-only" + }); + + let groups; + + if (groupData && groupData.groups && groupData.groups.nodes) { + groups = groupData.groups.nodes; + } + + const handleCreateGroup = async () => { + const { data } = await createGroup({ variables: { input: { shopId } } }); + + if (data) { + refetchGroups(); + } + + if (createGroupError) { + enqueueSnackbar(i18next.t("admin.productTable.bulkActions.error", { variant: "error" })); + } + }; + + return ( + + + + + + {!shopId || !groups || loading ? : groups.map((group) => ( + + + + + + + ))} + + + ); +} + +export default GroupCards; diff --git a/imports/plugins/core/accounts/client/graphql/mutations/archiveGroups.js b/imports/plugins/core/accounts/client/graphql/mutations/archiveGroups.js new file mode 100644 index 0000000000..f69c6b1910 --- /dev/null +++ b/imports/plugins/core/accounts/client/graphql/mutations/archiveGroups.js @@ -0,0 +1,9 @@ +import gql from "graphql-tag"; + +export default gql` + mutation createAccountGroup($input: CreateAccountGroupInput!) { + createAccountGroup(input: $input) { + group + } + } +`; diff --git a/imports/plugins/core/accounts/client/graphql/mutations/cloneGroups.js b/imports/plugins/core/accounts/client/graphql/mutations/cloneGroups.js new file mode 100644 index 0000000000..f69c6b1910 --- /dev/null +++ b/imports/plugins/core/accounts/client/graphql/mutations/cloneGroups.js @@ -0,0 +1,9 @@ +import gql from "graphql-tag"; + +export default gql` + mutation createAccountGroup($input: CreateAccountGroupInput!) { + createAccountGroup(input: $input) { + group + } + } +`; diff --git a/imports/plugins/core/accounts/client/graphql/mutations/createGroup.js b/imports/plugins/core/accounts/client/graphql/mutations/createGroup.js new file mode 100644 index 0000000000..f69c6b1910 --- /dev/null +++ b/imports/plugins/core/accounts/client/graphql/mutations/createGroup.js @@ -0,0 +1,9 @@ +import gql from "graphql-tag"; + +export default gql` + mutation createAccountGroup($input: CreateAccountGroupInput!) { + createAccountGroup(input: $input) { + group + } + } +`; diff --git a/imports/plugins/core/accounts/client/graphql/mutations/updateGroup.js b/imports/plugins/core/accounts/client/graphql/mutations/updateGroup.js new file mode 100644 index 0000000000..f69c6b1910 --- /dev/null +++ b/imports/plugins/core/accounts/client/graphql/mutations/updateGroup.js @@ -0,0 +1,9 @@ +import gql from "graphql-tag"; + +export default gql` + mutation createAccountGroup($input: CreateAccountGroupInput!) { + createAccountGroup(input: $input) { + group + } + } +`; diff --git a/imports/plugins/core/accounts/client/graphql/queries/accounts.js b/imports/plugins/core/accounts/client/graphql/queries/accounts.js new file mode 100644 index 0000000000..a527591d94 --- /dev/null +++ b/imports/plugins/core/accounts/client/graphql/queries/accounts.js @@ -0,0 +1,35 @@ +import gql from "graphql-tag"; + +export default gql` + query accounts( + $groupIds: [ID], + $first: ConnectionLimitInt, + $last: ConnectionLimitInt, + $before: ConnectionCursor, + $after: ConnectionCursor, + $offset: Int, + $sortBy: AccountSortByField, + $sortOrder: SortOrder + ) { + accounts( + groupIds: $groupIds, + first: $first, + last: $last, + before: $before, + after: $after, + offset: $offset, + sortBy: $sortBy, + sortOrder: $sortOrder + ) { + nodes { + _id + emailRecords { + address + verified + } + name + } + totalCount + } + } +`; diff --git a/imports/plugins/core/accounts/client/graphql/queries/groups.js b/imports/plugins/core/accounts/client/graphql/queries/groups.js new file mode 100644 index 0000000000..79a7bac479 --- /dev/null +++ b/imports/plugins/core/accounts/client/graphql/queries/groups.js @@ -0,0 +1,20 @@ +import gql from "graphql-tag"; + +export default gql` + query getGroups($shopId: ID!) { + groups(shopId: $shopId) { + nodes { + _id + createdAt + createdBy { + _id + } + description + name + permissions + slug + updatedAt + } + } + } +`; diff --git a/imports/plugins/core/accounts/client/hooks/useGroups.js b/imports/plugins/core/accounts/client/hooks/useGroups.js new file mode 100644 index 0000000000..7093aeebe9 --- /dev/null +++ b/imports/plugins/core/accounts/client/hooks/useGroups.js @@ -0,0 +1,59 @@ +import {Meteor} from "meteor/meteor"; + +/** + * Hook to get groups + * @return {Object} Permissions + */ +export default function useGroups() { + const history = useHistory(); + + // This is admittedly not ideal, but the `@axa-fr/react-oidc-context` pkg uses `window.history.pushState` + // directly when we finish the OIDC login flow, and for whatever reason React Router DOM does not pick it + // up. This workaround seems to work reliably: we call React Router's `history.push` with the same URL + // we are already on, and it forces a reload. + if (history && lastLocationChangeUrl) { + history.push(lastLocationChangeUrl); + lastLocationChangeUrl = null; + } + + const { logout: oidcLogout, oidcUser } = useReactOidc(); + + const { access_token: accessToken } = oidcUser || {}; + setAccessToken(accessToken); + + const [getViewer, { + data: viewerData + }] = useLazyQuery( + viewerQuery, + { + fetchPolicy: "network-only", + notifyOnNetworkStatusChange: true, + onError(error) { + // Can't find any more reliable way to check the status code from within this hook + if (typeof error.message === "string" && error.message.includes("Received status code 401")) { + // Token is expired or user was deleted from database + oidcLogout(); + } else { + Logger.error(error); + } + } + } + ); + + // Perform a `viewer` query whenever we get a new access token + useEffect(() => { + if (accessToken) getViewer(); + }, [accessToken, getViewer]); + + const logout = () => { + Meteor.logout(() => { + // This involves redirect, so the page will full refresh at this point + oidcLogout(); + }); + }; + + return { + logout, + viewer: viewerData ? viewerData.viewer : null + }; +} diff --git a/imports/plugins/core/accounts/client/index.js b/imports/plugins/core/accounts/client/index.js index 8d117bf7c1..dc6f60a3a2 100644 --- a/imports/plugins/core/accounts/client/index.js +++ b/imports/plugins/core/accounts/client/index.js @@ -2,7 +2,7 @@ import React from "react"; import AccountIcon from "mdi-material-ui/AccountMultiple"; import { registerOperatorRoute } from "/imports/client/ui"; -import Accounts from "./containers/accountsDashboardContainer"; +import GroupCards from "./components/GroupCards"; export { default as AccountsDashboard } from "./components/accountsDashboard"; export { default as AdminInviteForm } from "./components/adminInviteForm"; @@ -27,7 +27,7 @@ registerOperatorRoute({ group: "navigation", path: "/accounts", priority: 40, - MainComponent: Accounts, + MainComponent: GroupCards, // eslint-disable-next-line react/display-name, react/no-multi-comp SidebarIconComponent: (props) => , sidebarI18nLabel: "admin.dashboard.accountsLabel" From 9623c725b45f5f1a8de723a0c3bc85209e566e62 Mon Sep 17 00:00:00 2001 From: Loan Laux Date: Mon, 20 Apr 2020 02:29:14 +0200 Subject: [PATCH 10/38] feat: refactor getUserAvatar helper and use it in accounts table Signed-off-by: Loan Laux --- .../client/components/AccountsTable.js | 7 +++---- .../core/accounts/client/helpers/helpers.js | 20 ++++++++----------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/imports/plugins/core/accounts/client/components/AccountsTable.js b/imports/plugins/core/accounts/client/components/AccountsTable.js index f68897de80..8c8d9e6b87 100644 --- a/imports/plugins/core/accounts/client/components/AccountsTable.js +++ b/imports/plugins/core/accounts/client/components/AccountsTable.js @@ -6,6 +6,7 @@ import DataTable, { useDataTable } from "@reactioncommerce/catalyst/DataTable"; import { useSnackbar } from "notistack"; import { makeStyles } from "@material-ui/core"; import useCurrentShopId from "/imports/client/ui/hooks/useCurrentShopId"; +import { getAccountAvatar } from "/imports/plugins/core/accounts/client/helpers/helpers"; import accountsQuery from "../graphql/queries/accounts"; import archiveGroups from "../graphql/mutations/archiveGroups"; import updateGroup from "../graphql/mutations/updateGroup"; @@ -50,10 +51,8 @@ function AccountsTable(props) { const columns = useMemo(() => [ { Header: "", - accessor: (row) => { - const email = row.emailRecords[0].address; - - return email; + accessor: (account) => { + return getAccountAvatar(account); }, id: "profilePicture" }, { diff --git a/imports/plugins/core/accounts/client/helpers/helpers.js b/imports/plugins/core/accounts/client/helpers/helpers.js index 5fd838ddbc..a6c7fa16e2 100644 --- a/imports/plugins/core/accounts/client/helpers/helpers.js +++ b/imports/plugins/core/accounts/client/helpers/helpers.js @@ -1,20 +1,15 @@ import React from "react"; -import { Accounts } from "meteor/accounts-base"; -import * as Collections from "/lib/collections"; import { Components } from "@reactioncommerce/reaction-components"; /** - * @method getUserAvatar + * @method getAccountAvatar * @memberof Accounts - * @summary ReactionAvatar Component helper to get a user's Avatar - * @example const userAvatar = getUserAvatar(account); - * @param {Object} currentUser User + * @summary ReactionAvatar Component helper to get an account's Avatar + * @example const accountAvatar = getAccountAvatar(account); + * @param {Object} account The account to render the avatar for * @returns {Component} ReactionAvatar component */ -export function getUserAvatar(currentUser) { - const user = currentUser || Accounts.user(); - - const account = Collections.Accounts.findOne(user._id); +export function getAccountAvatar(account) { // first we check picture exists. Picture has higher priority to display if (account && account.profile && account.profile.picture) { const { picture } = account.profile; @@ -27,8 +22,9 @@ export function getUserAvatar(currentUser) { /> ); } - if (user.emails && user.emails.length === 1) { - const email = user.emails[0].address; + + if (Array.isArray(account.emailRecords) && account.emailRecords.length >= 1) { + const email = account.emailRecords[0].address; return ( Date: Mon, 20 Apr 2020 02:59:10 +0200 Subject: [PATCH 11/38] chore: set isFilterable to false on account tables Signed-off-by: Loan Laux --- .../client/components/AccountsTable.js | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/imports/plugins/core/accounts/client/components/AccountsTable.js b/imports/plugins/core/accounts/client/components/AccountsTable.js index 8c8d9e6b87..cc8a2ffc3e 100644 --- a/imports/plugins/core/accounts/client/components/AccountsTable.js +++ b/imports/plugins/core/accounts/client/components/AccountsTable.js @@ -1,4 +1,4 @@ -import React, { Fragment, useState, useMemo, useCallback } from "react"; +import React, { useState, useMemo, useCallback } from "react"; import { useApolloClient, useMutation } from "@apollo/react-hooks"; import i18next from "i18next"; import { useHistory } from "react-router-dom"; @@ -69,8 +69,7 @@ function AccountsTable(props) { } ], []); - - const onFetchData = useCallback(async ({ globalFilter, manualFilters, pageIndex, pageSize }) => { + const onFetchData = useCallback(async ({ pageIndex, pageSize }) => { // Wait for shop id to be available before fetching products. setIsLoading(true); if (!shopId) { @@ -80,9 +79,7 @@ function AccountsTable(props) { const { data } = await apolloClient.query({ query: accountsQuery, variables: { - shopId, groupIds: [props.group._id], - query: globalFilter, first: pageSize, limit: (pageIndex + 1) * pageSize, offset: pageIndex * pageSize @@ -101,14 +98,10 @@ function AccountsTable(props) { setSelectedRows(rows || []); }, []); - const labels = useMemo(() => ({ - globalFilterPlaceholder: i18next.t("admin.productTable.filters.placeholder") - }), []); - const dataTableProps = useDataTable({ columns, data: tableData, - labels, + isFilterable: false, pageCount, onFetchData, onRowSelect, @@ -126,26 +119,18 @@ function AccountsTable(props) { } if (createProductError) { - enqueueSnackbar(i18next.t("admin.productTable.bulkActions.error", { variant: "error" })); + enqueueSnackbar(i18next.t("admin.accountsTable.bulkActions.error", { variant: "error" })); } }; // Create options for the built-in ActionMenu in the DataTable const options = useMemo(() => [{ - label: i18next.t("admin.productTable.bulkActions.addRemoveTags"), + label: i18next.t("admin.accountsTable.bulkActions.addRemoveGroups"), isDisabled: selectedRows.length === 0, onClick: () => { setTagSelectorVisibility(true); } - }], [apolloClient, enqueueSnackbar, refetch, selectedRows, shopId]); - - const classes = useStyles(); - const selectedAccount = selectedRows.length ? `${selectedRows.length} selected` : ""; - const cardTitle = ( - - {i18next.t("admin.accounts")}{selectedAccount} - - ); + }], [selectedRows]); return ( Date: Thu, 23 Apr 2020 17:09:35 +0200 Subject: [PATCH 12/38] feat: introduce group selector modal Signed-off-by: Loan Laux --- .../client/components/AccountsTable.js | 49 +++-- .../accounts/client/components/GroupCards.js | 16 +- .../components/GroupSelector/GroupSelector.js | 192 +++++++++++++++++ .../GroupSelector/GroupSelectorDialog.js | 195 ++++++++++++++++++ .../components/GroupSelector/helpers.js | 27 +++ .../client/components/GroupSelector/index.js | 1 + .../components/GroupSelector/mutations.js | 29 +++ .../components/GroupSelector/queries.js | 20 ++ .../client/graphql/queries/accounts.js | 1 + .../core/accounts/client/hooks/useGroups.js | 60 ++---- 10 files changed, 515 insertions(+), 75 deletions(-) create mode 100644 imports/plugins/core/accounts/client/components/GroupSelector/GroupSelector.js create mode 100644 imports/plugins/core/accounts/client/components/GroupSelector/GroupSelectorDialog.js create mode 100644 imports/plugins/core/accounts/client/components/GroupSelector/helpers.js create mode 100644 imports/plugins/core/accounts/client/components/GroupSelector/index.js create mode 100644 imports/plugins/core/accounts/client/components/GroupSelector/mutations.js create mode 100644 imports/plugins/core/accounts/client/components/GroupSelector/queries.js diff --git a/imports/plugins/core/accounts/client/components/AccountsTable.js b/imports/plugins/core/accounts/client/components/AccountsTable.js index cc8a2ffc3e..2462e2fc43 100644 --- a/imports/plugins/core/accounts/client/components/AccountsTable.js +++ b/imports/plugins/core/accounts/client/components/AccountsTable.js @@ -1,4 +1,4 @@ -import React, { useState, useMemo, useCallback } from "react"; +import React, { Fragment, useState, useMemo, useCallback } from "react"; import { useApolloClient, useMutation } from "@apollo/react-hooks"; import i18next from "i18next"; import { useHistory } from "react-router-dom"; @@ -12,6 +12,8 @@ import archiveGroups from "../graphql/mutations/archiveGroups"; import updateGroup from "../graphql/mutations/updateGroup"; import cloneGroups from "../graphql/mutations/cloneGroups"; import createGroupMutation from "../graphql/mutations/createGroup"; +import GroupSelectorDialog from "./GroupSelector/GroupSelectorDialog"; +import useGroups from "../hooks/useGroups"; const useStyles = makeStyles((theme) => ({ card: { @@ -37,6 +39,7 @@ function AccountsTable(props) { const history = useHistory(); const [shopId] = useCurrentShopId(); const [createGroup, { error: createProductError }] = useMutation(createGroupMutation); + const { isLoadingGroups, groups } = useGroups(shopId); // React-Table state const [isLoading, setIsLoading] = useState(false); @@ -45,15 +48,13 @@ function AccountsTable(props) { const [tableData, setTableData] = useState([]); // Tag selector state - const [isTagSelectorVisible, setTagSelectorVisibility] = useState(false); + const [isGroupSelectorVisible, setGroupSelectorVisibility] = useState(false); // Create and memoize the column data const columns = useMemo(() => [ { Header: "", - accessor: (account) => { - return getAccountAvatar(account); - }, + accessor: (account) => getAccountAvatar(account), id: "profilePicture" }, { Header: i18next.t("admin.accountsTable.header.email"), @@ -110,34 +111,32 @@ function AccountsTable(props) { const { refetch } = dataTableProps; - const handleCreateGroup = async () => { - const { data } = await createGroup({ variables: { input: { shopId } } }); - - if (data) { - const { createGroup: { product } } = data; - history.push(`/products/${product._id}`); - } - - if (createProductError) { - enqueueSnackbar(i18next.t("admin.accountsTable.bulkActions.error", { variant: "error" })); - } - }; - // Create options for the built-in ActionMenu in the DataTable const options = useMemo(() => [{ - label: i18next.t("admin.accountsTable.bulkActions.addRemoveGroups"), + label: i18next.t("admin.accountsTable.bulkActions.addRemoveGroupsFromAccount"), isDisabled: selectedRows.length === 0, onClick: () => { - setTagSelectorVisibility(true); + setGroupSelectorVisibility(true); } }], [selectedRows]); return ( - + + {selectedRows && !isLoadingGroups && + null} + onClose={() => setGroupSelectorVisibility(false)} + accounts={tableData.filter((account) => selectedRows.includes(account._id))} + groups={groups} + /> + } + + ); } diff --git a/imports/plugins/core/accounts/client/components/GroupCards.js b/imports/plugins/core/accounts/client/components/GroupCards.js index 599b01457b..70ccf5c424 100644 --- a/imports/plugins/core/accounts/client/components/GroupCards.js +++ b/imports/plugins/core/accounts/client/components/GroupCards.js @@ -1,9 +1,11 @@ import React, { useState } from "react"; +import classNames from "classnames"; import { useMutation, useQuery } from "@apollo/react-hooks"; import i18next from "i18next"; import Button from "@reactioncommerce/catalyst/Button"; import { useSnackbar } from "notistack"; -import { Card, CardHeader, CardContent, Grid, makeStyles } from "@material-ui/core"; +import { Card, CardHeader, CardContent, Collapse, Grid, IconButton, makeStyles } from "@material-ui/core"; +import ExpandMoreIcon from "mdi-material-ui/ChevronUp"; import { Components } from "@reactioncommerce/reaction-components"; import useCurrentShopId from "/imports/client/ui/hooks/useCurrentShopId"; import groupsQuery from "../graphql/queries/groups"; @@ -18,11 +20,21 @@ const useStyles = makeStyles((theme) => ({ overflow: "visible" }, cardHeader: { - paddingBottom: 0 + // paddingBottom: 0 }, selectedProducts: { fontWeight: 400, marginLeft: theme.spacing(1) + }, + expand: { + transform: 'rotate(0deg)', + marginLeft: 'auto', + transition: theme.transitions.create('transform', { + duration: theme.transitions.duration.shortest, + }) + }, + expandOpen: { + transform: 'rotate(180deg)', } })); diff --git a/imports/plugins/core/accounts/client/components/GroupSelector/GroupSelector.js b/imports/plugins/core/accounts/client/components/GroupSelector/GroupSelector.js new file mode 100644 index 0000000000..9bf8ccece1 --- /dev/null +++ b/imports/plugins/core/accounts/client/components/GroupSelector/GroupSelector.js @@ -0,0 +1,192 @@ +import React, { Fragment, useState } from "react"; +import PropTypes from "prop-types"; +import i18next from "i18next"; +import CloseIcon from "mdi-material-ui/Close"; +import Select from "@reactioncommerce/catalyst/Select"; +import SplitButton from "@reactioncommerce/catalyst/SplitButton"; +import { useApolloClient } from "@apollo/react-hooks"; +import { useSnackbar } from "notistack"; +import { + Grid, + Card as MuiCard, + CardActions, + CardHeader, + CardContent, + Zoom, + IconButton, + makeStyles +} from "@material-ui/core"; +import getTranslation from "../../utils/getTranslation"; +import { getTags } from "./helpers"; +import { ADD_TAGS_TO_PRODUCTS, REMOVE_TAGS_FROM_PRODUCTS } from "./mutations"; + +const ACTION_OPTIONS = [{ + label: "Add tags to products", + type: "ADD" +}, { + label: "Remove tags from products", + isDestructive: true, + type: "REMOVE" +}]; + +const useStyles = makeStyles((theme) => ({ + cardRoot: { + overflow: "visible", + padding: theme.spacing(2) + }, + cardContainer: { + alignItems: "center" + }, + cardActions: { + padding: theme.spacing(2), + justifyContent: "flex-end" + }, + hidden: { + display: "none" + }, + visible: { + display: "block" + } +})); + +/** + * TagSelector component + * @param {Object} props Component props + * @returns {React.Component} A React component + */ +function GroupSelector({ isVisible, selectedProductIds, setVisibility, shopId }) { + const apolloClient = useApolloClient(); + const [selectedTags, setSelectedTags] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const classes = useStyles(); + const { enqueueSnackbar } = useSnackbar(); + + // eslint-disable-next-line consistent-return + const handleTagsAction = async (option) => { + const tagIds = selectedTags && selectedTags.map(({ value }) => (value)); + const tags = selectedTags && selectedTags.map(({ label }) => (label)).join(", "); + + // Prevent user from executing action if he/she has not // yet selected at least one tag + if (!tagIds.length) { + return enqueueSnackbar(i18next.t("admin.addRemoveTags.invalidSelection"), { variant: "warning" }); + } + + let mutationName; + let data; let loading; let error; + setIsLoading(true); + switch (option.type) { + case "ADD": + mutationName = "addTagsToProducts"; + ({ data, loading, error } = await apolloClient.mutate({ + mutation: ADD_TAGS_TO_PRODUCTS, + variables: { + input: { + productIds: selectedProductIds, + shopId, + tagIds + } + } + })); + + setIsLoading(loading); + break; + case "REMOVE": + mutationName = "removeTagsFromProducts"; + ({ data, loading, error } = await apolloClient.mutate({ + mutation: REMOVE_TAGS_FROM_PRODUCTS, + variables: { + input: { + productIds: selectedProductIds, + shopId, + tagIds + } + } + })); + + setIsLoading(loading); + break; + + default: + break; + } + + if (data && data[mutationName]) { + // Notify user of performed action + if (mutationName.startsWith("add")) { + enqueueSnackbar(getTranslation( + "admin.addRemoveTags.addConfirmation", + { count: selectedProductIds.length, tags } + )); + } else { + enqueueSnackbar(getTranslation( + "admin.addRemoveTags.removeConfirmation", + { count: selectedProductIds.length, tags } + )); + } + } + + if (error) { + enqueueSnackbar(i18next.t("admin.addRemoveTags.errorMessage"), { variant: "error" }); + } + + setVisibility(false); + setSelectedTags([]); + }; + + return ( + + {isVisible && + + + + setVisibility(false)}> + + + } + title={i18next.t("admin.productTable.bulkActions.addRemoveTags")} + /> + + + + setSelectedTags(tags)} + placeholder={i18next.t("admin.addRemoveGroupsFromAccount.inputPlaceholder")} + /> + + + + + + + + + + + ); +} + +GroupSelector.propTypes = { + isOpen: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + onSuccess: PropTypes.func, + accounts: PropTypes.arrayOf(PropTypes.object).isRequired, + groups: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default GroupSelector; diff --git a/imports/plugins/core/accounts/client/components/GroupSelector/helpers.js b/imports/plugins/core/accounts/client/components/GroupSelector/helpers.js new file mode 100644 index 0000000000..ab68220868 --- /dev/null +++ b/imports/plugins/core/accounts/client/components/GroupSelector/helpers.js @@ -0,0 +1,27 @@ +import { GET_PRIMARY_SHOP_ID, GET_GROUPS } from "./queries"; + +/** + * @summary Queries for tags the match the provided user query + * @param {Object} apolloClient The apolloClient + * @param {String} query Query provided by the user + * @returns {Array} An array of options formatted for use with react-select + */ +export async function getGroups(apolloClient, query) { + const { data, error } = await apolloClient.query({ + query: GET_GROUPS, + variables: { + shopId: primaryShopId, + filter: query + } + }); + + let options = []; + if (!error && data) { + options = data.tags.edges.map(({ node }) => ({ + label: node.name, + value: node._id + })); + } + + return options; +} diff --git a/imports/plugins/core/accounts/client/components/GroupSelector/index.js b/imports/plugins/core/accounts/client/components/GroupSelector/index.js new file mode 100644 index 0000000000..4d72264503 --- /dev/null +++ b/imports/plugins/core/accounts/client/components/GroupSelector/index.js @@ -0,0 +1 @@ +export { default } from "./GroupSelector"; diff --git a/imports/plugins/core/accounts/client/components/GroupSelector/mutations.js b/imports/plugins/core/accounts/client/components/GroupSelector/mutations.js new file mode 100644 index 0000000000..f5b3558546 --- /dev/null +++ b/imports/plugins/core/accounts/client/components/GroupSelector/mutations.js @@ -0,0 +1,29 @@ +import gql from "graphql-tag"; + +export const ADD_TAGS_TO_PRODUCTS = gql` + mutation addTagsToProducts($input: ProductTagsOperationInput!) { + addTagsToProducts(input: $input) { + foundCount + notFoundCount + updatedCount + writeErrors { + documentId + errorMsg + } + } + } +`; + +export const REMOVE_TAGS_FROM_PRODUCTS = gql` + mutation removeTagsFromProducts($input: ProductTagsOperationInput!) { + removeTagsFromProducts(input: $input) { + foundCount + notFoundCount + updatedCount + writeErrors { + documentId + errorMsg + } + } + } +`; diff --git a/imports/plugins/core/accounts/client/components/GroupSelector/queries.js b/imports/plugins/core/accounts/client/components/GroupSelector/queries.js new file mode 100644 index 0000000000..3c9c14151b --- /dev/null +++ b/imports/plugins/core/accounts/client/components/GroupSelector/queries.js @@ -0,0 +1,20 @@ +import gql from "graphql-tag"; + +export const GET_PRIMARY_SHOP_ID = gql` + query getPrimaryShopId { + primaryShopId + } +`; + +export const GET_TAGS = gql` + query Tags($shopId: ID!, $filter: String) { + tags(shopId: $shopId, filter: $filter) { + edges { + node { + _id + name + } + } + } + } +`; diff --git a/imports/plugins/core/accounts/client/graphql/queries/accounts.js b/imports/plugins/core/accounts/client/graphql/queries/accounts.js index a527591d94..b0f039d224 100644 --- a/imports/plugins/core/accounts/client/graphql/queries/accounts.js +++ b/imports/plugins/core/accounts/client/graphql/queries/accounts.js @@ -27,6 +27,7 @@ export default gql` address verified } + groups name } totalCount diff --git a/imports/plugins/core/accounts/client/hooks/useGroups.js b/imports/plugins/core/accounts/client/hooks/useGroups.js index 7093aeebe9..83e1c04a15 100644 --- a/imports/plugins/core/accounts/client/hooks/useGroups.js +++ b/imports/plugins/core/accounts/client/hooks/useGroups.js @@ -1,59 +1,23 @@ -import {Meteor} from "meteor/meteor"; +import { useLazyQuery } from "@apollo/react-hooks"; +import groupsQuery from "../graphql/queries/groups"; /** * Hook to get groups * @return {Object} Permissions */ -export default function useGroups() { - const history = useHistory(); +export default function useGroups(shopId) { + const [getGroups, { called, data, loading, refetch }] = useLazyQuery(groupsQuery); - // This is admittedly not ideal, but the `@axa-fr/react-oidc-context` pkg uses `window.history.pushState` - // directly when we finish the OIDC login flow, and for whatever reason React Router DOM does not pick it - // up. This workaround seems to work reliably: we call React Router's `history.push` with the same URL - // we are already on, and it forces a reload. - if (history && lastLocationChangeUrl) { - history.push(lastLocationChangeUrl); - lastLocationChangeUrl = null; - } - - const { logout: oidcLogout, oidcUser } = useReactOidc(); - - const { access_token: accessToken } = oidcUser || {}; - setAccessToken(accessToken); - - const [getViewer, { - data: viewerData - }] = useLazyQuery( - viewerQuery, - { - fetchPolicy: "network-only", - notifyOnNetworkStatusChange: true, - onError(error) { - // Can't find any more reliable way to check the status code from within this hook - if (typeof error.message === "string" && error.message.includes("Received status code 401")) { - // Token is expired or user was deleted from database - oidcLogout(); - } else { - Logger.error(error); - } - } - } - ); - - // Perform a `viewer` query whenever we get a new access token - useEffect(() => { - if (accessToken) getViewer(); - }, [accessToken, getViewer]); - - const logout = () => { - Meteor.logout(() => { - // This involves redirect, so the page will full refresh at this point - oidcLogout(); + // Wait until we're sure we have a shop ID to call the query + if (shopId && !called) { + getGroups({ + variables: { shopId } }); - }; + } return { - logout, - viewer: viewerData ? viewerData.viewer : null + isLoadingGroups: loading || !called, + refetchGroups: refetch, + groups: (data && data.groups.nodes) || [] }; } From f0e9775a1dc0cb6c3c25da56b827e0e770e6605f Mon Sep 17 00:00:00 2001 From: Loan Laux Date: Fri, 24 Apr 2020 17:32:38 +0200 Subject: [PATCH 13/38] fix: populate groups in GroupSelectorDialog Signed-off-by: Loan Laux --- .../GroupSelector/GroupSelectorDialog.js | 23 ++++++++++--------- .../client/graphql/queries/accounts.js | 7 +++++- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/imports/plugins/core/accounts/client/components/GroupSelector/GroupSelectorDialog.js b/imports/plugins/core/accounts/client/components/GroupSelector/GroupSelectorDialog.js index ca78bffdbf..7ab7ba7f68 100644 --- a/imports/plugins/core/accounts/client/components/GroupSelector/GroupSelectorDialog.js +++ b/imports/plugins/core/accounts/client/components/GroupSelector/GroupSelectorDialog.js @@ -55,16 +55,14 @@ const useStyles = makeStyles((theme) => ({ */ function GroupSelector({ isOpen, onClose, onSuccess, accounts, groups }) { const apolloClient = useApolloClient(); - const [selectedTags, setSelectedTags] = useState([]); + const [selectedGroups, setSelectedGroups] = useState([]); const [isLoading, setIsLoading] = useState(false); const classes = useStyles(); const { enqueueSnackbar } = useSnackbar(); - console.log(accounts); - // eslint-disable-next-line consistent-return const handleTagsAction = async (option) => { - const tagIds = selectedTags && selectedTags.map(({ value }) => (value)); + const tagIds = selectedGroups && selectedGroups.map(({ value }) => (value)); // Prevent user from executing action if he/she has not // yet selected at least one tag if (!tagIds.length) { @@ -125,13 +123,18 @@ function GroupSelector({ isOpen, onClose, onSuccess, accounts, groups }) { } onClose(); - setSelectedTags([]); + setSelectedGroups([]); }; const groupsForSelect = groups.map((group) => ({ value: group._id, label: group.name })); - if (accounts && accounts.length === 1 && selectedTags && selectedTags.length === 0) { - setSelectedTags(accounts.groups); + // If modifying one single account, pre-select groups that the account already belongs to + if (Array.isArray(accounts) && accounts.length === 1 && + accounts[0].groups && Array.isArray(accounts[0].groups.nodes) && accounts[0].groups.nodes.length > 0 && + selectedGroups && selectedGroups.length === 0) { + + const preSelectedGroups = accounts[0].groups.nodes.map((group) => ({ value: group._id, label: group.name })); + setSelectedGroups(preSelectedGroups); } return ( @@ -154,13 +157,11 @@ function GroupSelector({ isOpen, onClose, onSuccess, accounts, groups }) { setSelectedRoles(roles)} + placeholder={i18next.t("admin.groupCards.createGroupDialog.selectRoles")} + value={selectedRoles} + /> + + + + + + + + + + + ); +} + +CreateGroup.propTypes = { + isOpen: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + onSuccess: PropTypes.func, + shopId: PropTypes.string +}; + +export default CreateGroup; diff --git a/imports/plugins/core/accounts/client/components/CreateGroup/mutations.js b/imports/plugins/core/accounts/client/components/CreateGroup/mutations.js new file mode 100644 index 0000000000..f5b3558546 --- /dev/null +++ b/imports/plugins/core/accounts/client/components/CreateGroup/mutations.js @@ -0,0 +1,29 @@ +import gql from "graphql-tag"; + +export const ADD_TAGS_TO_PRODUCTS = gql` + mutation addTagsToProducts($input: ProductTagsOperationInput!) { + addTagsToProducts(input: $input) { + foundCount + notFoundCount + updatedCount + writeErrors { + documentId + errorMsg + } + } + } +`; + +export const REMOVE_TAGS_FROM_PRODUCTS = gql` + mutation removeTagsFromProducts($input: ProductTagsOperationInput!) { + removeTagsFromProducts(input: $input) { + foundCount + notFoundCount + updatedCount + writeErrors { + documentId + errorMsg + } + } + } +`; diff --git a/imports/plugins/core/accounts/client/components/GroupCards.js b/imports/plugins/core/accounts/client/components/GroupCards.js index 70ccf5c424..9c3f88cb35 100644 --- a/imports/plugins/core/accounts/client/components/GroupCards.js +++ b/imports/plugins/core/accounts/client/components/GroupCards.js @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { Fragment, useState } from "react"; import classNames from "classnames"; import { useMutation, useQuery } from "@apollo/react-hooks"; import i18next from "i18next"; @@ -14,27 +14,12 @@ import updateGroup from "../graphql/mutations/updateGroup"; import cloneGroups from "../graphql/mutations/cloneGroups"; import createGroupMutation from "../graphql/mutations/createGroup"; import AccountsTable from "./AccountsTable"; +import CreateGroupDialog from "./CreateGroup/CreateGroupDialog"; +import useGroups from "../hooks/useGroups"; -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles(() => ({ card: { overflow: "visible" - }, - cardHeader: { - // paddingBottom: 0 - }, - selectedProducts: { - fontWeight: 400, - marginLeft: theme.spacing(1) - }, - expand: { - transform: 'rotate(0deg)', - marginLeft: 'auto', - transition: theme.transitions.create('transform', { - duration: theme.transitions.duration.shortest, - }) - }, - expandOpen: { - transform: 'rotate(180deg)', } })); @@ -48,21 +33,10 @@ function GroupCards() { const [shopId] = useCurrentShopId(); const [createGroup, { error: createGroupError }] = useMutation(createGroupMutation); const classes = useStyles(); + const [isCreateGroupDialogVisible, setCreateGroupDialogVisibility] = useState(false); + const { isLoadingGroups, groups, refetchGroups } = useGroups(shopId); - const { data: groupData, loading, refetch: refetchGroups } = useQuery(groupsQuery, { - variables: { - shopId - }, - fetchPolicy: "network-only" - }); - - let groups; - - if (groupData && groupData.groups && groupData.groups.nodes) { - groups = groupData.groups.nodes; - } - - const handleCreateGroup = async () => { + const handleShowCreateGroupModal = async () => { const { data } = await createGroup({ variables: { input: { shopId } } }); if (data) { @@ -75,23 +49,31 @@ function GroupCards() { }; return ( - - - - - - {!shopId || !groups || loading ? : groups.map((group) => ( - - - - - - - ))} + + setCreateGroupDialogVisibility(false)} + shopId={shopId} + /> + + + + + + {!shopId || !groups || isLoadingGroups ? : groups.map((group) => ( + + + + + + + ))} + - + ); } diff --git a/imports/plugins/core/accounts/client/components/GroupSelector/GroupSelector.js b/imports/plugins/core/accounts/client/components/GroupSelector/GroupSelector.js deleted file mode 100644 index 9bf8ccece1..0000000000 --- a/imports/plugins/core/accounts/client/components/GroupSelector/GroupSelector.js +++ /dev/null @@ -1,192 +0,0 @@ -import React, { Fragment, useState } from "react"; -import PropTypes from "prop-types"; -import i18next from "i18next"; -import CloseIcon from "mdi-material-ui/Close"; -import Select from "@reactioncommerce/catalyst/Select"; -import SplitButton from "@reactioncommerce/catalyst/SplitButton"; -import { useApolloClient } from "@apollo/react-hooks"; -import { useSnackbar } from "notistack"; -import { - Grid, - Card as MuiCard, - CardActions, - CardHeader, - CardContent, - Zoom, - IconButton, - makeStyles -} from "@material-ui/core"; -import getTranslation from "../../utils/getTranslation"; -import { getTags } from "./helpers"; -import { ADD_TAGS_TO_PRODUCTS, REMOVE_TAGS_FROM_PRODUCTS } from "./mutations"; - -const ACTION_OPTIONS = [{ - label: "Add tags to products", - type: "ADD" -}, { - label: "Remove tags from products", - isDestructive: true, - type: "REMOVE" -}]; - -const useStyles = makeStyles((theme) => ({ - cardRoot: { - overflow: "visible", - padding: theme.spacing(2) - }, - cardContainer: { - alignItems: "center" - }, - cardActions: { - padding: theme.spacing(2), - justifyContent: "flex-end" - }, - hidden: { - display: "none" - }, - visible: { - display: "block" - } -})); - -/** - * TagSelector component - * @param {Object} props Component props - * @returns {React.Component} A React component - */ -function GroupSelector({ isVisible, selectedProductIds, setVisibility, shopId }) { - const apolloClient = useApolloClient(); - const [selectedTags, setSelectedTags] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const classes = useStyles(); - const { enqueueSnackbar } = useSnackbar(); - - // eslint-disable-next-line consistent-return - const handleTagsAction = async (option) => { - const tagIds = selectedTags && selectedTags.map(({ value }) => (value)); - const tags = selectedTags && selectedTags.map(({ label }) => (label)).join(", "); - - // Prevent user from executing action if he/she has not // yet selected at least one tag - if (!tagIds.length) { - return enqueueSnackbar(i18next.t("admin.addRemoveTags.invalidSelection"), { variant: "warning" }); - } - - let mutationName; - let data; let loading; let error; - setIsLoading(true); - switch (option.type) { - case "ADD": - mutationName = "addTagsToProducts"; - ({ data, loading, error } = await apolloClient.mutate({ - mutation: ADD_TAGS_TO_PRODUCTS, - variables: { - input: { - productIds: selectedProductIds, - shopId, - tagIds - } - } - })); - - setIsLoading(loading); - break; - case "REMOVE": - mutationName = "removeTagsFromProducts"; - ({ data, loading, error } = await apolloClient.mutate({ - mutation: REMOVE_TAGS_FROM_PRODUCTS, - variables: { - input: { - productIds: selectedProductIds, - shopId, - tagIds - } - } - })); - - setIsLoading(loading); - break; - - default: - break; - } - - if (data && data[mutationName]) { - // Notify user of performed action - if (mutationName.startsWith("add")) { - enqueueSnackbar(getTranslation( - "admin.addRemoveTags.addConfirmation", - { count: selectedProductIds.length, tags } - )); - } else { - enqueueSnackbar(getTranslation( - "admin.addRemoveTags.removeConfirmation", - { count: selectedProductIds.length, tags } - )); - } - } - - if (error) { - enqueueSnackbar(i18next.t("admin.addRemoveTags.errorMessage"), { variant: "error" }); - } - - setVisibility(false); - setSelectedTags([]); - }; - - return ( - - {isVisible && - - - - setVisibility(false)}> - - - } - title={i18next.t("admin.productTable.bulkActions.addRemoveTags")} - /> - - - - setSelectedGroups(groups)} + placeholder={i18next.t("admin.groupCards.inviteShopMemberDialog.selectRoles")} + value={selectedGroups} + /> + + + + + + + + + + + ); +} + +InviteShopMember.propTypes = { + isOpen: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + onSuccess: PropTypes.func, + shopId: PropTypes.string +}; + +export default InviteShopMember; diff --git a/imports/plugins/core/accounts/client/graphql/mutations/inviteShopMember.js b/imports/plugins/core/accounts/client/graphql/mutations/inviteShopMember.js new file mode 100644 index 0000000000..63c9a539c6 --- /dev/null +++ b/imports/plugins/core/accounts/client/graphql/mutations/inviteShopMember.js @@ -0,0 +1,12 @@ +import gql from "graphql-tag"; + +export default gql` + mutation inviteShopMember($input: InviteShopMemberInput!) { + inviteShopMember(input: $input) { + clientMutationId + account { + _id + } + } + } +`; From ffda5a79268f40a2b2bf9d24df2cd4b2be501f73 Mon Sep 17 00:00:00 2001 From: Loan Laux Date: Thu, 30 Apr 2020 15:26:03 +0200 Subject: [PATCH 19/38] fix: update schemas Signed-off-by: Loan Laux --- .../accounts/client/components/InviteShopMemberDialog.js | 6 +++++- .../accounts/client/graphql/mutations/inviteShopMember.js | 3 --- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/imports/plugins/core/accounts/client/components/InviteShopMemberDialog.js b/imports/plugins/core/accounts/client/components/InviteShopMemberDialog.js index 24f1e7ec9d..4a6add049c 100644 --- a/imports/plugins/core/accounts/client/components/InviteShopMemberDialog.js +++ b/imports/plugins/core/accounts/client/components/InviteShopMemberDialog.js @@ -44,9 +44,13 @@ const useStyles = makeStyles((theme) => ({ })); const formSchema = new SimpleSchema({ - groupName: { + name: { type: String }, + email: { + type: String, + min: 3 + } }); const validator = formSchema.getFormValidator(); diff --git a/imports/plugins/core/accounts/client/graphql/mutations/inviteShopMember.js b/imports/plugins/core/accounts/client/graphql/mutations/inviteShopMember.js index 63c9a539c6..a5c08c8e63 100644 --- a/imports/plugins/core/accounts/client/graphql/mutations/inviteShopMember.js +++ b/imports/plugins/core/accounts/client/graphql/mutations/inviteShopMember.js @@ -4,9 +4,6 @@ export default gql` mutation inviteShopMember($input: InviteShopMemberInput!) { inviteShopMember(input: $input) { clientMutationId - account { - _id - } } } `; From d6d154a90d3334caea85275ec7233125e7365a72 Mon Sep 17 00:00:00 2001 From: Loan Laux Date: Thu, 30 Apr 2020 16:19:39 +0200 Subject: [PATCH 20/38] feat: add tabs for new views Signed-off-by: Loan Laux --- .../accounts/client/components/Accounts.js | 41 +++++++++++++++++++ imports/plugins/core/accounts/client/index.js | 4 +- 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 imports/plugins/core/accounts/client/components/Accounts.js diff --git a/imports/plugins/core/accounts/client/components/Accounts.js b/imports/plugins/core/accounts/client/components/Accounts.js new file mode 100644 index 0000000000..5c9a4a9bec --- /dev/null +++ b/imports/plugins/core/accounts/client/components/Accounts.js @@ -0,0 +1,41 @@ +import React, { Fragment, useState } from "react"; +import i18next from "i18next"; +import Divider from "@material-ui/core/Divider"; +import Tab from "@material-ui/core/Tab"; +import Tabs from "@material-ui/core/Tabs"; +import GroupCards from "./GroupCards"; + +/** + * @summary Main accounts view + * @name Accounts + * @returns {React.Component} A React component + */ +function Accounts() { + const [currentTab, setCurrentTab] = useState(0); + + return ( + + setCurrentTab(value)}> + + + + + + + + {currentTab === 0 && + + } + + {currentTab === 1 && +

Customers

+ } + + {currentTab === 2 && +

Invites

+ } +
+ ); +} + +export default Accounts; diff --git a/imports/plugins/core/accounts/client/index.js b/imports/plugins/core/accounts/client/index.js index 639661a920..b8fb0471af 100644 --- a/imports/plugins/core/accounts/client/index.js +++ b/imports/plugins/core/accounts/client/index.js @@ -2,7 +2,7 @@ import React from "react"; import AccountIcon from "mdi-material-ui/AccountMultiple"; import { registerOperatorRoute } from "/imports/client/ui"; -import GroupCards from "./components/GroupCards"; +import Accounts from "./components/Accounts"; export { default as AdminInviteForm } from "./components/adminInviteForm"; export { default as EditGroup } from "./components/editGroup"; @@ -24,7 +24,7 @@ registerOperatorRoute({ group: "navigation", path: "/accounts", priority: 40, - MainComponent: GroupCards, + MainComponent: Accounts, // eslint-disable-next-line react/display-name, react/no-multi-comp SidebarIconComponent: (props) => , sidebarI18nLabel: "admin.dashboard.accountsLabel" From c99219cc246a3effe9918d84c2be49147e6754c6 Mon Sep 17 00:00:00 2001 From: Loan Laux Date: Thu, 30 Apr 2020 16:22:15 +0200 Subject: [PATCH 21/38] chore: use startCase on group titles Signed-off-by: Loan Laux --- imports/plugins/core/accounts/client/components/GroupCards.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/imports/plugins/core/accounts/client/components/GroupCards.js b/imports/plugins/core/accounts/client/components/GroupCards.js index e8f1792d8f..9f388adc22 100644 --- a/imports/plugins/core/accounts/client/components/GroupCards.js +++ b/imports/plugins/core/accounts/client/components/GroupCards.js @@ -2,6 +2,7 @@ import React, { Fragment, useState } from "react"; import classNames from "classnames"; import { useMutation, useQuery } from "@apollo/react-hooks"; import i18next from "i18next"; +import startCase from "lodash/startCase"; import Button from "@reactioncommerce/catalyst/Button"; import { useSnackbar } from "notistack"; import { Card, CardHeader, CardContent, Collapse, Grid, IconButton, makeStyles } from "@material-ui/core"; @@ -65,7 +66,7 @@ function GroupCards() { {!shopId || !groups || isLoadingGroups ? : groups.map((group) => ( - + From ccb074e0352412757fb1dd99412d7d84b7e762da Mon Sep 17 00:00:00 2001 From: Loan Laux Date: Thu, 30 Apr 2020 17:00:05 +0200 Subject: [PATCH 22/38] chore: add margins between elements Signed-off-by: Loan Laux --- .../accounts/client/components/GroupCard.js | 34 +++++++++++++++++ .../accounts/client/components/GroupCards.js | 38 ++++++++----------- 2 files changed, 50 insertions(+), 22 deletions(-) create mode 100644 imports/plugins/core/accounts/client/components/GroupCard.js diff --git a/imports/plugins/core/accounts/client/components/GroupCard.js b/imports/plugins/core/accounts/client/components/GroupCard.js new file mode 100644 index 0000000000..b83ef1391c --- /dev/null +++ b/imports/plugins/core/accounts/client/components/GroupCard.js @@ -0,0 +1,34 @@ +import React, { useState } from "react"; +import startCase from "lodash/startCase"; +import { Card, CardHeader, CardContent, Collapse, IconButton, makeStyles } from "@material-ui/core"; +import AccountsTable from "./AccountsTable"; + +const useStyles = makeStyles(() => ({ + card: { + overflow: "visible", + marginBottom: "1rem" + } +})); + +/** + * @summary Group card view + * @name GroupCard + * @returns {React.Component} A React component + */ +function GroupCard({ group, groups, isLoadingGroups }) { + const classes = useStyles(); + const [isExpanded, setIsExpanded] = useState(false); + + return ( + + + + + + + + + ); +} + +export default GroupCard; diff --git a/imports/plugins/core/accounts/client/components/GroupCards.js b/imports/plugins/core/accounts/client/components/GroupCards.js index 9f388adc22..ca33a9ca2a 100644 --- a/imports/plugins/core/accounts/client/components/GroupCards.js +++ b/imports/plugins/core/accounts/client/components/GroupCards.js @@ -1,12 +1,7 @@ import React, { Fragment, useState } from "react"; -import classNames from "classnames"; -import { useMutation, useQuery } from "@apollo/react-hooks"; import i18next from "i18next"; -import startCase from "lodash/startCase"; import Button from "@reactioncommerce/catalyst/Button"; -import { useSnackbar } from "notistack"; -import { Card, CardHeader, CardContent, Collapse, Grid, IconButton, makeStyles } from "@material-ui/core"; -import ExpandMoreIcon from "mdi-material-ui/ChevronUp"; +import { Grid, makeStyles } from "@material-ui/core"; import { Components } from "@reactioncommerce/reaction-components"; import useCurrentShopId from "/imports/client/ui/hooks/useCurrentShopId"; import groupsQuery from "../graphql/queries/groups"; @@ -14,14 +9,20 @@ import archiveGroups from "../graphql/mutations/archiveGroups"; import updateGroup from "../graphql/mutations/updateGroup"; import cloneGroups from "../graphql/mutations/cloneGroups"; import createGroupMutation from "../graphql/mutations/createGroup"; -import AccountsTable from "./AccountsTable"; import CreateGroupDialog from "./CreateGroupDialog"; import InviteShopMemberDialog from "./InviteShopMemberDialog"; +import GroupCard from "./GroupCard"; import useGroups from "../hooks/useGroups"; const useStyles = makeStyles(() => ({ - card: { - overflow: "visible" + actionButtons: { + marginTop: "1rem" + }, + actionButton: { + marginLeft: ".5rem", + "&:first-child": { + marginLeft: 0 + } } })); @@ -31,13 +32,11 @@ const useStyles = makeStyles(() => ({ * @returns {React.Component} A React component */ function GroupCards() { - const { enqueueSnackbar } = useSnackbar(); const [shopId] = useCurrentShopId(); - const [createGroup, { error: createGroupError }] = useMutation(createGroupMutation); - const classes = useStyles(); const [isCreateGroupDialogVisible, setCreateGroupDialogVisibility] = useState(false); const [isInviteShopMemberDialogVisible, setInviteShopMemberDialogVisibility] = useState(false); const { isLoadingGroups, groups, refetchGroups } = useGroups(shopId); + const classes = useStyles(); return ( @@ -55,22 +54,17 @@ function GroupCards() { shopId={shopId} /> - - - {!shopId || !groups || isLoadingGroups ? : groups.map((group) => ( - - - - - - + ))} From b0c4a6df290644e7cfa39830659d950603e1416a Mon Sep 17 00:00:00 2001 From: Loan Laux Date: Thu, 30 Apr 2020 18:36:20 +0200 Subject: [PATCH 23/38] feat: make group cards expandable Signed-off-by: Loan Laux --- .../accounts/client/components/GroupCard.js | 7 ++- .../client/components/GroupCardHeader.js | 50 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 imports/plugins/core/accounts/client/components/GroupCardHeader.js diff --git a/imports/plugins/core/accounts/client/components/GroupCard.js b/imports/plugins/core/accounts/client/components/GroupCard.js index b83ef1391c..20a6b6f691 100644 --- a/imports/plugins/core/accounts/client/components/GroupCard.js +++ b/imports/plugins/core/accounts/client/components/GroupCard.js @@ -2,6 +2,7 @@ import React, { useState } from "react"; import startCase from "lodash/startCase"; import { Card, CardHeader, CardContent, Collapse, IconButton, makeStyles } from "@material-ui/core"; import AccountsTable from "./AccountsTable"; +import GroupCardHeader from "./GroupCardHeader"; const useStyles = makeStyles(() => ({ card: { @@ -21,7 +22,11 @@ function GroupCard({ group, groups, isLoadingGroups }) { return ( - + } + title={startCase(group.name)} + /> diff --git a/imports/plugins/core/accounts/client/components/GroupCardHeader.js b/imports/plugins/core/accounts/client/components/GroupCardHeader.js new file mode 100644 index 0000000000..33ed04ea5e --- /dev/null +++ b/imports/plugins/core/accounts/client/components/GroupCardHeader.js @@ -0,0 +1,50 @@ +import React from "react"; +import classNames from "classnames"; +import { Grid, Typography, makeStyles } from "@material-ui/core"; +import ChevronDownIcon from "mdi-material-ui/ChevronDown"; +import IconButton from "@material-ui/core/IconButton"; + +const useStyles = makeStyles((theme) => ({ + expand: { + float: "right", + transform: "rotate(0deg)", + marginLeft: "auto", + transition: theme.transitions.create("transform", { + duration: theme.transitions.duration.shortest + }) + }, + expandOpen: { + transform: "rotate(180deg)", + }, + titleContainer: { + display: "flex", + alignItems: "center" + } +})); + +function GroupCardHeader({ children, expanded, onExpandClick }) { + const classes = useStyles(); + + return ( + + + {children} + + + onExpandClick(!expanded)} + aria-expanded={expanded} + aria-label="show more" + > + + + + + ); +} + +export default GroupCardHeader; From 60c9a17448db24eb4c53c134514c800a4bc12245 Mon Sep 17 00:00:00 2001 From: Loan Laux Date: Fri, 1 May 2020 16:24:10 +0200 Subject: [PATCH 24/38] feat: add edit group capabilities Signed-off-by: Loan Laux --- ...upDialog.js => CreateOrEditGroupDialog.js} | 84 ++++++++++++++----- .../accounts/client/components/GroupCard.js | 48 ++++++++--- .../client/components/GroupCardHeader.js | 8 +- .../accounts/client/components/GroupCards.js | 12 ++- .../client/graphql/mutations/updateGroup.js | 8 +- 5 files changed, 117 insertions(+), 43 deletions(-) rename imports/plugins/core/accounts/client/components/{CreateGroupDialog.js => CreateOrEditGroupDialog.js} (61%) diff --git a/imports/plugins/core/accounts/client/components/CreateGroupDialog.js b/imports/plugins/core/accounts/client/components/CreateOrEditGroupDialog.js similarity index 61% rename from imports/plugins/core/accounts/client/components/CreateGroupDialog.js rename to imports/plugins/core/accounts/client/components/CreateOrEditGroupDialog.js index 8ec05dfea7..2b128c80b6 100644 --- a/imports/plugins/core/accounts/client/components/CreateGroupDialog.js +++ b/imports/plugins/core/accounts/client/components/CreateOrEditGroupDialog.js @@ -22,6 +22,7 @@ import { } from "@material-ui/core"; import useRoles from "../hooks/useRoles"; import createGroupMutation from "../graphql/mutations/createGroup"; +import updateGroupMutation from "../graphql/mutations/updateGroup"; const useStyles = makeStyles((theme) => ({ cardRoot: { @@ -44,38 +45,53 @@ const useStyles = makeStyles((theme) => ({ })); const formSchema = new SimpleSchema({ - groupName: { + name: { type: String - }, + } }); const validator = formSchema.getFormValidator(); /** - * CreateGroup component + * CreateOrEditGroup component * @param {Object} props Component props * @returns {React.Component} A React component */ -function CreateGroup({ isOpen, onClose, onSuccess, shopId }) { +function CreateOrEditGroup({ isOpen, onClose, onSuccess, group, shopId }) { const [isSubmitting, setIsSubmitting] = useState(false); - const [selectedRoles, setSelectedRoles] = useState([]); - const [isLoading, setIsLoading] = useState(false); const classes = useStyles(); const { enqueueSnackbar } = useSnackbar(); const { roles } = useRoles(shopId); - const [createGroup] = useMutation(createGroupMutation, { + const [selectedRoles, setSelectedRoles] = useState(group ? group.permissions.map((role) => ({ label: role, value: role })) : []); + + let mutation = createGroupMutation; + + if (group) { + mutation = updateGroupMutation; + } + + const [createOrUpdateGroup] = useMutation(mutation, { ignoreResults: true, onCompleted() { setIsSubmitting(false); onSuccess(); + onClose(); }, onError() { setIsSubmitting(false); - enqueueSnackbar(i18next.t("admin.groupCards.createGroupDialog.saveFailed"), { variant: "error" }); + enqueueSnackbar(i18next.t("admin.groupCards.createOrUpdateGroupDialog.saveFailed"), { variant: "error" }); } }); + const initialValues = {}; + const groupIdVariable = {}; + + if (group) { + initialValues.value = group; + groupIdVariable.groupId = group._id; + } + const { getFirstErrorMessage, getInputProps, @@ -86,14 +102,17 @@ function CreateGroup({ isOpen, onClose, onSuccess, shopId }) { async onSubmit(formData) { setIsSubmitting(true); - await createGroup({ + await createOrUpdateGroup({ variables: { input: { group: { - name: formData.groupName, + name: formData.name, + description: formData.description, + slug: formData.slug, permissions: selectedRoles.map((role) => role.value) }, - shopId + shopId, + ...groupIdVariable } } }); @@ -102,7 +121,8 @@ function CreateGroup({ isOpen, onClose, onSuccess, shopId }) { }, validator(formData) { return validator(formSchema.clean(formData)); - } + }, + ...initialValues }); const handleSubmit = (event) => { @@ -126,25 +146,44 @@ function CreateGroup({ isOpen, onClose, onSuccess, shopId }) { } - title={i18next.t("admin.groupCards.createGroupDialog.title")} + title={i18next.t("admin.groupCards.createOrUpdateGroupDialog.title")} /> + + + + + + setSelectedGroups(groups)} + onSelection={(groupsToSelect) => setSelectedGroups(groupsToSelect)} placeholder={i18next.t("admin.groupCards.inviteStaffMemberDialog.selectGroups")} value={selectedGroups} /> @@ -182,6 +181,7 @@ function InviteShopMember({ isOpen, onClose, onSuccess, groups, shopId }) { } InviteShopMember.propTypes = { + groups: PropTypes.arrayOf(PropTypes.object), isOpen: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, onSuccess: PropTypes.func, diff --git a/imports/plugins/core/accounts/client/hooks/useGroups.js b/imports/plugins/core/accounts/client/hooks/useGroups.js index 83e1c04a15..018325ddc8 100644 --- a/imports/plugins/core/accounts/client/hooks/useGroups.js +++ b/imports/plugins/core/accounts/client/hooks/useGroups.js @@ -2,7 +2,9 @@ import { useLazyQuery } from "@apollo/react-hooks"; import groupsQuery from "../graphql/queries/groups"; /** - * Hook to get groups + * @summary Hook to get groups + * @name useGroups + * @param {String} shopId - the shop ID to get groups for * @return {Object} Permissions */ export default function useGroups(shopId) { diff --git a/imports/plugins/core/accounts/client/hooks/useRoles.js b/imports/plugins/core/accounts/client/hooks/useRoles.js index dbcd4fcd6d..4ae252cf50 100644 --- a/imports/plugins/core/accounts/client/hooks/useRoles.js +++ b/imports/plugins/core/accounts/client/hooks/useRoles.js @@ -2,7 +2,9 @@ import { useLazyQuery } from "@apollo/react-hooks"; import rolesQuery from "../graphql/queries/roles"; /** - * Hook to get groups + * @summary Hook to get groups + * @name useRoles + * @param {String} shopId - the shop ID to get roles for * @return {Object} Permissions */ export default function useRoles(shopId) { diff --git a/imports/plugins/core/accounts/server/methods/index.js b/imports/plugins/core/accounts/server/methods/index.js index b2beb02246..69397901dd 100644 --- a/imports/plugins/core/accounts/server/methods/index.js +++ b/imports/plugins/core/accounts/server/methods/index.js @@ -19,5 +19,5 @@ import verifyAccount from "./verifyAccount"; export default { "accounts/setActiveShopId": setActiveShopId, "accounts/updateEmailAddress": updateEmailAddress, - "accounts/verifyAccount": verifyAccount, + "accounts/verifyAccount": verifyAccount }; From 0554bdd19e9c5a1c399f1f1df130051f00bf08ef Mon Sep 17 00:00:00 2001 From: Loan Laux Date: Wed, 10 Jun 2020 10:17:55 +0200 Subject: [PATCH 35/38] fix: prevent select options from being cut off Signed-off-by: Loan Laux --- .../accounts/client/components/CreateOrEditGroupDialog.js | 4 ++-- .../core/accounts/client/components/InviteShopMemberDialog.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/imports/plugins/core/accounts/client/components/CreateOrEditGroupDialog.js b/imports/plugins/core/accounts/client/components/CreateOrEditGroupDialog.js index 422c4946c4..afb968e3b6 100644 --- a/imports/plugins/core/accounts/client/components/CreateOrEditGroupDialog.js +++ b/imports/plugins/core/accounts/client/components/CreateOrEditGroupDialog.js @@ -25,7 +25,7 @@ import createGroupMutation from "../graphql/mutations/createGroup"; import updateGroupMutation from "../graphql/mutations/updateGroup"; const useStyles = makeStyles((theme) => ({ - cardRoot: { + dialogPaper: { overflow: "visible", padding: theme.spacing(2) }, @@ -134,9 +134,9 @@ function CreateOrEditGroup({ isOpen, onClose, onSuccess, group, shopId }) { return ( diff --git a/imports/plugins/core/accounts/client/components/InviteShopMemberDialog.js b/imports/plugins/core/accounts/client/components/InviteShopMemberDialog.js index 75f3bf4ca9..9b032c91b0 100644 --- a/imports/plugins/core/accounts/client/components/InviteShopMemberDialog.js +++ b/imports/plugins/core/accounts/client/components/InviteShopMemberDialog.js @@ -23,7 +23,7 @@ import { import inviteShopMemberMutation from "../graphql/mutations/inviteShopMember"; const useStyles = makeStyles((theme) => ({ - cardRoot: { + dialogPaper: { overflow: "visible", padding: theme.spacing(2) }, @@ -113,9 +113,9 @@ function InviteShopMember({ isOpen, onClose, onSuccess, groups, shopId }) { return ( From 15485e950509accbe941c6702c5aef6550b1d3dd Mon Sep 17 00:00:00 2001 From: Loan Laux Date: Wed, 10 Jun 2020 10:20:29 +0200 Subject: [PATCH 36/38] fix: remove required shopId prop Signed-off-by: Loan Laux --- .../core/accounts/client/components/CreateOrEditGroupDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imports/plugins/core/accounts/client/components/CreateOrEditGroupDialog.js b/imports/plugins/core/accounts/client/components/CreateOrEditGroupDialog.js index afb968e3b6..0813bcd87a 100644 --- a/imports/plugins/core/accounts/client/components/CreateOrEditGroupDialog.js +++ b/imports/plugins/core/accounts/client/components/CreateOrEditGroupDialog.js @@ -216,7 +216,7 @@ CreateOrEditGroup.propTypes = { isOpen: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, onSuccess: PropTypes.func, - shopId: PropTypes.string.isRequired + shopId: PropTypes.string }; export default CreateOrEditGroup; From 557226b6f03fb3dedb068ff7a81161993e231345 Mon Sep 17 00:00:00 2001 From: Loan Laux Date: Wed, 10 Jun 2020 10:21:29 +0200 Subject: [PATCH 37/38] fix: use correct PropType for children prop Signed-off-by: Loan Laux --- .../plugins/core/accounts/client/components/GroupCardHeader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imports/plugins/core/accounts/client/components/GroupCardHeader.js b/imports/plugins/core/accounts/client/components/GroupCardHeader.js index 7cc4def944..f7133d1261 100644 --- a/imports/plugins/core/accounts/client/components/GroupCardHeader.js +++ b/imports/plugins/core/accounts/client/components/GroupCardHeader.js @@ -63,7 +63,7 @@ function GroupCardHeader({ children: title, isExpanded, onEdit, onExpandClick }) } GroupCardHeader.propTypes = { - children: PropTypes.func.isRequired, + children: PropTypes.arrayOf(PropTypes.node).isRequired, isExpanded: PropTypes.bool, onEdit: PropTypes.func, onExpandClick: PropTypes.func From c194a12d68540a9fbec31ca811c556cf0a20a8b0 Mon Sep 17 00:00:00 2001 From: Loan Laux Date: Wed, 10 Jun 2020 10:22:15 +0200 Subject: [PATCH 38/38] fix: add React key for GroupCard component Signed-off-by: Loan Laux --- imports/plugins/core/accounts/client/components/GroupCards.js | 1 + 1 file changed, 1 insertion(+) diff --git a/imports/plugins/core/accounts/client/components/GroupCards.js b/imports/plugins/core/accounts/client/components/GroupCards.js index 594346c741..40e8ff2ffd 100644 --- a/imports/plugins/core/accounts/client/components/GroupCards.js +++ b/imports/plugins/core/accounts/client/components/GroupCards.js @@ -63,6 +63,7 @@ function GroupCards() { group={group} groups={groups} isLoadingGroups={isLoadingGroups} + key={group._id} refetchGroups={refetchGroups} shopId={shopId} />