Merge pull request #231 from mirumee/fix/permissions

Fix permission handling
This commit is contained in:
Marcin Gębala 2019-10-25 15:54:13 +02:00 committed by GitHub
commit 3c88c49010
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 3053 additions and 1144 deletions

View file

@ -48,3 +48,4 @@ All notable, unreleased changes to this project will be documented in this file.
- Add readonly mode - #229 by @dominik-zeglen - Add readonly mode - #229 by @dominik-zeglen
- Add mailing configuration - #222 by @dominik-zeglen - Add mailing configuration - #222 by @dominik-zeglen
- Fix minor bugs - #230 by @dominik-zeglen - Fix minor bugs - #230 by @dominik-zeglen
- Fix permission handling - #231 by @dominik-zeglen

View file

@ -1,6 +1,6 @@
msgid "" msgid ""
msgstr "" msgstr ""
"POT-Creation-Date: 2019-10-24T13:17:27.157Z\n" "POT-Creation-Date: 2019-10-25T13:36:05.943Z\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@ -9667,14 +9667,6 @@ msgctxt "button"
msgid "{languageName} - {languageCode}" msgid "{languageName} - {languageCode}"
msgstr "" msgstr ""
#: build/locale/src/translations/components/TranslationFields/TranslationFields.json
#. [src.translations.components.TranslationFields.282734765]
#. defaultMessage is:
#. {numberOFields} Translations, {numberOfTranslatedFields} Completed
msgctxt "description"
msgid "{numberOFields} Translations, {numberOfTranslatedFields} Completed"
msgstr ""
#: build/locale/src/components/SeoForm/SeoForm.json #: build/locale/src/components/SeoForm/SeoForm.json
#. [src.components.SeoForm.3877274856] - character limit #. [src.components.SeoForm.3877274856] - character limit
#. defaultMessage is: #. defaultMessage is:
@ -9683,6 +9675,14 @@ msgctxt "character limit"
msgid "{numberOfCharacters} of {maxCharacters} characters" msgid "{numberOfCharacters} of {maxCharacters} characters"
msgstr "" msgstr ""
#: build/locale/src/translations/components/TranslationFields/TranslationFields.json
#. [src.translations.components.TranslationFields.1308081812]
#. defaultMessage is:
#. {numberOfFields} Translations, {numberOfTranslatedFields} Completed
msgctxt "description"
msgid "{numberOfFields} Translations, {numberOfTranslatedFields} Completed"
msgstr ""
#: build/locale/src/components/ColumnPicker/ColumnPickerContent.json #: build/locale/src/components/ColumnPicker/ColumnPickerContent.json
#. [src.components.ColumnPicker.2715399461] - pick columns to display #. [src.components.ColumnPicker.2715399461] - pick columns to display
#. defaultMessage is: #. defaultMessage is:

View file

@ -15,8 +15,6 @@ export const fragmentUser = gql`
email email
firstName firstName
lastName lastName
isStaff
note
permissions { permissions {
code code
name name

View file

@ -31,8 +31,6 @@ export interface SetPassword_setPassword_user {
email: string; email: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
isStaff: boolean;
note: string | null;
permissions: (SetPassword_setPassword_user_permissions | null)[] | null; permissions: (SetPassword_setPassword_user_permissions | null)[] | null;
avatar: SetPassword_setPassword_user_avatar | null; avatar: SetPassword_setPassword_user_avatar | null;
} }

View file

@ -31,8 +31,6 @@ export interface TokenAuth_tokenCreate_user {
email: string; email: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
isStaff: boolean;
note: string | null;
permissions: (TokenAuth_tokenCreate_user_permissions | null)[] | null; permissions: (TokenAuth_tokenCreate_user_permissions | null)[] | null;
avatar: TokenAuth_tokenCreate_user_avatar | null; avatar: TokenAuth_tokenCreate_user_avatar | null;
} }

View file

@ -25,8 +25,6 @@ export interface User {
email: string; email: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
isStaff: boolean;
note: string | null;
permissions: (User_permissions | null)[] | null; permissions: (User_permissions | null)[] | null;
avatar: User_avatar | null; avatar: User_avatar | null;
} }

View file

@ -25,8 +25,6 @@ export interface VerifyToken_tokenVerify_user {
email: string; email: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
isStaff: boolean;
note: string | null;
permissions: (VerifyToken_tokenVerify_user_permissions | null)[] | null; permissions: (VerifyToken_tokenVerify_user_permissions | null)[] | null;
avatar: VerifyToken_tokenVerify_user_avatar | null; avatar: VerifyToken_tokenVerify_user_avatar | null;
} }

View file

@ -24,11 +24,13 @@ import saleorDarkLogoSmall from "@assets/images/logo-dark-small.svg";
import saleorDarkLogo from "@assets/images/logo-dark.svg"; import saleorDarkLogo from "@assets/images/logo-dark.svg";
import menuArrowIcon from "@assets/images/menu-arrow-icon.svg"; import menuArrowIcon from "@assets/images/menu-arrow-icon.svg";
import AppProgressProvider from "@saleor/components/AppProgress"; import AppProgressProvider from "@saleor/components/AppProgress";
import { createConfigurationMenu } from "@saleor/configuration";
import useLocalStorage from "@saleor/hooks/useLocalStorage"; import useLocalStorage from "@saleor/hooks/useLocalStorage";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useTheme from "@saleor/hooks/useTheme"; import useTheme from "@saleor/hooks/useTheme";
import useUser from "@saleor/hooks/useUser"; import useUser from "@saleor/hooks/useUser";
import ArrowDropdown from "@saleor/icons/ArrowDropdown"; import ArrowDropdown from "@saleor/icons/ArrowDropdown";
import { maybe } from "@saleor/misc";
import { staffMemberDetailsUrl } from "@saleor/staff/urls"; import { staffMemberDetailsUrl } from "@saleor/staff/urls";
import Container from "../Container"; import Container from "../Container";
import AppActionContext from "./AppActionContext"; import AppActionContext from "./AppActionContext";
@ -297,6 +299,17 @@ const AppLayout = withStyles(styles, {
const intl = useIntl(); const intl = useIntl();
const menuStructure = createMenuStructure(intl); const menuStructure = createMenuStructure(intl);
const configurationMenu = createConfigurationMenu(intl);
const userPermissions = maybe(() => user.permissions, []);
const renderConfigure = configurationMenu.some(section =>
section.menuItems.some(
menuItem =>
!!userPermissions.find(
userPermission => userPermission.code === menuItem.permission
)
)
);
const handleLogout = () => { const handleLogout = () => {
setMenuState(false); setMenuState(false);
@ -365,7 +378,7 @@ const AppLayout = withStyles(styles, {
isMenuSmall={!isMenuSmall} isMenuSmall={!isMenuSmall}
location={location.pathname} location={location.pathname}
user={user} user={user}
renderConfigure={true} renderConfigure={renderConfigure}
onMenuItemClick={handleMenuItemClick} onMenuItemClick={handleMenuItemClick}
/> />
</ResponsiveDrawer> </ResponsiveDrawer>

View file

@ -0,0 +1,31 @@
import React from "react";
import { User_permissions } from "@saleor/auth/types/User";
import { PermissionEnum } from "@saleor/types/globalTypes";
export function hasPermissions(
userPermissions: User_permissions[],
requiredPermissions: PermissionEnum[]
): boolean {
return requiredPermissions.reduce(
(acc, perm) =>
acc && !!userPermissions.find(userPerm => userPerm.code === perm),
true
);
}
export interface RequirePermissionsProps {
children: React.ReactNode | React.ReactNodeArray;
requiredPermissions: PermissionEnum[];
userPermissions: User_permissions[];
}
const RequirePermissions: React.FC<RequirePermissionsProps> = ({
children,
requiredPermissions,
userPermissions
}) =>
hasPermissions(userPermissions, requiredPermissions) ? <>{children}</> : null;
RequirePermissions.displayName = "RequirePermissions";
export default RequirePermissions;

View file

@ -99,7 +99,7 @@ export const ConfigurationPage = withStyles(styles, {
})( })(
({ ({
classes, classes,
menu, menu: menus,
user, user,
onSectionClick onSectionClick
}: ConfigurationPageProps & WithStyles<typeof styles>) => { }: ConfigurationPageProps & WithStyles<typeof styles>) => {
@ -110,9 +110,11 @@ export const ConfigurationPage = withStyles(styles, {
className={classes.header} className={classes.header}
title={intl.formatMessage(sectionNames.configuration)} title={intl.formatMessage(sectionNames.configuration)}
/> />
{menu {menus
.filter(menu => .filter(menu =>
menu.menuItems.map(item => hasPermission(item.permission, user)) menu.menuItems.some(menuItem =>
hasPermission(menuItem.permission, user)
)
) )
.map((menu, menuIndex) => ( .map((menu, menuIndex) => (
<div className={classes.configurationCategory} key={menuIndex}> <div className={classes.configurationCategory} key={menuIndex}>
@ -120,7 +122,9 @@ export const ConfigurationPage = withStyles(styles, {
<Typography>{menu.label}</Typography> <Typography>{menu.label}</Typography>
</div> </div>
<div className={classes.configurationItem}> <div className={classes.configurationItem}>
{menu.menuItems.map((item, itemIndex) => ( {menu.menuItems
.filter(menuItem => hasPermission(menuItem.permission, user))
.map((item, itemIndex) => (
<Card <Card
className={item.url ? classes.card : classes.cardDisabled} className={item.url ? classes.card : classes.cardDisabled}
onClick={() => onSectionClick(item.url)} onClick={() => onSectionClick(item.url)}

View file

@ -81,7 +81,7 @@ export function createConfigurationMenu(intl: IntlShape): MenuSection[] {
id: "configurationMenuTaxes" id: "configurationMenuTaxes"
}), }),
icon: <Taxes fontSize="inherit" viewBox="0 0 44 44" />, icon: <Taxes fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_PRODUCTS, permission: PermissionEnum.MANAGE_SETTINGS,
title: intl.formatMessage(sectionNames.taxes), title: intl.formatMessage(sectionNames.taxes),
url: taxSection url: taxSection
} }
@ -151,7 +151,7 @@ export function createConfigurationMenu(intl: IntlShape): MenuSection[] {
preserveAspectRatio="xMinYMin meet" preserveAspectRatio="xMinYMin meet"
/> />
), ),
permission: PermissionEnum.MANAGE_SETTINGS, permission: PermissionEnum.MANAGE_PLUGINS,
title: intl.formatMessage(sectionNames.plugins), title: intl.formatMessage(sectionNames.plugins),
url: pluginsListUrl() url: pluginsListUrl()
}, },

View file

@ -430,6 +430,18 @@ export const permissions: ShopInfo_shop_permissions[] = [
{ {
code: PermissionEnum.MANAGE_USERS, code: PermissionEnum.MANAGE_USERS,
name: "Manage customers." name: "Manage customers."
},
{
code: PermissionEnum.MANAGE_PLUGINS,
name: "Manage plugins."
},
{
code: PermissionEnum.MANAGE_SERVICE_ACCOUNTS,
name: "Manage service accounts."
},
{
code: PermissionEnum.MANAGE_WEBHOOKS,
name: "Manage webhooks."
} }
].map(perm => ({ ].map(perm => ({
__typename: "PermissionDisplay" as "PermissionDisplay", __typename: "PermissionDisplay" as "PermissionDisplay",

View file

@ -14,7 +14,10 @@ import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight";
import React from "react"; import React from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import RequirePermissions from "@saleor/components/RequirePermissions";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import { UserPermissionProps } from "@saleor/types";
import { PermissionEnum } from "@saleor/types/globalTypes";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@ -26,7 +29,7 @@ const styles = (theme: Theme) =>
} }
}); });
interface HomeNotificationTableProps extends WithStyles<typeof styles> { interface HomeNotificationTableProps extends UserPermissionProps {
ordersToCapture: number; ordersToCapture: number;
ordersToFulfill: number; ordersToFulfill: number;
productsOutOfStock: number; productsOutOfStock: number;
@ -45,11 +48,16 @@ const HomeNotificationTable = withStyles(styles, {
onProductsOutOfStockClick, onProductsOutOfStockClick,
ordersToCapture, ordersToCapture,
ordersToFulfill, ordersToFulfill,
productsOutOfStock productsOutOfStock,
}: HomeNotificationTableProps) => ( userPermissions
}: HomeNotificationTableProps & WithStyles<typeof styles>) => (
<Card> <Card>
<Table> <Table>
<TableBody className={classes.tableRow}> <TableBody className={classes.tableRow}>
<RequirePermissions
userPermissions={userPermissions}
requiredPermissions={[PermissionEnum.MANAGE_ORDERS]}
>
<TableRow hover={true} onClick={onOrdersToFulfillClick}> <TableRow hover={true} onClick={onOrdersToFulfillClick}>
<TableCell> <TableCell>
{ordersToFulfill === undefined ? ( {ordersToFulfill === undefined ? (
@ -110,6 +118,11 @@ const HomeNotificationTable = withStyles(styles, {
<KeyboardArrowRight /> <KeyboardArrowRight />
</TableCell> </TableCell>
</TableRow> </TableRow>
</RequirePermissions>
<RequirePermissions
userPermissions={userPermissions}
requiredPermissions={[PermissionEnum.MANAGE_PRODUCTS]}
>
<TableRow hover={true} onClick={onProductsOutOfStockClick}> <TableRow hover={true} onClick={onProductsOutOfStockClick}>
<TableCell> <TableCell>
{productsOutOfStock === undefined ? ( {productsOutOfStock === undefined ? (
@ -140,6 +153,7 @@ const HomeNotificationTable = withStyles(styles, {
<KeyboardArrowRight /> <KeyboardArrowRight />
</TableCell> </TableCell>
</TableRow> </TableRow>
</RequirePermissions>
</TableBody> </TableBody>
</Table> </Table>
</Card> </Card>

View file

@ -10,7 +10,10 @@ import CardSpacer from "@saleor/components/CardSpacer";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import Money from "@saleor/components/Money"; import Money from "@saleor/components/Money";
import RequirePermissions from "@saleor/components/RequirePermissions";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import { UserPermissionProps } from "@saleor/types";
import { PermissionEnum } from "@saleor/types/globalTypes";
import Orders from "../../../icons/Orders"; import Orders from "../../../icons/Orders";
import Sales from "../../../icons/Sales"; import Sales from "../../../icons/Sales";
import { import {
@ -39,7 +42,7 @@ const styles = (theme: Theme) =>
} }
}); });
export interface HomePageProps extends WithStyles<typeof styles> { export interface HomePageProps extends UserPermissionProps {
activities: Home_activities_edges_node[]; activities: Home_activities_edges_node[];
orders: number; orders: number;
ordersToCapture: number; ordersToCapture: number;
@ -68,13 +71,18 @@ const HomePage = withStyles(styles, { name: "HomePage" })(
onProductsOutOfStockClick, onProductsOutOfStockClick,
ordersToCapture, ordersToCapture,
ordersToFulfill, ordersToFulfill,
productsOutOfStock productsOutOfStock,
}: HomePageProps) => ( userPermissions
}: HomePageProps & WithStyles<typeof styles>) => (
<Container> <Container>
<HomeHeader userName={userName} /> <HomeHeader userName={userName} />
<CardSpacer /> <CardSpacer />
<Grid> <Grid>
<div> <div>
<RequirePermissions
userPermissions={userPermissions}
requiredPermissions={[PermissionEnum.MANAGE_ORDERS]}
>
<div className={classes.cardContainer}> <div className={classes.cardContainer}>
<HomeAnalyticsCard <HomeAnalyticsCard
title={"Sales"} title={"Sales"}
@ -97,6 +105,7 @@ const HomePage = withStyles(styles, { name: "HomePage" })(
)} )}
</HomeAnalyticsCard> </HomeAnalyticsCard>
</div> </div>
</RequirePermissions>
<HomeNotificationTable <HomeNotificationTable
onOrdersToCaptureClick={onOrdersToCaptureClick} onOrdersToCaptureClick={onOrdersToCaptureClick}
onOrdersToFulfillClick={onOrdersToFulfillClick} onOrdersToFulfillClick={onOrdersToFulfillClick}
@ -104,16 +113,30 @@ const HomePage = withStyles(styles, { name: "HomePage" })(
ordersToCapture={ordersToCapture} ordersToCapture={ordersToCapture}
ordersToFulfill={ordersToFulfill} ordersToFulfill={ordersToFulfill}
productsOutOfStock={productsOutOfStock} productsOutOfStock={productsOutOfStock}
userPermissions={userPermissions}
/> />
<CardSpacer /> <CardSpacer />
<RequirePermissions
userPermissions={userPermissions}
requiredPermissions={[
PermissionEnum.MANAGE_ORDERS,
PermissionEnum.MANAGE_PRODUCTS
]}
>
<HomeProductListCard <HomeProductListCard
onRowClick={onProductClick} onRowClick={onProductClick}
topProducts={topProducts} topProducts={topProducts}
/> />
<CardSpacer /> <CardSpacer />
</RequirePermissions>
</div> </div>
<div> <div>
<RequirePermissions
userPermissions={userPermissions}
requiredPermissions={[PermissionEnum.MANAGE_ORDERS]}
>
<HomeActivityCard activities={activities} /> <HomeActivityCard activities={activities} />
</RequirePermissions>
</div> </div>
</Grid> </Grid>
</Container> </Container>

View file

@ -56,6 +56,7 @@ const HomeSection = () => {
ordersToFulfill={maybe(() => data.ordersToFulfill.totalCount)} ordersToFulfill={maybe(() => data.ordersToFulfill.totalCount)}
productsOutOfStock={maybe(() => data.productsOutOfStock.totalCount)} productsOutOfStock={maybe(() => data.productsOutOfStock.totalCount)}
userName={getUserName(user, true)} userName={getUserName(user, true)}
userPermissions={maybe(() => user.permissions, [])}
/> />
)} )}
</HomePageQuery> </HomePageQuery>

View file

@ -16,11 +16,13 @@ import ExternalLink from "@saleor/components/ExternalLink";
import Form from "@saleor/components/Form"; import Form from "@saleor/components/Form";
import Hr from "@saleor/components/Hr"; import Hr from "@saleor/components/Hr";
import Link from "@saleor/components/Link"; import Link from "@saleor/components/Link";
import RequirePermissions from "@saleor/components/RequirePermissions";
import SingleAutocompleteSelectField from "@saleor/components/SingleAutocompleteSelectField"; import SingleAutocompleteSelectField from "@saleor/components/SingleAutocompleteSelectField";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { buttonMessages } from "@saleor/intl"; import { buttonMessages } from "@saleor/intl";
import { FetchMoreProps } from "@saleor/types"; import { FetchMoreProps, UserPermissionProps } from "@saleor/types";
import { PermissionEnum } from "@saleor/types/globalTypes";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import { SearchCustomers_search_edges_node } from "../../../containers/SearchCustomers/types/SearchCustomers"; import { SearchCustomers_search_edges_node } from "../../../containers/SearchCustomers/types/SearchCustomers";
import { customerUrl } from "../../../customers/urls"; import { customerUrl } from "../../../customers/urls";
@ -49,7 +51,9 @@ const styles = (theme: Theme) =>
} }
}); });
export interface OrderCustomerProps extends Partial<FetchMoreProps> { export interface OrderCustomerProps
extends Partial<FetchMoreProps>,
UserPermissionProps {
order: OrderDetails_order; order: OrderDetails_order;
users?: SearchCustomers_search_edges_node[]; users?: SearchCustomers_search_edges_node[];
loading?: boolean; loading?: boolean;
@ -72,6 +76,7 @@ const OrderCustomer = withStyles(styles, { name: "OrderCustomer" })(
loading, loading,
order, order,
users, users,
userPermissions,
onCustomerEdit, onCustomerEdit,
onBillingAddressEdit, onBillingAddressEdit,
onFetchMore: onFetchMoreUsers, onFetchMore: onFetchMoreUsers,
@ -81,6 +86,7 @@ const OrderCustomer = withStyles(styles, { name: "OrderCustomer" })(
const intl = useIntl(); const intl = useIntl();
const user = maybe(() => order.user); const user = maybe(() => order.user);
const userEmail = maybe(()=>order.userEmail)
const [userDisplayName, setUserDisplayName] = useStateFromProps( const [userDisplayName, setUserDisplayName] = useStateFromProps(
maybe(() => user.email, "") maybe(() => user.email, "")
@ -100,6 +106,10 @@ const OrderCustomer = withStyles(styles, { name: "OrderCustomer" })(
})} })}
toolbar={ toolbar={
!!canEditCustomer && ( !!canEditCustomer && (
<RequirePermissions
userPermissions={userPermissions}
requiredPermissions={[PermissionEnum.MANAGE_USERS]}
>
<Button <Button
color="primary" color="primary"
variant="text" variant="text"
@ -108,6 +118,7 @@ const OrderCustomer = withStyles(styles, { name: "OrderCustomer" })(
> >
{intl.formatMessage(buttonMessages.edit)} {intl.formatMessage(buttonMessages.edit)}
</Button> </Button>
</RequirePermissions>
) )
} }
/> />
@ -155,14 +166,22 @@ const OrderCustomer = withStyles(styles, { name: "OrderCustomer" })(
}} }}
</Form> </Form>
) : user === null ? ( ) : user === null ? (
userEmail === null ? (
<Typography> <Typography>
<FormattedMessage defaultMessage="Anonymous user" /> <FormattedMessage defaultMessage="Anonymous user" />
</Typography> </Typography>
) : (
<Typography className={classes.userEmail}>{userEmail}</Typography>
)
) : ( ) : (
<> <>
<Typography className={classes.userEmail}> <Typography className={classes.userEmail}>
{user.email} {user.email}
</Typography> </Typography>
<RequirePermissions
userPermissions={userPermissions}
requiredPermissions={[PermissionEnum.MANAGE_USERS]}
>
<div> <div>
<Link <Link
underline={false} underline={false}
@ -175,6 +194,7 @@ const OrderCustomer = withStyles(styles, { name: "OrderCustomer" })(
/> />
</Link> </Link>
</div> </div>
</RequirePermissions>
{/* TODO: Uncomment it after adding ability to filter {/* TODO: Uncomment it after adding ability to filter
orders by customer */} orders by customer */}
{/* <div> {/* <div>
@ -187,6 +207,8 @@ const OrderCustomer = withStyles(styles, { name: "OrderCustomer" })(
</> </>
)} )}
</CardContent> </CardContent>
{!!user && (
<>
<Hr /> <Hr />
<CardContent> <CardContent>
<div className={classes.sectionHeader}> <div className={classes.sectionHeader}>
@ -217,6 +239,8 @@ const OrderCustomer = withStyles(styles, { name: "OrderCustomer" })(
</ExternalLink> </ExternalLink>
)} )}
</CardContent> </CardContent>
</>
)}
<Hr /> <Hr />
<CardContent> <CardContent>
<div className={classes.sectionHeader}> <div className={classes.sectionHeader}>

View file

@ -17,6 +17,7 @@ import Grid from "@saleor/components/Grid";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { UserPermissionProps } from "@saleor/types";
import { maybe, renderCollection } from "../../../misc"; import { maybe, renderCollection } from "../../../misc";
import { OrderStatus } from "../../../types/globalTypes"; import { OrderStatus } from "../../../types/globalTypes";
import { OrderDetails_order } from "../../types/OrderDetails"; import { OrderDetails_order } from "../../types/OrderDetails";
@ -38,7 +39,7 @@ const styles = (theme: Theme) =>
} }
}); });
export interface OrderDetailsPageProps extends WithStyles<typeof styles> { export interface OrderDetailsPageProps extends UserPermissionProps {
order: OrderDetails_order; order: OrderDetails_order;
shippingMethods?: Array<{ shippingMethods?: Array<{
id: string; id: string;
@ -68,12 +69,13 @@ const OrderDetailsPage = withStyles(styles, { name: "OrderDetailsPage" })(
({ ({
classes, classes,
order, order,
onOrderCancel, userPermissions,
onBack, onBack,
onBillingAddressEdit, onBillingAddressEdit,
onFulfillmentCancel, onFulfillmentCancel,
onFulfillmentTrackingNumberUpdate, onFulfillmentTrackingNumberUpdate,
onNoteAdd, onNoteAdd,
onOrderCancel,
onOrderFulfill, onOrderFulfill,
onPaymentCapture, onPaymentCapture,
onPaymentPaid, onPaymentPaid,
@ -81,7 +83,7 @@ const OrderDetailsPage = withStyles(styles, { name: "OrderDetailsPage" })(
onPaymentVoid, onPaymentVoid,
onShippingAddressEdit, onShippingAddressEdit,
onProfileView onProfileView
}: OrderDetailsPageProps) => { }: OrderDetailsPageProps & WithStyles<typeof styles>) => {
const intl = useIntl(); const intl = useIntl();
const canCancel = maybe(() => order.status) !== OrderStatus.CANCELED; const canCancel = maybe(() => order.status) !== OrderStatus.CANCELED;
@ -170,6 +172,7 @@ const OrderDetailsPage = withStyles(styles, { name: "OrderDetailsPage" })(
canEditAddresses={canEditAddresses} canEditAddresses={canEditAddresses}
canEditCustomer={false} canEditCustomer={false}
order={order} order={order}
userPermissions={userPermissions}
onBillingAddressEdit={onBillingAddressEdit} onBillingAddressEdit={onBillingAddressEdit}
onShippingAddressEdit={onShippingAddressEdit} onShippingAddressEdit={onShippingAddressEdit}
onProfileView={onProfileView} onProfileView={onProfileView}

View file

@ -18,7 +18,7 @@ import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { FetchMoreProps } from "@saleor/types"; import { FetchMoreProps, UserPermissionProps } from "@saleor/types";
import { SearchCustomers_search_edges_node } from "../../../containers/SearchCustomers/types/SearchCustomers"; import { SearchCustomers_search_edges_node } from "../../../containers/SearchCustomers/types/SearchCustomers";
import { maybe } from "../../../misc"; import { maybe } from "../../../misc";
import { DraftOrderInput } from "../../../types/globalTypes"; import { DraftOrderInput } from "../../../types/globalTypes";
@ -39,7 +39,9 @@ const styles = (theme: Theme) =>
} }
}); });
export interface OrderDraftPageProps extends FetchMoreProps { export interface OrderDraftPageProps
extends FetchMoreProps,
UserPermissionProps {
disabled: boolean; disabled: boolean;
order: OrderDetails_order; order: OrderDetails_order;
users: SearchCustomers_search_edges_node[]; users: SearchCustomers_search_edges_node[];
@ -90,7 +92,8 @@ const OrderDraftPage = withStyles(styles, { name: "OrderDraftPage" })(
onProfileView, onProfileView,
order, order,
users, users,
usersLoading usersLoading,
userPermissions
}: OrderDraftPageProps & WithStyles<typeof styles>) => { }: OrderDraftPageProps & WithStyles<typeof styles>) => {
const intl = useIntl(); const intl = useIntl();
@ -147,6 +150,7 @@ const OrderDraftPage = withStyles(styles, { name: "OrderDraftPage" })(
loading={usersLoading} loading={usersLoading}
order={order} order={order}
users={users} users={users}
userPermissions={userPermissions}
onBillingAddressEdit={onBillingAddressEdit} onBillingAddressEdit={onBillingAddressEdit}
onCustomerEdit={onCustomerEdit} onCustomerEdit={onCustomerEdit}
onFetchMore={onFetchMore} onFetchMore={onFetchMore}

View file

@ -2,6 +2,7 @@ import React from "react";
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useUser from "@saleor/hooks/useUser";
import { DEFAULT_INITIAL_SEARCH_DATA } from "../../../config"; import { DEFAULT_INITIAL_SEARCH_DATA } from "../../../config";
import SearchCustomers from "../../../containers/SearchCustomers"; import SearchCustomers from "../../../containers/SearchCustomers";
import { customerUrl } from "../../../customers/urls"; import { customerUrl } from "../../../customers/urls";
@ -80,6 +81,7 @@ export const OrderDetails: React.StatelessComponent<OrderDetailsProps> = ({
params params
}) => { }) => {
const navigate = useNavigator(); const navigate = useNavigator();
const { user } = useUser();
return ( return (
<TypedOrderDetailsQuery <TypedOrderDetailsQuery
@ -183,6 +185,10 @@ export const OrderDetails: React.StatelessComponent<OrderDetailsProps> = ({
() => data.order.availableShippingMethods, () => data.order.availableShippingMethods,
[] []
)} )}
userPermissions={maybe(
() => user.permissions,
[]
)}
onOrderCancel={() => openModal("cancel")} onOrderCancel={() => openModal("cancel")}
onOrderFulfill={() => openModal("fulfill")} onOrderFulfill={() => openModal("fulfill")}
onFulfillmentCancel={fulfillmentId => onFulfillmentCancel={fulfillmentId =>
@ -466,6 +472,10 @@ export const OrderDetails: React.StatelessComponent<OrderDetailsProps> = ({
onProfileView={() => onProfileView={() =>
navigate(customerUrl(order.user.id)) navigate(customerUrl(order.user.id))
} }
userPermissions={maybe(
() => user.permissions,
[]
)}
/> />
<OrderDraftCancelDialog <OrderDraftCancelDialog
confirmButtonState={getMutationState( confirmButtonState={getMutationState(

View file

@ -10,7 +10,7 @@ import ErrorPage from "./components/ErrorPage/ErrorPage";
import useNavigator from "./hooks/useNavigator"; import useNavigator from "./hooks/useNavigator";
import useNotifier from "./hooks/useNotifier"; import useNotifier from "./hooks/useNotifier";
import { commonMessages } from "./intl"; import { commonMessages } from "./intl";
import { RequireAtLeastOne } from "./misc"; import { maybe, RequireAtLeastOne } from "./misc";
export interface LoadMore<TData, TVariables> { export interface LoadMore<TData, TVariables> {
loadMore: ( loadMore: (
@ -81,13 +81,22 @@ export function TypedQuery<TData, TVariables>(
variables={variables} variables={variables}
skip={skip} skip={skip}
context={{ useBatching: true }} context={{ useBatching: true }}
errorPolicy="all"
> >
{(queryData: QueryResult<TData, TVariables>) => { {(queryData: QueryResult<TData, TVariables>) => {
if (queryData.error) { if (queryData.error) {
if (
!queryData.error.graphQLErrors.every(
err =>
maybe(() => err.extensions.exception.code) ===
"PermissionDenied"
)
) {
pushMessage({ pushMessage({
text: intl.formatMessage(commonMessages.somethingWentWrong) text: intl.formatMessage(commonMessages.somethingWentWrong)
}); });
} }
}
const loadMore = ( const loadMore = (
mergeFunc: ( mergeFunc: (

View file

@ -5,14 +5,18 @@ import AppHeader from "@saleor/components/AppHeader";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import RequirePermissions from "@saleor/components/RequirePermissions";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { ListActions, PageListProps } from "@saleor/types"; import { ListActions, PageListProps, UserPermissionProps } from "@saleor/types";
import { WeightUnitsEnum } from "@saleor/types/globalTypes"; import { PermissionEnum, WeightUnitsEnum } from "@saleor/types/globalTypes";
import { ShippingZoneFragment } from "../../types/ShippingZoneFragment"; import { ShippingZoneFragment } from "../../types/ShippingZoneFragment";
import ShippingWeightUnitForm from "../ShippingWeightUnitForm"; import ShippingWeightUnitForm from "../ShippingWeightUnitForm";
import ShippingZonesList from "../ShippingZonesList"; import ShippingZonesList from "../ShippingZonesList";
export interface ShippingZonesListPageProps extends PageListProps, ListActions { export interface ShippingZonesListPageProps
extends PageListProps,
ListActions,
UserPermissionProps {
defaultWeightUnit: WeightUnitsEnum; defaultWeightUnit: WeightUnitsEnum;
shippingZones: ShippingZoneFragment[]; shippingZones: ShippingZoneFragment[];
onBack: () => void; onBack: () => void;
@ -22,7 +26,14 @@ export interface ShippingZonesListPageProps extends PageListProps, ListActions {
const ShippingZonesListPage: React.StatelessComponent< const ShippingZonesListPage: React.StatelessComponent<
ShippingZonesListPageProps ShippingZonesListPageProps
> = ({ defaultWeightUnit, disabled, onBack, onSubmit, ...listProps }) => { > = ({
defaultWeightUnit,
disabled,
userPermissions,
onBack,
onSubmit,
...listProps
}) => {
const intl = useIntl(); const intl = useIntl();
return ( return (
@ -41,11 +52,16 @@ const ShippingZonesListPage: React.StatelessComponent<
<ShippingZonesList disabled={disabled} {...listProps} /> <ShippingZonesList disabled={disabled} {...listProps} />
</div> </div>
<div> <div>
<RequirePermissions
userPermissions={userPermissions}
requiredPermissions={[PermissionEnum.MANAGE_SETTINGS]}
>
<ShippingWeightUnitForm <ShippingWeightUnitForm
defaultWeightUnit={defaultWeightUnit} defaultWeightUnit={defaultWeightUnit}
disabled={disabled} disabled={disabled}
onSubmit={onSubmit} onSubmit={onSubmit}
/> />
</RequirePermissions>
</div> </div>
</Grid> </Grid>
</Container> </Container>

View file

@ -14,6 +14,7 @@ import usePaginator, {
createPaginationState createPaginationState
} from "@saleor/hooks/usePaginator"; } from "@saleor/hooks/usePaginator";
import useShop from "@saleor/hooks/useShop"; import useShop from "@saleor/hooks/useShop";
import useUser from "@saleor/hooks/useUser";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { getMutationState, maybe } from "@saleor/misc"; import { getMutationState, maybe } from "@saleor/misc";
import { ListViews } from "@saleor/types"; import { ListViews } from "@saleor/types";
@ -45,6 +46,7 @@ export const ShippingZonesList: React.StatelessComponent<
const notify = useNotifier(); const notify = useNotifier();
const paginate = usePaginator(); const paginate = usePaginator();
const shop = useShop(); const shop = useShop();
const { user } = useUser();
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
params.ids params.ids
); );
@ -195,6 +197,7 @@ export const ShippingZonesList: React.StatelessComponent<
<DeleteIcon /> <DeleteIcon />
</IconButton> </IconButton>
} }
userPermissions={maybe(() => user.permissions, [])}
/> />
<ActionDialog <ActionDialog

View file

@ -1,5 +1,5 @@
import avatarImage from "@assets/images/avatars/avatar1.png"; import avatarImage from "@assets/images/avatars/avatar1.png";
import { PermissionEnum } from "../types/globalTypes"; import { permissions } from "@saleor/fixtures";
import { StaffList_staffUsers_edges_node } from "./types/StaffList"; import { StaffList_staffUsers_edges_node } from "./types/StaffList";
import { StaffMemberDetails_user } from "./types/StaffMemberDetails"; import { StaffMemberDetails_user } from "./types/StaffMemberDetails";
@ -145,49 +145,5 @@ export const staffMember: StaffMemberDetails_user = {
id: "VXNlcjoyMQ==", id: "VXNlcjoyMQ==",
isActive: true, isActive: true,
lastName: "Smith", lastName: "Smith",
permissions: [ permissions
{
code: PermissionEnum.IMPERSONATE_USERS,
name: "Impersonate customers."
},
{
code: PermissionEnum.MANAGE_DISCOUNTS,
name: "Manage sales and vouchers."
},
{
code: PermissionEnum.MANAGE_MENUS,
name: "Manage navigation."
},
{
code: PermissionEnum.MANAGE_ORDERS,
name: "Manage orders."
},
{
code: PermissionEnum.MANAGE_PAGES,
name: "Manage pages."
},
{
code: PermissionEnum.MANAGE_PRODUCTS,
name: "Manage products."
},
{
code: PermissionEnum.MANAGE_SETTINGS,
name: "Manage settings."
},
{
code: PermissionEnum.MANAGE_SHIPPING,
name: "Manage shipping."
},
{
code: PermissionEnum.MANAGE_STAFF,
name: "Manage staff."
},
{
code: PermissionEnum.MANAGE_USERS,
name: "Manage customers."
}
].map(perm => ({
__typename: "PermissionDisplay" as "PermissionDisplay",
...perm
}))
}; };

File diff suppressed because it is too large Load diff

View file

@ -3,6 +3,8 @@ import { storiesOf } from "@storybook/react";
import React from "react"; import React from "react";
import placeholderImage from "@assets/images/placeholder60x60.png"; import placeholderImage from "@assets/images/placeholder60x60.png";
import { permissions } from "@saleor/fixtures";
import { PermissionEnum } from "@saleor/types/globalTypes";
import HomePage, { HomePageProps } from "../../../home/components/HomePage"; import HomePage, { HomePageProps } from "../../../home/components/HomePage";
import { shop as shopFixture } from "../../../home/fixtures"; import { shop as shopFixture } from "../../../home/fixtures";
import Decorator from "../../Decorator"; import Decorator from "../../Decorator";
@ -21,7 +23,8 @@ const homePageProps: Omit<HomePageProps, "classes"> = {
productsOutOfStock: shop.productsOutOfStock.totalCount, productsOutOfStock: shop.productsOutOfStock.totalCount,
sales: shop.salesToday.gross, sales: shop.salesToday.gross,
topProducts: shop.productTopToday.edges.map(edge => edge.node), topProducts: shop.productTopToday.edges.map(edge => edge.node),
userName: "admin@example.com" userName: "admin@example.com",
userPermissions: permissions
}; };
storiesOf("Views / HomePage", module) storiesOf("Views / HomePage", module)
@ -42,4 +45,23 @@ storiesOf("Views / HomePage", module)
)) ))
.add("no data", () => ( .add("no data", () => (
<HomePage {...homePageProps} topProducts={[]} activities={[]} /> <HomePage {...homePageProps} topProducts={[]} activities={[]} />
))
.add("no permissions", () => (
<HomePage {...homePageProps} userPermissions={[]} />
))
.add("product permissions", () => (
<HomePage
{...homePageProps}
userPermissions={permissions.filter(
perm => perm.code === PermissionEnum.MANAGE_PRODUCTS
)}
/>
))
.add("order permissions", () => (
<HomePage
{...homePageProps}
userPermissions={permissions.filter(
perm => perm.code === PermissionEnum.MANAGE_ORDERS
)}
/>
)); ));

View file

@ -2,6 +2,7 @@ import { Omit } from "@material-ui/core";
import { storiesOf } from "@storybook/react"; import { storiesOf } from "@storybook/react";
import React from "react"; import React from "react";
import { permissions } from "@saleor/fixtures";
import OrderCustomer, { import OrderCustomer, {
OrderCustomerProps OrderCustomerProps
} from "../../../orders/components/OrderCustomer"; } from "../../../orders/components/OrderCustomer";
@ -19,6 +20,7 @@ const props: Omit<OrderCustomerProps, "classes"> = {
onProfileView: () => undefined, onProfileView: () => undefined,
onShippingAddressEdit: undefined, onShippingAddressEdit: undefined,
order, order,
userPermissions: permissions,
users: clients users: clients
}; };
@ -37,4 +39,10 @@ storiesOf("Orders / OrderCustomer", module)
)) ))
.add("editable", () => ( .add("editable", () => (
<OrderCustomer {...props} canEditAddresses={true} canEditCustomer={true} /> <OrderCustomer {...props} canEditAddresses={true} canEditCustomer={true} />
))
.add("editable", () => (
<OrderCustomer {...props} canEditAddresses={true} canEditCustomer={true} />
))
.add("no user permissions", () => (
<OrderCustomer {...props} userPermissions={[]} />
)); ));

View file

@ -3,6 +3,7 @@ import { storiesOf } from "@storybook/react";
import React from "react"; import React from "react";
import placeholderImage from "@assets/images/placeholder60x60.png"; import placeholderImage from "@assets/images/placeholder60x60.png";
import { permissions } from "@saleor/fixtures";
import OrderDetailsPage, { import OrderDetailsPage, {
OrderDetailsPageProps OrderDetailsPageProps
} from "../../../orders/components/OrderDetailsPage"; } from "../../../orders/components/OrderDetailsPage";
@ -32,7 +33,8 @@ const props: Omit<OrderDetailsPageProps, "classes"> = {
onProductClick: undefined, onProductClick: undefined,
onProfileView: () => undefined, onProfileView: () => undefined,
onShippingAddressEdit: undefined, onShippingAddressEdit: undefined,
order order,
userPermissions: permissions
}; };
storiesOf("Views / Orders / Order details", module) storiesOf("Views / Orders / Order details", module)

View file

@ -3,7 +3,7 @@ import { storiesOf } from "@storybook/react";
import React from "react"; import React from "react";
import placeholderImage from "@assets/images/placeholder60x60.png"; import placeholderImage from "@assets/images/placeholder60x60.png";
import { fetchMoreProps } from "@saleor/fixtures"; import { fetchMoreProps, permissions } from "@saleor/fixtures";
import OrderDraftPage, { import OrderDraftPage, {
OrderDraftPageProps OrderDraftPageProps
} from "../../../orders/components/OrderDraftPage"; } from "../../../orders/components/OrderDraftPage";
@ -32,6 +32,7 @@ const props: Omit<OrderDraftPageProps, "classes"> = {
onShippingMethodEdit: undefined, onShippingMethodEdit: undefined,
order, order,
saveButtonBarState: "default", saveButtonBarState: "default",
userPermissions: permissions,
users: clients, users: clients,
usersLoading: false usersLoading: false
}; };
@ -44,4 +45,7 @@ storiesOf("Views / Orders / Order draft", module)
)) ))
.add("without lines", () => ( .add("without lines", () => (
<OrderDraftPage {...props} order={{ ...order, lines: [] }} /> <OrderDraftPage {...props} order={{ ...order, lines: [] }} />
))
.add("no user permissions", () => (
<OrderDraftPage {...props} userPermissions={[]} />
)); ));

View file

@ -1,7 +1,11 @@
import { storiesOf } from "@storybook/react"; import { storiesOf } from "@storybook/react";
import React from "react"; import React from "react";
import { listActionsProps, pageListProps } from "../../../fixtures"; import {
listActionsProps,
pageListProps,
permissions
} from "../../../fixtures";
import ShippingZonesListPage, { import ShippingZonesListPage, {
ShippingZonesListPageProps ShippingZonesListPageProps
} from "../../../shipping/components/ShippingZonesListPage"; } from "../../../shipping/components/ShippingZonesListPage";
@ -17,7 +21,8 @@ const props: ShippingZonesListPageProps = {
onBack: () => undefined, onBack: () => undefined,
onRemove: () => undefined, onRemove: () => undefined,
onSubmit: () => undefined, onSubmit: () => undefined,
shippingZones shippingZones,
userPermissions: permissions
}; };
storiesOf("Views / Shipping / Shipping zones list", module) storiesOf("Views / Shipping / Shipping zones list", module)
@ -30,6 +35,7 @@ storiesOf("Views / Shipping / Shipping zones list", module)
shippingZones={undefined} shippingZones={undefined}
/> />
)) ))
.add("no data", () => ( .add("no data", () => <ShippingZonesListPage {...props} shippingZones={[]} />)
<ShippingZonesListPage {...props} shippingZones={[]} /> .add("no site settings permissions", () => (
<ShippingZonesListPage {...props} userPermissions={[]} />
)); ));

View file

@ -235,7 +235,7 @@ const TranslationFields = withStyles(styles, { name: "TranslationFields" })(
<CardContent> <CardContent>
<Typography className={classes.cardCaption} variant="caption"> <Typography className={classes.cardCaption} variant="caption">
<FormattedMessage <FormattedMessage
defaultMessage="{numberOFields} Translations, {numberOfTranslatedFields} Completed" defaultMessage="{numberOfFields} Translations, {numberOfTranslatedFields} Completed"
values={{ values={{
numberOfFields: fields.length, numberOfFields: fields.length,
numberOfTranslatedFields: fields.reduce( numberOfTranslatedFields: fields.reduce(

View file

@ -1,5 +1,6 @@
import { MutationResult } from "react-apollo"; import { MutationResult } from "react-apollo";
import { User_permissions } from "./auth/types/User";
import { FilterContentSubmitData } from "./components/Filter"; import { FilterContentSubmitData } from "./components/Filter";
import { Filter } from "./components/TableFilter"; import { Filter } from "./components/TableFilter";
@ -158,3 +159,7 @@ export interface FetchMoreProps {
} }
export type TabActionDialog = "save-search" | "delete-search"; export type TabActionDialog = "save-search" | "delete-search";
export interface UserPermissionProps {
userPermissions: User_permissions[];
}