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..78a7340f39
--- /dev/null
+++ b/imports/plugins/core/accounts/client/components/Accounts.js
@@ -0,0 +1,43 @@
+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 Customers from "./Customers";
+import GroupCards from "./GroupCards";
+import Invitations from "./Invitations";
+
+/**
+ * @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 &&
+
+ }
+
+ {currentTab === 2 &&
+
+ }
+
+ );
+}
+
+export default Accounts;
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..3bb26b6a64
--- /dev/null
+++ b/imports/plugins/core/accounts/client/components/AccountsTable.js
@@ -0,0 +1,122 @@
+import React, { Fragment, useState, useMemo, useCallback } from "react";
+import PropTypes from "prop-types";
+import { useApolloClient } from "@apollo/react-hooks";
+import i18next from "i18next";
+import DataTable, { useDataTable } from "@reactioncommerce/catalyst/DataTable";
+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 GroupSelectorDialog from "./GroupSelectorDialog";
+
+/**
+ * @summary Main products view
+ * @name ProductsTable
+ * @param {Object} props - the component's props
+ * @returns {React.Component} A React component
+ */
+function AccountsTable(props) {
+ const apolloClient = useApolloClient();
+ const [shopId] = useCurrentShopId();
+
+ // 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 [isGroupSelectorVisible, setGroupSelectorVisibility] = useState(false);
+
+ // Create and memoize the column data
+ const columns = useMemo(() => [
+ {
+ Header: "",
+ accessor: (account) => getAccountAvatar(account),
+ id: "profilePicture"
+ }, {
+ Header: i18next.t("admin.accountsTable.header.email"),
+ accessor: (row) => row.emailRecords[0].address,
+ id: "email"
+ }, {
+ Header: i18next.t("admin.accountsTable.header.name"),
+ accessor: "name"
+ }
+ ], []);
+
+ const onFetchData = useCallback(async ({ 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: {
+ groupIds: [props.group._id],
+ 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 dataTableProps = useDataTable({
+ columns,
+ data: tableData,
+ isFilterable: false,
+ pageCount,
+ onFetchData,
+ onRowSelect,
+ getRowId: (row) => row._id
+ });
+
+ // Create options for the built-in ActionMenu in the DataTable
+ const options = useMemo(() => [{
+ label: i18next.t("admin.accountsTable.bulkActions.addRemoveGroupsFromAccount"),
+ isDisabled: selectedRows.length === 0,
+ onClick: () => {
+ setGroupSelectorVisibility(true);
+ }
+ }], [selectedRows]);
+
+ return (
+
+ {selectedRows && !props.isLoadingGroups &&
+ null}
+ onClose={() => setGroupSelectorVisibility(false)}
+ accounts={tableData.filter((account) => selectedRows.includes(account._id))}
+ groups={props.groups}
+ />
+ }
+
+
+ );
+}
+
+AccountsTable.propTypes = {
+ group: PropTypes.shape({
+ _id: PropTypes.string.isRequired
+ }),
+ groups: PropTypes.arrayOf(PropTypes.object),
+ isLoadingGroups: PropTypes.bool
+};
+
+export default AccountsTable;
diff --git a/imports/plugins/core/accounts/client/components/CreateOrEditGroupDialog.js b/imports/plugins/core/accounts/client/components/CreateOrEditGroupDialog.js
new file mode 100644
index 0000000000..0813bcd87a
--- /dev/null
+++ b/imports/plugins/core/accounts/client/components/CreateOrEditGroupDialog.js
@@ -0,0 +1,222 @@
+import React, { useState } from "react";
+import PropTypes from "prop-types";
+import i18next from "i18next";
+import SimpleSchema from "simpl-schema";
+import CloseIcon from "mdi-material-ui/Close";
+import Button from "@reactioncommerce/catalyst/Button";
+import TextField from "@reactioncommerce/catalyst/TextField";
+import Select from "@reactioncommerce/catalyst/Select";
+import useReactoForm from "reacto-form/cjs/useReactoForm";
+import muiOptions from "reacto-form/cjs/muiOptions";
+import { useMutation } from "@apollo/react-hooks";
+import { useSnackbar } from "notistack";
+import {
+ Box,
+ Grid,
+ CardActions,
+ CardHeader,
+ CardContent,
+ Dialog,
+ IconButton,
+ makeStyles
+} 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) => ({
+ dialogPaper: {
+ overflow: "visible",
+ padding: theme.spacing(2)
+ },
+ cardContainer: {
+ alignItems: "center"
+ },
+ cardActions: {
+ padding: theme.spacing(2),
+ justifyContent: "flex-end"
+ },
+ hidden: {
+ display: "none"
+ },
+ visible: {
+ display: "block"
+ }
+}));
+
+const formSchema = new SimpleSchema({
+ name: {
+ type: String
+ }
+});
+const validator = formSchema.getFormValidator();
+
+/**
+ * CreateOrEditGroup component
+ * @param {Object} props Component props
+ * @returns {React.Component} A React component
+ */
+function CreateOrEditGroup({ isOpen, onClose, onSuccess, group, shopId }) {
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const classes = useStyles();
+ const { enqueueSnackbar } = useSnackbar();
+
+ const { roles } = useRoles(shopId);
+
+ 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.createOrUpdateGroupDialog.saveFailed"), { variant: "error" });
+ }
+ });
+
+ const initialValues = {};
+ const groupIdVariable = {};
+
+ if (group) {
+ initialValues.value = group;
+ groupIdVariable.groupId = group._id;
+ }
+
+ const {
+ getFirstErrorMessage,
+ getInputProps,
+ hasErrors,
+ isDirty,
+ submitForm
+ } = useReactoForm({
+ async onSubmit(formData) {
+ setIsSubmitting(true);
+
+ await createOrUpdateGroup({
+ variables: {
+ input: {
+ group: {
+ name: formData.name,
+ description: formData.description,
+ slug: formData.slug,
+ permissions: selectedRoles.map((role) => role.value)
+ },
+ shopId,
+ ...groupIdVariable
+ }
+ }
+ });
+
+ setIsSubmitting(false);
+ },
+ validator(formData) {
+ return validator(formSchema.clean(formData));
+ },
+ ...initialValues
+ });
+
+ const handleSubmit = (event) => {
+ event.preventDefault();
+ submitForm();
+ };
+
+ const rolesForSelect = roles.map((role) => ({ value: role.name, label: role.name }));
+
+ return (
+
+
+
+
+ }
+ title={i18next.t("admin.groupCards.createOrUpdateGroupDialog.title")}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {i18next.t("app.cancel")}
+
+
+
+ {isSubmitting ? i18next.t("app.settings.saveProcessing") : i18next.t("app.saveChanges")}
+
+
+
+ );
+}
+
+CreateOrEditGroup.propTypes = {
+ group: PropTypes.object,
+ isOpen: PropTypes.bool.isRequired,
+ onClose: PropTypes.func.isRequired,
+ onSuccess: PropTypes.func,
+ shopId: PropTypes.string
+};
+
+export default CreateOrEditGroup;
diff --git a/imports/plugins/core/accounts/client/components/Customers.js b/imports/plugins/core/accounts/client/components/Customers.js
new file mode 100644
index 0000000000..8943fa461c
--- /dev/null
+++ b/imports/plugins/core/accounts/client/components/Customers.js
@@ -0,0 +1,93 @@
+import React, { useState, useMemo, useCallback } from "react";
+import { useApolloClient } from "@apollo/react-hooks";
+import i18next from "i18next";
+import { Card, CardContent, makeStyles } from "@material-ui/core";
+import DataTable, { useDataTable } from "@reactioncommerce/catalyst/DataTable";
+import useCurrentShopId from "/imports/client/ui/hooks/useCurrentShopId";
+import { getAccountAvatar } from "/imports/plugins/core/accounts/client/helpers/helpers";
+import customersQuery from "../graphql/queries/customers";
+
+const useStyles = makeStyles(() => ({
+ card: {
+ marginTop: "1rem"
+ }
+}));
+
+/**
+ * @summary Main customers view
+ * @name CustomersTable
+ * @returns {React.Component} A React component
+ */
+function Customers() {
+ const apolloClient = useApolloClient();
+ const [shopId] = useCurrentShopId();
+
+ // React-Table state
+ const [isLoading, setIsLoading] = useState(false);
+ const [pageCount, setPageCount] = useState(1);
+ const [tableData, setTableData] = useState([]);
+
+ const classes = useStyles();
+
+ // Create and memoize the column data
+ const columns = useMemo(() => [
+ {
+ Header: "",
+ accessor: (account) => getAccountAvatar(account),
+ id: "profilePicture"
+ }, {
+ Header: i18next.t("admin.accountsTable.header.email"),
+ accessor: (row) => row.emailRecords[0].address,
+ id: "email"
+ }, {
+ Header: i18next.t("admin.accountsTable.header.name"),
+ accessor: "name"
+ }
+ ], []);
+
+ const onFetchData = useCallback(async ({ pageIndex, pageSize }) => {
+ // Wait for shop id to be available before fetching products.
+ setIsLoading(true);
+ if (!shopId) {
+ return;
+ }
+
+ const { data } = await apolloClient.query({
+ query: customersQuery,
+ variables: {
+ 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.customers.nodes);
+ setPageCount(Math.ceil(data.customers.totalCount / pageSize));
+
+ setIsLoading(false);
+ }, [apolloClient, shopId]);
+
+ const dataTableProps = useDataTable({
+ columns,
+ data: tableData,
+ isFilterable: false,
+ pageCount,
+ onFetchData,
+ getRowId: (row) => row._id
+ });
+
+ return (
+
+
+
+
+
+ );
+}
+
+export default Customers;
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..5f5246650a
--- /dev/null
+++ b/imports/plugins/core/accounts/client/components/GroupCard.js
@@ -0,0 +1,73 @@
+import React, { Fragment, useState } from "react";
+import PropTypes from "prop-types";
+import startCase from "lodash/startCase";
+import { Card, CardHeader, CardContent, Collapse, makeStyles } from "@material-ui/core";
+import AccountsTable from "./AccountsTable";
+import GroupCardHeader from "./GroupCardHeader";
+import CreateOrEditGroupDialog from "./CreateOrEditGroupDialog";
+
+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, refetchGroups, shopId }) {
+ const classes = useStyles();
+ const [isExpanded, setIsExpanded] = useState(false);
+ const [isEditGroupDialogVisible, setEditGroupDialogVisibility] = useState(false);
+
+ return (
+
+ setEditGroupDialogVisibility(false)}
+ group={group}
+ shopId={shopId}
+ />
+
+ (
+ setEditGroupDialogVisibility(true)}
+ onExpandClick={setIsExpanded}
+ {...props}
+ />
+ )}
+ title={startCase(group.name)}
+ />
+
+
+
+
+
+
+
+ );
+}
+
+GroupCard.propTypes = {
+ group: PropTypes.shape({
+ _id: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired
+ }),
+ groups: PropTypes.arrayOf(PropTypes.object),
+ isLoadingGroups: PropTypes.bool,
+ refetchGroups: PropTypes.func,
+ shopId: PropTypes.string
+};
+
+export default GroupCard;
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..f7133d1261
--- /dev/null
+++ b/imports/plugins/core/accounts/client/components/GroupCardHeader.js
@@ -0,0 +1,72 @@
+import React from "react";
+import PropTypes from "prop-types";
+import classNames from "classnames";
+import { Grid, makeStyles } from "@material-ui/core";
+import ChevronDownIcon from "mdi-material-ui/ChevronDown";
+import PencilIcon from "mdi-material-ui/Pencil";
+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"
+ }
+}));
+
+/**
+ * @summary Renders the group card header
+ * @name GroupCardHeader
+ * @param {Object} props - the component's props
+ * @param {String} props.children - the group's title component
+ * @param {Boolean} props.isExpanded - whether the group card should be expanded
+ * @param {Function} props.onEdit - called back when clicking the edit button
+ * @param {Function} props.onExpandClick - called back when clicking the expand button
+ * @returns {React.Component} - the GroupCardHeader component
+ */
+function GroupCardHeader({ children: title, isExpanded, onEdit, onExpandClick }) {
+ const classes = useStyles();
+
+ return (
+
+
+
+
+
+ {title}
+
+
+ onExpandClick(!isExpanded)}
+ aria-expanded={isExpanded}
+ aria-label="show more"
+ >
+
+
+
+
+ );
+}
+
+GroupCardHeader.propTypes = {
+ children: PropTypes.arrayOf(PropTypes.node).isRequired,
+ isExpanded: PropTypes.bool,
+ onEdit: PropTypes.func,
+ onExpandClick: PropTypes.func
+};
+
+export default GroupCardHeader;
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..40e8ff2ffd
--- /dev/null
+++ b/imports/plugins/core/accounts/client/components/GroupCards.js
@@ -0,0 +1,77 @@
+import React, { Fragment, useState } from "react";
+import i18next from "i18next";
+import Button from "@reactioncommerce/catalyst/Button";
+import { Grid, makeStyles } from "@material-ui/core";
+import { Components } from "@reactioncommerce/reaction-components";
+import useCurrentShopId from "/imports/client/ui/hooks/useCurrentShopId";
+import useGroups from "../hooks/useGroups";
+import CreateOrEditGroupDialog from "./CreateOrEditGroupDialog";
+import InviteShopMemberDialog from "./InviteShopMemberDialog";
+import GroupCard from "./GroupCard";
+
+const useStyles = makeStyles(() => ({
+ actionButtons: {
+ marginTop: "1rem"
+ },
+ actionButton: {
+ "marginLeft": ".5rem",
+ "&:first-child": {
+ marginLeft: 0
+ }
+ }
+}));
+
+/**
+ * @summary Main groups view
+ * @name GroupCards
+ * @returns {React.Component} A React component
+ */
+function GroupCards() {
+ const [shopId] = useCurrentShopId();
+ const [isCreateGroupDialogVisible, setCreateGroupDialogVisibility] = useState(false);
+ const [isInviteShopMemberDialogVisible, setInviteShopMemberDialogVisibility] = useState(false);
+ const { isLoadingGroups, groups, refetchGroups } = useGroups(shopId);
+ const classes = useStyles();
+
+ return (
+
+ setCreateGroupDialogVisibility(false)}
+ shopId={shopId}
+ />
+ true}
+ onClose={() => setInviteShopMemberDialogVisibility(false)}
+ groups={groups}
+ shopId={shopId}
+ />
+
+
+ setCreateGroupDialogVisibility(true)}>
+ {i18next.t("admin.accounts.createGroup") || "Create group"}
+
+ setInviteShopMemberDialogVisibility(true)}>
+ {i18next.t("admin.accounts.inviteStaffMember") || "Invite staff member"}
+
+
+
+ {!shopId || !groups || isLoadingGroups ? : groups.map((group) => (
+
+ ))}
+
+
+
+ );
+}
+
+export default GroupCards;
diff --git a/imports/plugins/core/accounts/client/components/GroupSelectorDialog.js b/imports/plugins/core/accounts/client/components/GroupSelectorDialog.js
new file mode 100644
index 0000000000..326a56d33d
--- /dev/null
+++ b/imports/plugins/core/accounts/client/components/GroupSelectorDialog.js
@@ -0,0 +1,160 @@
+import React, { useState } from "react";
+import PropTypes from "prop-types";
+import i18next from "i18next";
+import CloseIcon from "mdi-material-ui/Close";
+import Button from "@reactioncommerce/catalyst/Button";
+import Select from "@reactioncommerce/catalyst/Select";
+import { useMutation } from "@apollo/react-hooks";
+import { useSnackbar } from "notistack";
+import {
+ Box,
+ Grid,
+ CardActions,
+ CardHeader,
+ CardContent,
+ Dialog,
+ IconButton,
+ makeStyles
+} from "@material-ui/core";
+import updateGroupsForAccountsMutation from "../graphql/mutations/updateGroupsForAccounts";
+
+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"
+ }
+}));
+
+/**
+ * GroupSelector component
+ * @param {Object} props Component props
+ * @returns {React.Component} A React component
+ */
+function GroupSelector({ isOpen, onClose, onSuccess, accounts, groups }) {
+ const [selectedGroups, setSelectedGroups] = useState([]);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const classes = useStyles();
+ const { enqueueSnackbar } = useSnackbar();
+
+ const [updateGroupsForAccounts] = useMutation(updateGroupsForAccountsMutation, {
+ ignoreResults: true,
+ onCompleted() {
+ setIsSubmitting(false);
+ onSuccess();
+ },
+ onError() {
+ setIsSubmitting(false);
+ enqueueSnackbar(i18next.t("admin.accountsTable.groupSelectorDialog.saveFailed"), { variant: "error" });
+ }
+ });
+
+ // eslint-disable-next-line consistent-return
+ const handleSubmit = async () => {
+ const groupIds = selectedGroups && selectedGroups.map(({ value }) => (value));
+
+ // Prevent user from executing action if they haven't yet selected at least one group
+ if (!groupIds.length) {
+ return enqueueSnackbar(i18next.t("admin.accountsTable.groupSelectorDialog.invalidSelection"), { variant: "warning" });
+ }
+
+ setIsSubmitting(true);
+
+ await updateGroupsForAccounts({
+ variables: {
+ input: {
+ groupIds,
+ accountIds: accounts.map((account) => account._id)
+ }
+ }
+ });
+
+ setIsSubmitting(false);
+ onClose();
+ setSelectedGroups([]);
+ };
+
+ const groupsForSelect = groups.map((group) => ({ value: group._id, label: group.name }));
+
+ const editingSingleAccount = Array.isArray(accounts) && accounts.length === 1;
+ const accountHasGroups = accounts[0] && accounts[0].groups && Array.isArray(accounts[0].groups.nodes) && accounts[0].groups.nodes.length > 0;
+ const noGroupsSelected = selectedGroups && selectedGroups.length === 0;
+
+ // If modifying one single account, pre-select groups that the account already belongs to
+ if (editingSingleAccount && accountHasGroups && noGroupsSelected) {
+ const preSelectedGroups = accounts[0].groups.nodes.map((group) => ({ value: group._id, label: group.name }));
+ setSelectedGroups(preSelectedGroups);
+ }
+
+ return (
+
+
+
+
+ }
+ title={i18next.t("admin.accountsTable.bulkActions.addRemoveGroupsFromAccount")}
+ />
+
+
+
+ setSelectedGroups(tags)}
+ placeholder={i18next.t("admin.accountsTable.groupSelectorDialog.inputPlaceholder")}
+ value={selectedGroups}
+ />
+
+
+
+
+
+
+ {i18next.t("app.cancel")}
+
+
+
+ {isSubmitting ? i18next.t("app.settings.saveProcessing") : i18next.t("app.saveChanges")}
+
+
+
+ );
+}
+
+GroupSelector.propTypes = {
+ accounts: PropTypes.arrayOf(PropTypes.object).isRequired,
+ groups: PropTypes.arrayOf(PropTypes.object).isRequired,
+ isOpen: PropTypes.bool.isRequired,
+ onClose: PropTypes.func.isRequired,
+ onSuccess: PropTypes.func
+};
+
+export default GroupSelector;
diff --git a/imports/plugins/core/accounts/client/components/Invitations.js b/imports/plugins/core/accounts/client/components/Invitations.js
new file mode 100644
index 0000000000..1be2fce74d
--- /dev/null
+++ b/imports/plugins/core/accounts/client/components/Invitations.js
@@ -0,0 +1,104 @@
+import React, { useState, useMemo, useCallback } from "react";
+import { useApolloClient } from "@apollo/react-hooks";
+import i18next from "i18next";
+import { Card, CardContent, makeStyles } from "@material-ui/core";
+import DataTable, { useDataTable } from "@reactioncommerce/catalyst/DataTable";
+import useCurrentShopId from "/imports/client/ui/hooks/useCurrentShopId";
+import { getAccountAvatar } from "/imports/plugins/core/accounts/client/helpers/helpers";
+import invitationsQuery from "../graphql/queries/invitations";
+
+const useStyles = makeStyles(() => ({
+ card: {
+ marginTop: "1rem"
+ }
+}));
+
+
+/**
+ * @summary Main invitations view
+ * @name Invitations
+ * @returns {React.Component} A React component
+ */
+function Invitations() {
+ const apolloClient = useApolloClient();
+ const [shopId] = useCurrentShopId();
+
+ // React-Table state
+ const [isLoading, setIsLoading] = useState(false);
+ const [pageCount, setPageCount] = useState(1);
+ const [tableData, setTableData] = useState([]);
+
+ const classes = useStyles();
+
+ // Create and memoize the column data
+ const columns = useMemo(() => [
+ {
+ Header: "",
+ accessor: (account) => getAccountAvatar(account),
+ id: "profilePicture"
+ }, {
+ Header: i18next.t("admin.invitationsTable.header.email"),
+ accessor: (row) => row.email,
+ id: "email"
+ }, {
+ Header: i18next.t("admin.invitationsTable.header.invitedBy"),
+ accessor: (row) => row.invitedBy.emailRecords[0].address,
+ id: "invitedBy"
+ }, {
+ Header: i18next.t("admin.invitationsTable.header.shop"),
+ accessor: (row) => row.shop.name,
+ id: "shop"
+ }, {
+ Header: i18next.t("admin.invitationsTable.header.groups"),
+ accessor: (row) => row.groups.reduce((allGroups, group) => `${allGroups}, ${group.name}`, "").substring(2),
+ id: "groups"
+ }
+ ], []);
+
+ const onFetchData = useCallback(async ({ pageIndex, pageSize }) => {
+ // Wait for shop id to be available before fetching products.
+ setIsLoading(true);
+ if (!shopId) {
+ return;
+ }
+
+ const { data } = await apolloClient.query({
+ query: invitationsQuery,
+ variables: {
+ shopIds: [shopId],
+ 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.invitations.nodes);
+ setPageCount(Math.ceil(data.invitations.totalCount / pageSize));
+
+ setIsLoading(false);
+ }, [apolloClient, shopId]);
+
+ const dataTableProps = useDataTable({
+ columns,
+ data: tableData,
+ isFilterable: false,
+ pageCount,
+ onFetchData,
+ getRowId: (row) => row._id
+ });
+
+ return (
+
+
+
+
+
+ );
+}
+
+export default Invitations;
diff --git a/imports/plugins/core/accounts/client/components/InviteShopMemberDialog.js b/imports/plugins/core/accounts/client/components/InviteShopMemberDialog.js
new file mode 100644
index 0000000000..9b032c91b0
--- /dev/null
+++ b/imports/plugins/core/accounts/client/components/InviteShopMemberDialog.js
@@ -0,0 +1,191 @@
+import React, { useState } from "react";
+import PropTypes from "prop-types";
+import i18next from "i18next";
+import SimpleSchema from "simpl-schema";
+import CloseIcon from "mdi-material-ui/Close";
+import Button from "@reactioncommerce/catalyst/Button";
+import TextField from "@reactioncommerce/catalyst/TextField";
+import Select from "@reactioncommerce/catalyst/Select";
+import useReactoForm from "reacto-form/cjs/useReactoForm";
+import muiOptions from "reacto-form/cjs/muiOptions";
+import { useMutation } from "@apollo/react-hooks";
+import { useSnackbar } from "notistack";
+import {
+ Box,
+ Grid,
+ CardActions,
+ CardHeader,
+ CardContent,
+ Dialog,
+ IconButton,
+ makeStyles
+} from "@material-ui/core";
+import inviteShopMemberMutation from "../graphql/mutations/inviteShopMember";
+
+const useStyles = makeStyles((theme) => ({
+ dialogPaper: {
+ overflow: "visible",
+ padding: theme.spacing(2)
+ },
+ cardContainer: {
+ alignItems: "center"
+ },
+ cardActions: {
+ padding: theme.spacing(2),
+ justifyContent: "flex-end"
+ },
+ hidden: {
+ display: "none"
+ },
+ visible: {
+ display: "block"
+ }
+}));
+
+const formSchema = new SimpleSchema({
+ name: {
+ type: String
+ },
+ email: {
+ type: String,
+ min: 3
+ }
+});
+const validator = formSchema.getFormValidator();
+
+/**
+ * InviteShopMember component
+ * @param {Object} props Component props
+ * @returns {React.Component} A React component
+ */
+function InviteShopMember({ isOpen, onClose, onSuccess, groups, shopId }) {
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [selectedGroups, setSelectedGroups] = useState([]);
+ const classes = useStyles();
+ const { enqueueSnackbar } = useSnackbar();
+
+ const [inviteShopMember] = useMutation(inviteShopMemberMutation, {
+ ignoreResults: true,
+ onCompleted() {
+ setIsSubmitting(false);
+ onSuccess();
+ },
+ onError() {
+ setIsSubmitting(false);
+ enqueueSnackbar(i18next.t("admin.groupCards.inviteStaffMemberDialog.saveFailed"), { variant: "error" });
+ }
+ });
+
+ const {
+ getFirstErrorMessage,
+ getInputProps,
+ hasErrors,
+ isDirty,
+ submitForm
+ } = useReactoForm({
+ async onSubmit(formData) {
+ setIsSubmitting(true);
+
+ await inviteShopMember({
+ variables: {
+ input: {
+ email: formData.email,
+ groupIds: selectedGroups.map((role) => role.value),
+ name: formData.name,
+ shopId
+ }
+ }
+ });
+
+ setIsSubmitting(false);
+ },
+ validator(formData) {
+ return validator(formSchema.clean(formData));
+ }
+ });
+
+ const handleSubmit = (event) => {
+ event.preventDefault();
+ submitForm();
+ };
+
+ const groupsForSelect = groups.map((group) => ({ value: group._id, label: group.name }));
+
+ return (
+
+
+
+
+ }
+ title={i18next.t("admin.groupCards.inviteStaffMemberDialog.title")}
+ />
+
+
+
+
+
+
+
+
+
+ setSelectedGroups(groupsToSelect)}
+ placeholder={i18next.t("admin.groupCards.inviteStaffMemberDialog.selectGroups")}
+ value={selectedGroups}
+ />
+
+
+
+
+
+
+ {i18next.t("app.cancel")}
+
+
+
+ {isSubmitting ? i18next.t("app.settings.saveProcessing") : i18next.t("app.saveChanges")}
+
+
+
+ );
+}
+
+InviteShopMember.propTypes = {
+ groups: PropTypes.arrayOf(PropTypes.object),
+ 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/components/accountsDashboard.js b/imports/plugins/core/accounts/client/components/accountsDashboard.js
deleted file mode 100644
index 7cadef9f5f..0000000000
--- a/imports/plugins/core/accounts/client/components/accountsDashboard.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import React, { Component } from "react";
-import PropTypes from "prop-types";
-import { Components } from "@reactioncommerce/reaction-components";
-import { i18next } from "/client/api";
-import sortUsersIntoGroups, { sortGroups } from "../helpers/accountsHelper";
-import DetailDrawer from "/imports/client/ui/components/DetailDrawer";
-import DetailDrawerButton from "/imports/client/ui/components/DetailDrawerButton";
-
-class AccountsDashboard extends Component {
- static propTypes = {
- accounts: PropTypes.array,
- adminGroups: PropTypes.array, // only admin groups
- groups: PropTypes.array // all groups including non-admin default groups
- };
-
- constructor(props) {
- super(props);
- const { accounts, adminGroups, groups } = this.props;
- const sortedGroups = sortUsersIntoGroups({ groups: sortGroups(adminGroups), accounts }) || [];
- const defaultSelectedGroup = sortedGroups[0];
-
- this.state = {
- accounts,
- groups: sortGroups(groups),
- adminGroups: sortedGroups,
- selectedGroup: defaultSelectedGroup
- };
- }
-
- // 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
- });
- }
-
- handleGroupSelect = (group) => {
- this.setState({ selectedGroup: group });
- };
-
- handleMethodLoad = () => {
- this.setState({ loading: true });
- };
-
- handleMethodDone = () => {
- this.setState({ loading: false });
- };
-
- renderGroupDetail = () => {
- const { groups, adminGroups, accounts } = this.state;
- return (
-
- );
- };
-
- renderGroupsTable(groups) {
- if (Array.isArray(groups)) {
- return (
-
- {this.state.loading && }
- {groups.map((group, index) => (
-
- ))}
-
- );
- }
-
- return null;
- }
-
- render() {
- return (
-
-
- {i18next.t("admin.dashboard.manageGroups")}
-
- {this.renderGroupsTable(this.state.adminGroups)}
-
- {this.renderGroupDetail()}
-
-
- );
- }
-}
-
-export default AccountsDashboard;
diff --git a/imports/plugins/core/accounts/client/components/adminInviteForm.js b/imports/plugins/core/accounts/client/components/adminInviteForm.js
deleted file mode 100644
index 2792d16059..0000000000
--- a/imports/plugins/core/accounts/client/components/adminInviteForm.js
+++ /dev/null
@@ -1,263 +0,0 @@
-import React, { Component } from "react";
-import PropTypes from "prop-types";
-import _ from "lodash";
-import { Components, registerComponent } from "@reactioncommerce/reaction-components";
-import ReactionAlerts from "/imports/plugins/core/layout/client/templates/layout/alerts/inlineAlerts";
-import { Reaction, i18next } from "/client/api";
-import Card from "@material-ui/core/Card";
-import CardContent from "@material-ui/core/CardContent";
-import CardHeader from "@material-ui/core/CardHeader";
-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";
-
-const iconComponents = {
- iconDismiss:
-};
-
-const inviteShopMember = gql`
- mutation inviteShopMember($input: InviteShopMemberInput!) {
- inviteShopMember(input: $input) {
- clientMutationId
- account {
- _id
- }
- }
- }
-`;
-
-class AdminInviteForm extends Component {
- static propTypes = {
- groups: PropTypes.array
- };
-
- constructor(props) {
- super(props);
- const { groups } = props;
-
- this.state = {
- alertId: "admin-invite-form",
- groups,
- name: "",
- email: "",
- group: getDefaultUserInviteGroup(groups)
- };
-
- this.onChange = this.onChange.bind(this);
- }
-
- // eslint-disable-next-line camelcase
- UNSAFE_componentWillReceiveProps(nextProps) {
- const { groups } = nextProps;
- this.setState({ groups, group: getDefaultUserInviteGroup(groups) });
- }
-
- onChange(event) {
- this.setState({ [event.target.name]: event.target.value });
- }
-
- handleGroupSelect = (event, group) => {
- this.setState({ group });
- };
-
- removeAlert = (oldAlert) => this.setState({
- 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 }
- ]);
-
- await mutation({
- variables: {
- input: {
- email: options.email,
- groupId: opaqueGroupId,
- name: options.name,
- shopId: opaqueShopId
- }
- }
- });
- }
-
- handleSubmit(event, mutation) {
- event.preventDefault();
- const { name, email, group, alertId } = this.state;
- const alertOptions = { placement: alertId, id: alertId, autoHide: 4000 };
-
- // if no group is selected, show alert that group is required to send invitation
- if (!group._id) {
- return ReactionAlerts.add(
- "A group is required to invite an admin",
- "danger",
- Object.assign({}, alertOptions, { i18nKey: "admin.groupsInvite.groupRequired" })
- );
- }
-
- const matchingAccount = getUserByEmail(email);
- const matchingEmail = matchingAccount &&
- matchingAccount.emails &&
- matchingAccount.emails.find((emailObject) => emailObject.address === email);
-
- const isEmailVerified = matchingEmail && matchingEmail.verified;
-
- const options = { email, name, shopId: Reaction.getShopId(), groupId: group._id };
-
- if (matchingAccount) {
- return Alerts.alert({
- title: i18next.t(`accountsUI.error.${isEmailVerified ? "userWithEmailAlreadyExists" : "inviteAlreadyPending"}`),
- text: i18next.t(`accountsUI.${isEmailVerified ? "promoteExistingAccountConfirm" : "sendNewInviteConfirm"}`),
- type: "warning",
- showCancelButton: true,
- showCloseButton: true,
- confirmButtonColor: "#98afbc",
- cancelButtonColor: "#98afbc",
- confirmButtonText: i18next.t("accountsUI.yes"),
- cancelButtonText: i18next.t("app.cancel")
- }, (isConfirm) => {
- if (isConfirm) {
- this.sendInvitation(options, mutation);
- }
- });
- }
-
- return this.sendInvitation(options, mutation);
- }
-
- renderDropDownButton() {
- const { group } = this.state;
-
- if (!group._id) {
- return null;
- }
- const buttonElement = (opt) => (
-
-
- {opt && opt.length && // add icon only if there's a list of options
-
- }
-
- );
-
- // current selected option and "owner" should not show in list options
- const dropOptions = this.state.groups.filter((grp) => grp._id !== group._id);
- if (!dropOptions.length) { return buttonElement(); } // do not use dropdown if only one option
-
- return (
-
- {dropOptions
- .map((grp, index) => (
-
- ))}
-
- );
- }
-
- renderForm() {
- return (
-
-
-
-
- {(mutationFunc, { data, error }) => (
-
- )}
-
-
-
- );
- }
-
- render() {
- return (
-
-
-
- {this.renderForm()}
-
-
- );
- }
-}
-
-registerComponent("AdminInviteForm", AdminInviteForm);
-
-export default AdminInviteForm;
diff --git a/imports/plugins/core/accounts/client/components/editGroup.js b/imports/plugins/core/accounts/client/components/editGroup.js
deleted file mode 100644
index 5091a04b67..0000000000
--- a/imports/plugins/core/accounts/client/components/editGroup.js
+++ /dev/null
@@ -1,413 +0,0 @@
-import { Meteor } from "meteor/meteor";
-import _ from "lodash";
-import React, { Component } from "react";
-import classnames from "classnames";
-import PropTypes from "prop-types";
-import { Components } from "@reactioncommerce/reaction-components";
-import ReactionAlerts from "/imports/plugins/core/layout/client/templates/layout/alerts/inlineAlerts";
-import { Reaction, i18next } from "/client/api";
-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,
- groups: PropTypes.array,
- onChangeGroup: PropTypes.func,
- selectedGroup: PropTypes.object
- };
-
- constructor(props) {
- super(props);
- const { accounts, selectedGroup, groups } = props;
- const alertId = "edit-group-comp";
-
- this.state = {
- alertOptions: { placement: alertId, id: alertId, autoHide: 4000 },
- selectedGroup: selectedGroup || {},
- isEditing: false,
- groups,
- accounts
- };
- }
-
- // eslint-disable-next-line camelcase
- UNSAFE_componentWillReceiveProps(nextProps) {
- const { groups, selectedGroup } = nextProps;
- this.setState({ groups, selectedGroup: selectedGroup || {} });
- }
-
- selectGroup = (grp) => (event) => {
- event.preventDefault();
- if (this.props.onChangeGroup) {
- this.props.onChangeGroup(grp);
- }
- this.setState({ isEditing: false });
- };
-
- groupListClass = (grp) => classnames({
- "groups-item-selected": grp._id === this.state.selectedGroup._id,
- "groups-list": true
- });
-
- removeAlert = (oldAlert) => this.setState({
- alertArray: this.state.alertArray.filter((alert) => !_.isEqual(alert, oldAlert))
- });
-
- createGroup = (groupData) => {
- Meteor.call("group/createGroup", groupData, Reaction.getShopId(), (err, res) => {
- if (err) {
- return ReactionAlerts.add(
- err.reason,
- "danger",
- Object.assign({}, this.state.alertOptions, { i18nKey: "admin.settings.createGroupError" })
- );
- }
-
- if (this.props.onChangeGroup) {
- this.props.onChangeGroup(res.group);
- }
-
- ReactionAlerts.add(
- "Created successfully",
- "success",
- Object.assign({}, this.state.alertOptions, { i18nKey: "admin.settings.createGroupSuccess" })
- );
-
- return this.setState({ isEditing: false });
- });
- };
-
- updateGroup = (groupId, groupData) => {
- Meteor.call("group/updateGroup", groupId, groupData, Reaction.getShopId(), (err, res) => {
- if (err) {
- return ReactionAlerts.add(
- err.reason,
- "danger",
- Object.assign({}, this.state.alertOptions, { i18nKey: "admin.settings.updateGroupError" })
- );
- }
-
- if (this.props.onChangeGroup) {
- this.props.onChangeGroup(res.group);
- }
-
- ReactionAlerts.add(
- "Created successfully",
- "success",
- Object.assign({}, this.state.alertOptions, { i18nKey: "admin.settings.updateGroupSuccess" })
- );
-
- return this.setState({ isEditing: false });
- });
- };
-
- showForm = ((grp) = {}) => (event) => {
- event.preventDefault();
- event.stopPropagation();
- this.setState({ isEditing: true, selectedGroup: grp });
- };
-
- renderGroupForm = () => {
- if (!this.state.isEditing) {
- return null;
- }
- if (_.isEmpty(this.state.selectedGroup)) {
- return (
-
- );
- }
- return (
-
- );
- };
-
- renderGroups() {
- return (
-
- {this.state.groups.map((grp, index) => (
-
-
- {grp.slug !== "owner" ?
- : null}
-
-
- ))}
-
-
-
-
- );
- }
-
- renderPermissionsList = () => {
- if (this.state.isEditing) {
- return null;
- }
- return (
-
- );
- };
-
- render() {
- const alertId = this.state.alertOptions.id;
- return (
-
-
-
-
-
-
- {this.renderGroups()}
- {this.renderGroupForm()}
- {this.renderPermissionsList()}
-
-
-
-
- );
- }
-}
-
-export default EditGroup;
diff --git a/imports/plugins/core/accounts/client/components/groupForm.js b/imports/plugins/core/accounts/client/components/groupForm.js
deleted file mode 100644
index 4ba58b9dfb..0000000000
--- a/imports/plugins/core/accounts/client/components/groupForm.js
+++ /dev/null
@@ -1,92 +0,0 @@
-import React, { Component } from "react";
-import PropTypes from "prop-types";
-import { Components, registerComponent } from "@reactioncommerce/reaction-components";
-
-class GroupForm extends Component {
- static propTypes = {
- createGroup: PropTypes.func,
- group: PropTypes.object,
- i18nKeyLabel: PropTypes.string,
- submitLabel: PropTypes.string,
- updateGroup: PropTypes.func
- };
-
- constructor(props) {
- super(props);
- const { name, description } = props.group;
-
- this.state = {
- name: name || "",
- description: description || ""
- };
- }
-
- // eslint-disable-next-line camelcase
- UNSAFE_componentWillReceiveProps(nextProps) {
- const { name, description } = nextProps.group;
- this.setState({ name, description });
- }
-
- onChange = (event) => {
- this.setState({ [event.target.name]: event.target.value });
- };
-
- handleSubmit = (event) => {
- event.preventDefault();
- if (this.props.createGroup) {
- return this.props.createGroup(this.state);
- }
- if (this.props.updateGroup) {
- return this.props.updateGroup(this.props.group._id, this.state);
- }
-
- return null;
- };
-
- render() {
- return (
-
- );
- }
-}
-
-registerComponent("GroupForm", GroupForm);
-
-export default GroupForm;
diff --git a/imports/plugins/core/accounts/client/components/groupHeader.js b/imports/plugins/core/accounts/client/components/groupHeader.js
deleted file mode 100644
index 981ce1f101..0000000000
--- a/imports/plugins/core/accounts/client/components/groupHeader.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import React from "react";
-import PropTypes from "prop-types";
-import { Components, registerComponent } from "@reactioncommerce/reaction-components";
-
-const GroupHeader = ({ columnName }) => {
- const showSortItems = false; // to be used to show sort icons. When filters fields are available, we'll show icons
-
- if (columnName === "name") {
- return (
-
- {showSortItems && }
-
- {showSortItems && }
-
- );
- }
- if (columnName === "email") {
- return (
-
-
- {showSortItems && }
-
- );
- }
- if (columnName === "createdAt") {
- return (
-
-
- {showSortItems && }
-
- );
- }
- return null;
-};
-
-GroupHeader.propTypes = {
- columnName: PropTypes.string,
- numberOfRows: PropTypes.number
-};
-
-registerComponent("GroupHeader", GroupHeader);
-
-export default GroupHeader;
diff --git a/imports/plugins/core/accounts/client/components/groupsTable.js b/imports/plugins/core/accounts/client/components/groupsTable.js
deleted file mode 100644
index 5ae447715a..0000000000
--- a/imports/plugins/core/accounts/client/components/groupsTable.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import React from "react";
-import PropTypes from "prop-types";
-import classnames from "classnames";
-import { Components, registerComponent } from "@reactioncommerce/reaction-components";
-import { SortableTable } from "/imports/plugins/core/ui/client/components";
-
-const GroupsTable = (props) => {
- const { group } = props;
- const fields = ["name", "email", "createdAt", "dropdown", "button"];
-
- const tableClass = (length) => classnames({
- "accounts-group-table": true,
- "empty-table": !length
- });
-
- const columnMetadata = fields.map((columnName) => ({
- Header: ,
- accessor: "",
- // TODO: Review this line - copied disable line from shippo carriers.js
- Cell: (data) => { // eslint-disable-line
- return ;
- }
- }));
-
- return (
-
-
-
-
-
-
- );
-};
-
-GroupsTable.propTypes = {
- accounts: PropTypes.array,
- group: PropTypes.object,
- groups: PropTypes.array,
- onGroupSelect: PropTypes.func
-};
-
-registerComponent("GroupsTable", GroupsTable);
-
-export default GroupsTable;
diff --git a/imports/plugins/core/accounts/client/components/groupsTableButton.js b/imports/plugins/core/accounts/client/components/groupsTableButton.js
deleted file mode 100644
index c2c1b231b9..0000000000
--- a/imports/plugins/core/accounts/client/components/groupsTableButton.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import React from "react";
-import PropTypes from "prop-types";
-import { Components, registerComponent } from "@reactioncommerce/reaction-components";
-
-/**
- * @summary React stateless component for "remove from group" button for groupTable
- * @memberof Accounts
- * @example
- * @param {Object} props - React PropTypes
- * @property {Object} account - User account object
- * @property {Object} group - Group data
- * @property {Function} handleRemoveUserFromGroup - function to call on button click
- * @returns {Node} React node containing wrapped button
- */
-const GroupsTableButton = ({ account, group, handleRemoveUserFromGroup }) => (
-
-
-
-);
-
-GroupsTableButton.propTypes = {
- account: PropTypes.object,
- group: PropTypes.object, // current group in interaction
- handleRemoveUserFromGroup: PropTypes.func
-};
-
-registerComponent("GroupsTableButton", GroupsTableButton);
-
-export default GroupsTableButton;
diff --git a/imports/plugins/core/accounts/client/components/groupsTableCell.js b/imports/plugins/core/accounts/client/components/groupsTableCell.js
deleted file mode 100644
index 0d70b1386e..0000000000
--- a/imports/plugins/core/accounts/client/components/groupsTableCell.js
+++ /dev/null
@@ -1,121 +0,0 @@
-import React from "react";
-import _ from "lodash";
-import PropTypes from "prop-types";
-import { Components, registerComponent, withMoment } from "@reactioncommerce/reaction-components";
-import { getUserAvatar } from "/imports/plugins/core/accounts/client/helpers/helpers";
-
-const GroupsTableCell = (props) => {
- const {
- account,
- columnName,
- group,
- adminGroups,
- handleRemoveUserFromGroup,
- handleUserGroupChange,
- moment
- } = props;
-
- const email = _.get(account, "emails[0].address", "");
- const groups = adminGroups;
- const userAvatar = getUserAvatar(account);
- const createdAt = (moment && moment(account.createdAt).format("MMM Do")) || account.createdAt.toLocaleString();
-
- if (columnName === "name") {
- // use first part of email, if account has no name
- const name = account.name || email.split("@")[0];
- return (
-
- {userAvatar}
- {name}
-
- );
- }
-
- if (columnName === "email") {
- return (
-
- {email}
-
- );
- }
-
- if (columnName === "createdAt") {
- return (
-
-
- {createdAt}
-
-
- );
- }
-
- if (columnName === "dropdown") {
- const groupName = group.name && _.startCase(group.name);
- const groupNameSpan = {groupName} ;
- const ownerGroup = groups.find((grp) => grp.slug === "owner") || {};
-
- // Permission check. Remove owner option, if user is not current owner.
- // Also remove groups user does not have roles to manage. This is also checked on the server
- const dropOptions = groups || [];
-
- if (dropOptions.length < 2) {
- return groupNameSpan;
- }
-
- const dropDownButton = (opt) => ( // eslint-disable-line
-
-
-
- {opt && opt.length > 1 && // add icon only if there's more than the current group
-
- }
-
-
- );
-
- const { onMethodDone, onMethodLoad } = props;
- return (
-
-
- {dropOptions
- .filter((grp) => grp._id !== group._id)
- .map((grp, index) => (
-
- ))}
-
-
- );
- }
-
- if (columnName === "button") {
- return ;
- }
-
- return null;
-};
-
-GroupsTableCell.propTypes = {
- account: PropTypes.object,
- adminGroups: PropTypes.array, // all admin groups
- columnName: PropTypes.string,
- group: PropTypes.object, // current group in interaction
- handleRemoveUserFromGroup: PropTypes.func,
- handleUserGroupChange: PropTypes.func,
- moment: PropTypes.func,
- onMethodDone: PropTypes.func,
- onMethodLoad: PropTypes.func
-};
-
-registerComponent("GroupsTableCell", GroupsTableCell, withMoment);
-
-export default withMoment(GroupsTableCell);
diff --git a/imports/plugins/core/accounts/client/components/manageGroups.js b/imports/plugins/core/accounts/client/components/manageGroups.js
deleted file mode 100644
index a9a3edae75..0000000000
--- a/imports/plugins/core/accounts/client/components/manageGroups.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import React, { Component } from "react";
-import PropTypes from "prop-types";
-import { Components, registerComponent } from "@reactioncommerce/reaction-components";
-import withStyles from "@material-ui/core/styles/withStyles";
-import { getInvitableGroups } from "../helpers/accountsHelper";
-
-const styles = (theme) => ({
- editGroup: {
- paddingTop: theme.spacing()
- }
-});
-
-class ManageGroups extends Component {
- static propTypes = {
- accounts: PropTypes.array,
- adminGroups: PropTypes.array,
- classes: PropTypes.object,
- group: PropTypes.object,
- groups: PropTypes.array,
- isAdmin: PropTypes.bool,
- onChangeGroup: PropTypes.func
- };
-
- constructor(props) {
- super(props);
-
- this.state = {
- accounts: props.accounts,
- adminGroups: props.adminGroups,
- group: props.group,
- groups: props.groups
- };
- }
-
- // eslint-disable-next-line camelcase
- UNSAFE_componentWillReceiveProps(nextProps) {
- const { group, groups, adminGroups, accounts } = nextProps;
- this.setState({ group, groups, accounts, adminGroups });
- }
-
- render() {
- const { classes } = this.props;
- // this gets a list of groups the user can invite to, we show only those in the dropdown
- // see doc for getInvitableGroups in helpers/accountsHelper.js
- const groupsInvitable = getInvitableGroups(this.state.adminGroups);
- return (
-
- { groupsInvitable && groupsInvitable.length &&
-
- }
- {this.props.isAdmin &&
-
-
-
- }
-
- );
- }
-}
-
-registerComponent("ManageGroups", ManageGroups, [
- withStyles(styles, { name: "RuiManageGroups" })
-]);
-
-export default withStyles(styles, { name: "RuiManageGroups" })(ManageGroups);
diff --git a/imports/plugins/core/accounts/client/components/permissionsList.js b/imports/plugins/core/accounts/client/components/permissionsList.js
deleted file mode 100644
index 6d282afad0..0000000000
--- a/imports/plugins/core/accounts/client/components/permissionsList.js
+++ /dev/null
@@ -1,138 +0,0 @@
-import React, { Component } from "react";
-import PropTypes from "prop-types";
-import _ from "lodash";
-import { Components, registerComponent } from "@reactioncommerce/reaction-components";
-
-/**
- * @function
- * @description helper to resolve toggled permissions
- * @param {Object} permission permission to check
- * @returns {Array} list of all parent and child permissions when a parent permission is toggled
- */
-function resolvePermissions(permission) {
- const result = [];
-
- if (permission.name) {
- result.push(permission.name);
- for (const pkgPermissions of permission.permissions) {
- result.push(pkgPermissions.permission);
- }
- } else {
- result.push(permission.permission);
- }
-
- return result;
-}
-
-/**
- * @function
- * @description helper to remove all array items in "old" from "current"
- * @param {Array} current current array items
- * @param {Array} old older array items
- * @returns {Array} updated permissions array
- */
-function removePermissions(current, old) {
- const currentArray = [...current];
-
- old.forEach((val) => {
- _.remove(currentArray, (item) => item === val);
- });
- return currentArray;
-}
-
-class PermissionsList extends Component {
- static propTypes = {
- createGroup: PropTypes.func,
- group: PropTypes.object,
- permissions: PropTypes.array,
- updateGroup: PropTypes.func
- };
-
- constructor(props) {
- super(props);
-
- this.state = {
- group: props.group
- };
- }
-
- // eslint-disable-next-line camelcase
- UNSAFE_componentWillReceiveProps(nextProps) {
- this.setState({ group: nextProps.group });
- }
-
- togglePermission = (toggledPermission) => (event, checked) => {
- const groupData = Object.assign({}, this.state.group);
- const permissions = resolvePermissions(toggledPermission);
- if (!groupData.permissions) {
- groupData.permissions = [];
- }
- if (checked) {
- groupData.permissions = _.uniq([...groupData.permissions, ...permissions]);
- } else {
- groupData.permissions = removePermissions(groupData.permissions, permissions);
- }
-
- if (this.props.updateGroup) {
- return this.props.updateGroup(this.state.group._id, groupData);
- }
-
- return null;
- };
-
- checked = (permission) => {
- if (_.includes(this.state.group.permissions, permission)) {
- return true;
- }
- 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() {
- return (
-
- {this.renderPermissions(_.compact(this.props.permissions))}
-
- );
- }
-}
-
-registerComponent("PermissionsList", PermissionsList);
-
-export default PermissionsList;
diff --git a/imports/plugins/core/accounts/client/containers/accountsDashboardContainer.js b/imports/plugins/core/accounts/client/containers/accountsDashboardContainer.js
deleted file mode 100644
index 9adcd16ce7..0000000000
--- a/imports/plugins/core/accounts/client/containers/accountsDashboardContainer.js
+++ /dev/null
@@ -1,142 +0,0 @@
-import { compose, withProps } from "recompose";
-import Alert from "sweetalert2";
-import { registerComponent, composeWithTracker, 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 AccountsDashboard from "../components/accountsDashboard";
-import addAccountToGroupMutation from "./addAccountToGroup.graphql";
-import removeAccountFromGroupMutation from "./removeAccountFromGroup.graphql";
-
-const addAccountToGroupMutate = simpleGraphQLClient.createMutationFunction(addAccountToGroupMutation);
-const removeAccountFromGroupMutate = simpleGraphQLClient.createMutationFunction(removeAccountFromGroupMutation);
-
-/**
- * @summary Show confirmation alert to verify the user wants to remove the user from the group
- * @return {Object} Object with `value` prop that is truthy if they want to continue
- */
-function alertConfirmRemoveUser() {
- return Alert({
- title: i18next.t("admin.settings.removeUser"),
- text: i18next.t("admin.settings.removeUserWarn"),
- type: "warning",
- showCancelButton: true,
- cancelButtonText: i18next.t("admin.settings.cancel"),
- confirmButtonText: i18next.t("admin.settings.continue")
- });
-}
-
-/**
- * @summary Show confirmation alert to verify the user wants to change the shop owner
- * @return {Object} Object with `value` prop that is truthy if they want to continue
- */
-function alertConfirmChangeOwner() {
- return Alert({
- title: i18next.t("admin.settings.changeOwner"),
- text: i18next.t("admin.settings.changeShopOwnerWarn"),
- type: "warning",
- showCancelButton: true,
- cancelButtonText: i18next.t("admin.settings.cancel"),
- confirmButtonText: i18next.t("admin.settings.continue")
- });
-}
-
-const handlers = {
- 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) {
- if (onMethodDone) onMethodDone();
- return null;
- }
- }
- }
-
- try {
- const [
- opaqueAccountId,
- opaqueGroupId
- ] = await getOpaqueIds([
- { namespace: "Account", id: account._id },
- { namespace: "Group", id: groupId }
- ]);
-
- await addAccountToGroupMutate({ accountId: opaqueAccountId, groupId: opaqueGroupId });
- } catch (error) {
- Alerts.toast(i18next.t("admin.groups.addUserError", { err: error.message }), "error");
- }
-
- if (onMethodDone) onMethodDone();
- return null;
- };
- },
-
- handleRemoveUserFromGroup(account, groupId) {
- return async () => {
- const { value } = await alertConfirmRemoveUser();
- 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 });
- } catch (error) {
- Alerts.toast(i18next.t("admin.groups.removeUserError", { err: error.message }), "error");
- }
-
- return null;
- };
- }
-};
-
-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),
- withProps(handlers)
-]);
-
-export default compose(
- withIsAdmin,
- composeWithTracker(composer),
- withProps(handlers)
-)(AccountsDashboard);
diff --git a/imports/plugins/core/accounts/client/containers/addAccountToGroup.graphql b/imports/plugins/core/accounts/client/containers/addAccountToGroup.graphql
deleted file mode 100644
index 15c173e586..0000000000
--- a/imports/plugins/core/accounts/client/containers/addAccountToGroup.graphql
+++ /dev/null
@@ -1,7 +0,0 @@
-mutation ($accountId: ID!, $groupId: ID!) {
- addAccountToGroup(input: { accountId: $accountId, groupId: $groupId }) {
- group {
- _id
- }
- }
-}
diff --git a/imports/plugins/core/accounts/client/containers/editGroupContainer.js b/imports/plugins/core/accounts/client/containers/editGroupContainer.js
deleted file mode 100644
index 2f1f1babe7..0000000000
--- a/imports/plugins/core/accounts/client/containers/editGroupContainer.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import { registerComponent } from "@reactioncommerce/reaction-components";
-import EditGroup from "../components/editGroup";
-
-registerComponent("EditGroup", EditGroup);
-
-export default EditGroup;
diff --git a/imports/plugins/core/accounts/client/containers/removeAccountFromGroup.graphql b/imports/plugins/core/accounts/client/containers/removeAccountFromGroup.graphql
deleted file mode 100644
index 66e9120d9a..0000000000
--- a/imports/plugins/core/accounts/client/containers/removeAccountFromGroup.graphql
+++ /dev/null
@@ -1,7 +0,0 @@
-mutation ($accountId: ID!, $groupId: ID!) {
- removeAccountFromGroup(input: { accountId: $accountId, groupId: $groupId }) {
- group {
- _id
- }
- }
-}
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..5c67c78c13
--- /dev/null
+++ b/imports/plugins/core/accounts/client/graphql/mutations/createGroup.js
@@ -0,0 +1,11 @@
+import gql from "graphql-tag";
+
+export default gql`
+ mutation createAccountGroup($input: CreateAccountGroupInput!) {
+ createAccountGroup(input: $input) {
+ group {
+ name
+ }
+ }
+ }
+`;
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..a5c08c8e63
--- /dev/null
+++ b/imports/plugins/core/accounts/client/graphql/mutations/inviteShopMember.js
@@ -0,0 +1,9 @@
+import gql from "graphql-tag";
+
+export default gql`
+ mutation inviteShopMember($input: InviteShopMemberInput!) {
+ inviteShopMember(input: $input) {
+ clientMutationId
+ }
+ }
+`;
diff --git a/imports/plugins/core/accounts/client/graphql/mutations/removeAccountFromGroup.js b/imports/plugins/core/accounts/client/graphql/mutations/removeAccountFromGroup.js
new file mode 100644
index 0000000000..36c812c7ee
--- /dev/null
+++ b/imports/plugins/core/accounts/client/graphql/mutations/removeAccountFromGroup.js
@@ -0,0 +1,11 @@
+import gql from "graphql-tag";
+
+export default gql`
+ mutation ($accountId: ID!, $groupId: ID!) {
+ removeAccountFromGroup(input: { accountId: $accountId, groupId: $groupId }) {
+ group {
+ _id
+ }
+ }
+ }
+`;
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..88b11697f5
--- /dev/null
+++ b/imports/plugins/core/accounts/client/graphql/mutations/updateGroup.js
@@ -0,0 +1,11 @@
+import gql from "graphql-tag";
+
+export default gql`
+ mutation updateAccountGroup($input: UpdateAccountGroupInput!) {
+ updateAccountGroup(input: $input) {
+ group {
+ _id
+ }
+ }
+ }
+`;
diff --git a/imports/plugins/core/accounts/client/graphql/mutations/updateGroupsForAccounts.js b/imports/plugins/core/accounts/client/graphql/mutations/updateGroupsForAccounts.js
new file mode 100644
index 0000000000..e0f5f08622
--- /dev/null
+++ b/imports/plugins/core/accounts/client/graphql/mutations/updateGroupsForAccounts.js
@@ -0,0 +1,11 @@
+import gql from "graphql-tag";
+
+export default gql`
+ mutation ($input: UpdateGroupsForAccountsInput!) {
+ updateGroupsForAccounts(input: $input) {
+ accounts {
+ _id
+ }
+ }
+ }
+`;
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..c370be253a
--- /dev/null
+++ b/imports/plugins/core/accounts/client/graphql/queries/accounts.js
@@ -0,0 +1,41 @@
+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
+ }
+ groups {
+ nodes {
+ _id
+ name
+ }
+ }
+ name
+ }
+ totalCount
+ }
+ }
+`;
diff --git a/imports/plugins/core/accounts/client/graphql/queries/customers.js b/imports/plugins/core/accounts/client/graphql/queries/customers.js
new file mode 100644
index 0000000000..f41ae75fae
--- /dev/null
+++ b/imports/plugins/core/accounts/client/graphql/queries/customers.js
@@ -0,0 +1,33 @@
+import gql from "graphql-tag";
+
+export default gql`
+ query customers(
+ $first: ConnectionLimitInt,
+ $last: ConnectionLimitInt,
+ $before: ConnectionCursor,
+ $after: ConnectionCursor,
+ $offset: Int,
+ $sortBy: AccountSortByField,
+ $sortOrder: SortOrder
+ ) {
+ customers(
+ 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/graphql/queries/invitations.js b/imports/plugins/core/accounts/client/graphql/queries/invitations.js
new file mode 100644
index 0000000000..a6a954e57d
--- /dev/null
+++ b/imports/plugins/core/accounts/client/graphql/queries/invitations.js
@@ -0,0 +1,38 @@
+import gql from "graphql-tag";
+
+export default gql`
+ query invitations(
+ $shopIds: [ID],
+ $first: ConnectionLimitInt,
+ $last: ConnectionLimitInt,
+ $offset: Int,
+ $sortBy: AccountSortByField,
+ $sortOrder: SortOrder
+ ) {
+ invitations(
+ shopIds: $shopIds,
+ first: $first,
+ last: $last,
+ offset: $offset,
+ sortBy: $sortBy,
+ sortOrder: $sortOrder
+ ) {
+ nodes {
+ _id
+ groups {
+ name
+ }
+ email
+ shop {
+ name
+ }
+ invitedBy {
+ emailRecords {
+ address
+ }
+ }
+ }
+ totalCount
+ }
+ }
+`;
diff --git a/imports/plugins/core/accounts/client/graphql/queries/roles.js b/imports/plugins/core/accounts/client/graphql/queries/roles.js
new file mode 100644
index 0000000000..41667a3f89
--- /dev/null
+++ b/imports/plugins/core/accounts/client/graphql/queries/roles.js
@@ -0,0 +1,11 @@
+import gql from "graphql-tag";
+
+export default gql`
+ query ($shopId: ID!) {
+ roles(shopId: $shopId) {
+ nodes {
+ name
+ }
+ }
+ }
+`;
diff --git a/imports/plugins/core/accounts/client/helpers/accountsHelper.js b/imports/plugins/core/accounts/client/helpers/accountsHelper.js
deleted file mode 100644
index 71b6d5ba3d..0000000000
--- a/imports/plugins/core/accounts/client/helpers/accountsHelper.js
+++ /dev/null
@@ -1,93 +0,0 @@
-import _ from "lodash";
-import { Reaction } from "/client/api";
-import * as Collections from "/lib/collections";
-
-/**
- * @method sortUsersIntoGroups
- * @memberof Accounts
- * @summary helper - client puts each full user object into an array on the group they belong
- * @param {Array} accounts - list of user account objects
- * @param {Array} groups - list of permission groups
- * @returns {Array} - array of groups, each having a `users` field
- */
-export default function sortUsersIntoGroups({ accounts, groups }) {
- const newGroups = groups.map((group) => {
- const matchingAccounts = accounts.filter((acc) => acc.groups && acc.groups.indexOf(group._id) > -1);
- group.users = _.compact(matchingAccounts);
- return group;
- });
- const accountsWithoutGroup = accounts.filter((acc) => !acc.groups || acc.groups.length === 0);
- if (accountsWithoutGroup.length) {
- newGroups.push({
- name: "Not In Any Group",
- slug: "_none",
- users: accountsWithoutGroup
- });
- }
- return newGroups;
-}
-
-/**
- * @method sortGroups
- * @memberof Accounts
- * @summary Sort to display higher permission groups and "owner" at the top
- * @param {Array} groups [description]
- * @returns {Array} [description]
- */
-export function sortGroups(groups) {
- return groups.sort((prev, next) => {
- if (next.slug === "owner") { return 1; } // owner tops
- return next.permissions.length - prev.permissions.length;
- });
-}
-
-/**
- * @method getInvitableGroups
- * @memberof Accounts
- * @summary helper - client This generates a list of groups the user can invite to.
- * It filters out the owner group (because you cannot invite directly to an existing shop as owner)
- * It also filters out groups that the user does not have needed permissions to invite to.
- * All these are also checked by the Meteor method, so this is done to prevent trying to invite and getting error
- * @param {Array} groups - list of user account objects
- * @returns {Array} - array of groups or empty object
- */
-export function getInvitableGroups(groups) {
- return groups || [];
-}
-
-/**
- * @method getDefaultUserInviteGroup
- * @memberof Accounts
- * @summary user's default invite groups is the group they belong
- * if the user belongs to owner group, it defaults to shop manager (because you cannot invite directly
- * to an existing shop as owner). If no match still, use the first of the groups passed
- * (e.g in case of Marketplace owner accessing a merchant shop)
- * @param {Array} groups [description]
- * @returns {Object} [description]
- */
-export function getDefaultUserInviteGroup(groups) {
- let result;
- const user = Collections.Accounts.findOne({ userId: Reaction.getUserId() });
- result = groups.find((grp) => user && user.groups.indexOf(grp._id) > -1);
-
- if (result && result.slug === "owner") {
- result = groups.find((grp) => grp.slug === "shop manager");
- }
-
- if (!result) {
- result = groups.find((firstGroup) => firstGroup);
- }
-
- return result;
-}
-
-/**
- * @method getUserByEmail
- * @memberOf Accounts
- * @summary Returns a user that matches the email address
- * @param {String} email email address
- * @returns {Object} account of user from provided email address
- */
-export function getUserByEmail(email) {
- return Collections.Accounts.findOne({ "emails.address": email });
-}
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 (
,
- sidebarI18nLabel: "admin.dashboard.accountsLabel"
+ sidebarI18nLabel: "admin.accounts.accountsLabel"
});
registerOperatorRoute({
diff --git a/imports/plugins/core/accounts/client/templates/accounts.html b/imports/plugins/core/accounts/client/templates/accounts.html
deleted file mode 100644
index 53358781b7..0000000000
--- a/imports/plugins/core/accounts/client/templates/accounts.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
- {{> Template.dynamic template=tpl}}
-
\ No newline at end of file
diff --git a/imports/plugins/core/accounts/server/methods/group/createGroup.js b/imports/plugins/core/accounts/server/methods/group/createGroup.js
deleted file mode 100644
index bb70e0dedc..0000000000
--- a/imports/plugins/core/accounts/server/methods/group/createGroup.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import _ from "lodash";
-import Logger from "@reactioncommerce/logger";
-import { check, Match } from "meteor/check";
-import Reaction from "/imports/plugins/core/core/server/Reaction";
-import ReactionError from "@reactioncommerce/reaction-error";
-import { Groups } from "/lib/collections";
-
-/**
- * @name group/createGroup
- * @method
- * @memberof Group/Methods
- * @summary Creates a new permission group for a shop
- * It creates permission group for a given shop with passed in roles
- * @param {Object} groupData - info about group to create
- * @param {String} groupData.name - name of the group to be created
- * @param {String} groupData.description - Optional description of the group to be created
- * @param {Array} groupData.permissions - permissions to assign to the group being created
- * @param {String} shopId - id of the shop the group belongs to
- * @returns {Object} - `object.status` of 200 on success or Error object on failure
- */
-export default function createGroup(groupData, shopId) {
- check(groupData, Object);
- check(groupData.name, String);
- check(groupData.description, Match.Optional(String));
- check(groupData.permissions, Match.Optional([String]));
- check(shopId, String);
- let _id;
-
- // we are limiting group method actions to only users with admin roles
- // this also include shop owners, since they have the `admin` role in their Roles.GLOBAL_GROUP
- if (!Reaction.hasPermission("reaction:legacy:groups/create", Reaction.getUserId(), shopId)) {
- throw new ReactionError("access-denied", "Access Denied");
- }
-
- const defaultCustomerGroupForShop = Groups.findOne({ slug: "customer", shopId }) || {};
- const defaultAdminPermissions = (defaultCustomerGroupForShop.permissions || []).concat("dashboard");
- const newGroupData = Object.assign({}, groupData, {
- slug: Reaction.getSlug(groupData.name), shopId
- });
-
- if (!newGroupData.permissions) {
- newGroupData.permissions = [];
- }
-
- newGroupData.permissions = _.uniq([...newGroupData.permissions, ...defaultAdminPermissions]);
-
- // ensure one group type per shop
- const groupExists = Groups.findOne({ slug: newGroupData.slug, shopId });
- if (groupExists) {
- throw new ReactionError("conflict", "Group already exist for this shop");
- }
- try {
- _id = Groups.insert(newGroupData);
- } catch (error) {
- Logger.error(error);
- throw new ReactionError("invalid-parameter", "Bad request");
- }
-
- return { status: 200, group: Groups.findOne({ _id }) };
-}
diff --git a/imports/plugins/core/accounts/server/methods/group/index.js b/imports/plugins/core/accounts/server/methods/group/index.js
deleted file mode 100644
index de69c739ac..0000000000
--- a/imports/plugins/core/accounts/server/methods/group/index.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import createGroup from "./createGroup";
-import updateGroup from "./updateGroup";
-
-/**
- * @file Methods for creating and managing admin user permission groups.
- * Run these methods using `Meteor.call()`.
- * @example Meteor.call("group/createGroup", sampleCustomerGroup, shop._id)
- * @namespace Group/Methods
-*/
-
-export default {
- "group/createGroup": createGroup,
- "group/updateGroup": updateGroup
-};
diff --git a/imports/plugins/core/accounts/server/methods/group/updateGroup.js b/imports/plugins/core/accounts/server/methods/group/updateGroup.js
deleted file mode 100644
index 5056f66275..0000000000
--- a/imports/plugins/core/accounts/server/methods/group/updateGroup.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import { check } from "meteor/check";
-import Reaction from "/imports/plugins/core/core/server/Reaction";
-import ReactionError from "@reactioncommerce/reaction-error";
-import { Groups } from "/lib/collections";
-
-/**
- * @name group/updateGroup
- * @method
- * @memberof Group/Methods
- * @summary Updates a permission group for a shop.
- * Change the details of a group (name, desc, permissions etc) to the values passed in.
- * @param {Object} groupId - group to be updated
- * @param {Object} newGroupData - updated group info (similar to current group data)
- * slug remains untouched; used as key in querying
- * @param {String} shopId - id of the shop the group belongs to
- * @returns {Object} - `object.status` of 200 on success or Error object on failure
- */
-export default function updateGroup(groupId, newGroupData, shopId) {
- check(groupId, String);
- check(newGroupData, Object);
- check(shopId, String);
-
- // we are limiting group method actions to only users with `reaction:legacy:groups/update` permissions
- if (!Reaction.hasPermission("reaction:legacy:groups/update", Reaction.getUserId(), shopId)) {
- throw new ReactionError("access-denied", "Access Denied");
- }
-
- // 1. Update the group data
- const update = newGroupData;
- delete update.slug; // slug remains constant because it's used as key in querying. So we remove it if it was passed
-
- const group = Groups.findOne({ _id: groupId }) || {};
-
- // prevent edits on owner. Owner groups is the default containing all roles, and as such should be untouched
- if (group.slug === "owner") {
- throw new ReactionError("invalid-parameter", "Bad request");
- }
-
- const updatedGroup = Groups.update({ _id: groupId, shopId }, { $set: update });
-
- if (updatedGroup === 1) {
- return { status: 200, group: Groups.findOne({ _id: groupId }) };
- }
-
- throw new ReactionError("server-error", "Update not successful");
-}
diff --git a/imports/plugins/core/accounts/server/methods/index.js b/imports/plugins/core/accounts/server/methods/index.js
index 23a74e4375..69397901dd 100644
--- a/imports/plugins/core/accounts/server/methods/index.js
+++ b/imports/plugins/core/accounts/server/methods/index.js
@@ -1,5 +1,3 @@
-import getUserId from "./getUserId";
-import groupMethods from "./group";
import setActiveShopId from "./setActiveShopId";
import updateEmailAddress from "./updateEmailAddress";
import verifyAccount from "./verifyAccount";
@@ -21,7 +19,5 @@ import verifyAccount from "./verifyAccount";
export default {
"accounts/setActiveShopId": setActiveShopId,
"accounts/updateEmailAddress": updateEmailAddress,
- "accounts/verifyAccount": verifyAccount,
- "reaction/getUserId": getUserId,
- ...groupMethods
+ "accounts/verifyAccount": verifyAccount
};