-
Notifications
You must be signed in to change notification settings - Fork 83
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #276 from outgrow/outgrow-accounts-page-graphql
GraphQL-powered Accounts page
- Loading branch information
Showing
44 changed files
with
1,415 additions
and
1,744 deletions.
There are no files selected for viewing
43 changes: 43 additions & 0 deletions
43
imports/plugins/core/accounts/client/components/Accounts.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<Fragment> | ||
<Tabs value={currentTab} onChange={(event, value) => setCurrentTab(value)}> | ||
<Tab label={i18next.t("admin.accounts.tabs.staff")} /> | ||
<Tab label={i18next.t("admin.accounts.tabs.customers")} /> | ||
<Tab label={i18next.t("admin.accounts.tabs.invites")} /> | ||
</Tabs> | ||
|
||
<Divider /> | ||
|
||
{currentTab === 0 && | ||
<GroupCards /> | ||
} | ||
|
||
{currentTab === 1 && | ||
<Customers /> | ||
} | ||
|
||
{currentTab === 2 && | ||
<Invitations /> | ||
} | ||
</Fragment> | ||
); | ||
} | ||
|
||
export default Accounts; |
122 changes: 122 additions & 0 deletions
122
imports/plugins/core/accounts/client/components/AccountsTable.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<Fragment> | ||
{selectedRows && !props.isLoadingGroups && | ||
<GroupSelectorDialog | ||
isOpen={isGroupSelectorVisible} | ||
onSuccess={() => null} | ||
onClose={() => setGroupSelectorVisibility(false)} | ||
accounts={tableData.filter((account) => selectedRows.includes(account._id))} | ||
groups={props.groups} | ||
/> | ||
} | ||
<DataTable | ||
{...dataTableProps} | ||
actionMenuProps={{ options }} | ||
isLoading={isLoading} | ||
/> | ||
</Fragment> | ||
); | ||
} | ||
|
||
AccountsTable.propTypes = { | ||
group: PropTypes.shape({ | ||
_id: PropTypes.string.isRequired | ||
}), | ||
groups: PropTypes.arrayOf(PropTypes.object), | ||
isLoadingGroups: PropTypes.bool | ||
}; | ||
|
||
export default AccountsTable; |
222 changes: 222 additions & 0 deletions
222
imports/plugins/core/accounts/client/components/CreateOrEditGroupDialog.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<Dialog | ||
classes={{ paper: classes.dialogPaper }} | ||
open={isOpen} | ||
onClose={onClose} | ||
fullWidth | ||
maxWidth="sm" | ||
> | ||
<CardHeader | ||
action={ | ||
<IconButton aria-label="close" onClick={onClose}> | ||
<CloseIcon /> | ||
</IconButton> | ||
} | ||
title={i18next.t("admin.groupCards.createOrUpdateGroupDialog.title")} | ||
/> | ||
<CardContent> | ||
<Grid container spacing={1} className={classes.cardContainer}> | ||
<Grid item sm={12}> | ||
<TextField | ||
error={hasErrors(["name"])} | ||
fullWidth | ||
helperText={getFirstErrorMessage(["name"])} | ||
label={i18next.t("admin.groupCards.createOrUpdateGroupDialog.name")} | ||
{...getInputProps("name", muiOptions)} | ||
/> | ||
</Grid> | ||
<Grid item sm={12}> | ||
<TextField | ||
error={hasErrors(["slug"])} | ||
fullWidth | ||
helperText={getFirstErrorMessage(["slug"])} | ||
label={i18next.t("admin.groupCards.createOrUpdateGroupDialog.slug")} | ||
{...getInputProps("slug", muiOptions)} | ||
/> | ||
</Grid> | ||
<Grid item sm={12}> | ||
<TextField | ||
error={hasErrors(["description"])} | ||
fullWidth | ||
helperText={getFirstErrorMessage(["description"])} | ||
label={i18next.t("admin.groupCards.createOrUpdateGroupDialog.description")} | ||
{...getInputProps("description", muiOptions)} | ||
/> | ||
</Grid> | ||
<Grid item sm={12}> | ||
<Select | ||
fullWidth | ||
isMulti | ||
options={rolesForSelect} | ||
onSelection={setSelectedRoles} | ||
placeholder={i18next.t("admin.groupCards.createOrUpdateGroupDialog.selectRoles")} | ||
value={selectedRoles} | ||
/> | ||
</Grid> | ||
</Grid> | ||
</CardContent> | ||
<CardActions className={classes.cardActions}> | ||
<Box> | ||
<Button | ||
onClick={onClose} | ||
> | ||
{i18next.t("app.cancel")} | ||
</Button> | ||
</Box> | ||
<Button | ||
color="primary" | ||
disabled={isSubmitting || !isDirty} | ||
variant="contained" | ||
onClick={handleSubmit} | ||
type="submit" | ||
> | ||
{isSubmitting ? i18next.t("app.settings.saveProcessing") : i18next.t("app.saveChanges")} | ||
</Button> | ||
</CardActions> | ||
</Dialog> | ||
); | ||
} | ||
|
||
CreateOrEditGroup.propTypes = { | ||
group: PropTypes.object, | ||
isOpen: PropTypes.bool.isRequired, | ||
onClose: PropTypes.func.isRequired, | ||
onSuccess: PropTypes.func, | ||
shopId: PropTypes.string | ||
}; | ||
|
||
export default CreateOrEditGroup; |
Oops, something went wrong.