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 mailing configuration - #222 by @dominik-zeglen
- Fix minor bugs - #230 by @dominik-zeglen
- Fix permission handling - #231 by @dominik-zeglen

View file

@ -1,6 +1,6 @@
msgid ""
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-Transfer-Encoding: 8bit\n"
"MIME-Version: 1.0\n"
@ -9667,14 +9667,6 @@ msgctxt "button"
msgid "{languageName} - {languageCode}"
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
#. [src.components.SeoForm.3877274856] - character limit
#. defaultMessage is:
@ -9683,6 +9675,14 @@ msgctxt "character limit"
msgid "{numberOfCharacters} of {maxCharacters} characters"
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
#. [src.components.ColumnPicker.2715399461] - pick columns to display
#. defaultMessage is:

View file

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

View file

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

View file

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

View file

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

View file

@ -25,8 +25,6 @@ export interface VerifyToken_tokenVerify_user {
email: string;
firstName: string;
lastName: string;
isStaff: boolean;
note: string | null;
permissions: (VerifyToken_tokenVerify_user_permissions | null)[] | 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 menuArrowIcon from "@assets/images/menu-arrow-icon.svg";
import AppProgressProvider from "@saleor/components/AppProgress";
import { createConfigurationMenu } from "@saleor/configuration";
import useLocalStorage from "@saleor/hooks/useLocalStorage";
import useNavigator from "@saleor/hooks/useNavigator";
import useTheme from "@saleor/hooks/useTheme";
import useUser from "@saleor/hooks/useUser";
import ArrowDropdown from "@saleor/icons/ArrowDropdown";
import { maybe } from "@saleor/misc";
import { staffMemberDetailsUrl } from "@saleor/staff/urls";
import Container from "../Container";
import AppActionContext from "./AppActionContext";
@ -297,6 +299,17 @@ const AppLayout = withStyles(styles, {
const intl = useIntl();
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 = () => {
setMenuState(false);
@ -365,7 +378,7 @@ const AppLayout = withStyles(styles, {
isMenuSmall={!isMenuSmall}
location={location.pathname}
user={user}
renderConfigure={true}
renderConfigure={renderConfigure}
onMenuItemClick={handleMenuItemClick}
/>
</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,
menu,
menu: menus,
user,
onSectionClick
}: ConfigurationPageProps & WithStyles<typeof styles>) => {
@ -110,9 +110,11 @@ export const ConfigurationPage = withStyles(styles, {
className={classes.header}
title={intl.formatMessage(sectionNames.configuration)}
/>
{menu
{menus
.filter(menu =>
menu.menuItems.map(item => hasPermission(item.permission, user))
menu.menuItems.some(menuItem =>
hasPermission(menuItem.permission, user)
)
)
.map((menu, menuIndex) => (
<div className={classes.configurationCategory} key={menuIndex}>
@ -120,28 +122,30 @@ export const ConfigurationPage = withStyles(styles, {
<Typography>{menu.label}</Typography>
</div>
<div className={classes.configurationItem}>
{menu.menuItems.map((item, itemIndex) => (
<Card
className={item.url ? classes.card : classes.cardDisabled}
onClick={() => onSectionClick(item.url)}
key={itemIndex}
>
<CardContent className={classes.cardContent}>
<div className={classes.icon}>{item.icon}</div>
<div>
<Typography
className={classes.sectionTitle}
color="primary"
>
{item.title}
</Typography>
<Typography className={classes.sectionDescription}>
{item.description}
</Typography>
</div>
</CardContent>
</Card>
))}
{menu.menuItems
.filter(menuItem => hasPermission(menuItem.permission, user))
.map((item, itemIndex) => (
<Card
className={item.url ? classes.card : classes.cardDisabled}
onClick={() => onSectionClick(item.url)}
key={itemIndex}
>
<CardContent className={classes.cardContent}>
<div className={classes.icon}>{item.icon}</div>
<div>
<Typography
className={classes.sectionTitle}
color="primary"
>
{item.title}
</Typography>
<Typography className={classes.sectionDescription}>
{item.description}
</Typography>
</div>
</CardContent>
</Card>
))}
</div>
</div>
))}

View file

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

View file

@ -430,6 +430,18 @@ export const permissions: ShopInfo_shop_permissions[] = [
{
code: PermissionEnum.MANAGE_USERS,
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 => ({
__typename: "PermissionDisplay" as "PermissionDisplay",

View file

@ -14,7 +14,10 @@ import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight";
import React from "react";
import { FormattedMessage } from "react-intl";
import RequirePermissions from "@saleor/components/RequirePermissions";
import Skeleton from "@saleor/components/Skeleton";
import { UserPermissionProps } from "@saleor/types";
import { PermissionEnum } from "@saleor/types/globalTypes";
const styles = (theme: Theme) =>
createStyles({
@ -26,7 +29,7 @@ const styles = (theme: Theme) =>
}
});
interface HomeNotificationTableProps extends WithStyles<typeof styles> {
interface HomeNotificationTableProps extends UserPermissionProps {
ordersToCapture: number;
ordersToFulfill: number;
productsOutOfStock: number;
@ -45,101 +48,112 @@ const HomeNotificationTable = withStyles(styles, {
onProductsOutOfStockClick,
ordersToCapture,
ordersToFulfill,
productsOutOfStock
}: HomeNotificationTableProps) => (
productsOutOfStock,
userPermissions
}: HomeNotificationTableProps & WithStyles<typeof styles>) => (
<Card>
<Table>
<TableBody className={classes.tableRow}>
<TableRow hover={true} onClick={onOrdersToFulfillClick}>
<TableCell>
{ordersToFulfill === undefined ? (
<Skeleton />
) : ordersToFulfill === 0 ? (
<Typography>
<FormattedMessage
defaultMessage="No orders ready to fulfill"
id="homeNotificationTableNoOrders"
/>
</Typography>
) : (
<Typography>
<FormattedMessage
defaultMessage="{amount, plural,
<RequirePermissions
userPermissions={userPermissions}
requiredPermissions={[PermissionEnum.MANAGE_ORDERS]}
>
<TableRow hover={true} onClick={onOrdersToFulfillClick}>
<TableCell>
{ordersToFulfill === undefined ? (
<Skeleton />
) : ordersToFulfill === 0 ? (
<Typography>
<FormattedMessage
defaultMessage="No orders ready to fulfill"
id="homeNotificationTableNoOrders"
/>
</Typography>
) : (
<Typography>
<FormattedMessage
defaultMessage="{amount, plural,
one {One order}
other {{amount} Orders}
} are ready to fulfill"
id="homeNotificationTableOrders"
values={{
amount: <strong>{ordersToFulfill}</strong>
}}
/>
</Typography>
)}
</TableCell>
<TableCell className={classes.arrowIcon}>
<KeyboardArrowRight />
</TableCell>
</TableRow>
<TableRow hover={true} onClick={onOrdersToCaptureClick}>
<TableCell>
{ordersToCapture === undefined ? (
<Skeleton />
) : ordersToCapture === 0 ? (
<Typography>
<FormattedMessage
defaultMessage="No payments waiting for capture"
id="homeNotificationsNoPayments"
/>
</Typography>
) : (
<Typography>
<FormattedMessage
defaultMessage="{amount, plural,
id="homeNotificationTableOrders"
values={{
amount: <strong>{ordersToFulfill}</strong>
}}
/>
</Typography>
)}
</TableCell>
<TableCell className={classes.arrowIcon}>
<KeyboardArrowRight />
</TableCell>
</TableRow>
<TableRow hover={true} onClick={onOrdersToCaptureClick}>
<TableCell>
{ordersToCapture === undefined ? (
<Skeleton />
) : ordersToCapture === 0 ? (
<Typography>
<FormattedMessage
defaultMessage="No payments waiting for capture"
id="homeNotificationsNoPayments"
/>
</Typography>
) : (
<Typography>
<FormattedMessage
defaultMessage="{amount, plural,
one {One payment}
other {{amount} Payments}
} to capture"
id="homeNotificationTablePayments"
values={{
amount: <strong>{ordersToCapture}</strong>
}}
/>
</Typography>
)}
</TableCell>
<TableCell className={classes.arrowIcon}>
<KeyboardArrowRight />
</TableCell>
</TableRow>
<TableRow hover={true} onClick={onProductsOutOfStockClick}>
<TableCell>
{productsOutOfStock === undefined ? (
<Skeleton />
) : productsOutOfStock === 0 ? (
<Typography>
<FormattedMessage
defaultMessage="No products out of stock"
id="homeNotificationsTableNoProducts"
/>
</Typography>
) : (
<Typography>
<FormattedMessage
defaultMessage="{amount, plural,
id="homeNotificationTablePayments"
values={{
amount: <strong>{ordersToCapture}</strong>
}}
/>
</Typography>
)}
</TableCell>
<TableCell className={classes.arrowIcon}>
<KeyboardArrowRight />
</TableCell>
</TableRow>
</RequirePermissions>
<RequirePermissions
userPermissions={userPermissions}
requiredPermissions={[PermissionEnum.MANAGE_PRODUCTS]}
>
<TableRow hover={true} onClick={onProductsOutOfStockClick}>
<TableCell>
{productsOutOfStock === undefined ? (
<Skeleton />
) : productsOutOfStock === 0 ? (
<Typography>
<FormattedMessage
defaultMessage="No products out of stock"
id="homeNotificationsTableNoProducts"
/>
</Typography>
) : (
<Typography>
<FormattedMessage
defaultMessage="{amount, plural,
one {One product}
other {{amount} Products}
} out of stock"
id="homeNotificationTableProducts"
values={{
amount: <strong>{productsOutOfStock}</strong>
}}
/>
</Typography>
)}
</TableCell>
<TableCell className={classes.arrowIcon}>
<KeyboardArrowRight />
</TableCell>
</TableRow>
id="homeNotificationTableProducts"
values={{
amount: <strong>{productsOutOfStock}</strong>
}}
/>
</Typography>
)}
</TableCell>
<TableCell className={classes.arrowIcon}>
<KeyboardArrowRight />
</TableCell>
</TableRow>
</RequirePermissions>
</TableBody>
</Table>
</Card>

View file

@ -10,7 +10,10 @@ import CardSpacer from "@saleor/components/CardSpacer";
import Container from "@saleor/components/Container";
import Grid from "@saleor/components/Grid";
import Money from "@saleor/components/Money";
import RequirePermissions from "@saleor/components/RequirePermissions";
import Skeleton from "@saleor/components/Skeleton";
import { UserPermissionProps } from "@saleor/types";
import { PermissionEnum } from "@saleor/types/globalTypes";
import Orders from "../../../icons/Orders";
import Sales from "../../../icons/Sales";
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[];
orders: number;
ordersToCapture: number;
@ -68,35 +71,41 @@ const HomePage = withStyles(styles, { name: "HomePage" })(
onProductsOutOfStockClick,
ordersToCapture,
ordersToFulfill,
productsOutOfStock
}: HomePageProps) => (
productsOutOfStock,
userPermissions
}: HomePageProps & WithStyles<typeof styles>) => (
<Container>
<HomeHeader userName={userName} />
<CardSpacer />
<Grid>
<div>
<div className={classes.cardContainer}>
<HomeAnalyticsCard
title={"Sales"}
icon={<Sales fontSize={"inherit"} viewBox="0 0 64 64" />}
>
{sales ? (
<Money money={sales} />
) : (
<Skeleton style={{ width: "5em" }} />
)}
</HomeAnalyticsCard>
<HomeAnalyticsCard
title={"Orders"}
icon={<Orders fontSize={"inherit"} viewBox="0 0 64 64" />}
>
{orders === undefined ? (
<Skeleton style={{ width: "5em" }} />
) : (
orders
)}
</HomeAnalyticsCard>
</div>
<RequirePermissions
userPermissions={userPermissions}
requiredPermissions={[PermissionEnum.MANAGE_ORDERS]}
>
<div className={classes.cardContainer}>
<HomeAnalyticsCard
title={"Sales"}
icon={<Sales fontSize={"inherit"} viewBox="0 0 64 64" />}
>
{sales ? (
<Money money={sales} />
) : (
<Skeleton style={{ width: "5em" }} />
)}
</HomeAnalyticsCard>
<HomeAnalyticsCard
title={"Orders"}
icon={<Orders fontSize={"inherit"} viewBox="0 0 64 64" />}
>
{orders === undefined ? (
<Skeleton style={{ width: "5em" }} />
) : (
orders
)}
</HomeAnalyticsCard>
</div>
</RequirePermissions>
<HomeNotificationTable
onOrdersToCaptureClick={onOrdersToCaptureClick}
onOrdersToFulfillClick={onOrdersToFulfillClick}
@ -104,16 +113,30 @@ const HomePage = withStyles(styles, { name: "HomePage" })(
ordersToCapture={ordersToCapture}
ordersToFulfill={ordersToFulfill}
productsOutOfStock={productsOutOfStock}
userPermissions={userPermissions}
/>
<CardSpacer />
<HomeProductListCard
onRowClick={onProductClick}
topProducts={topProducts}
/>
<CardSpacer />
<RequirePermissions
userPermissions={userPermissions}
requiredPermissions={[
PermissionEnum.MANAGE_ORDERS,
PermissionEnum.MANAGE_PRODUCTS
]}
>
<HomeProductListCard
onRowClick={onProductClick}
topProducts={topProducts}
/>
<CardSpacer />
</RequirePermissions>
</div>
<div>
<HomeActivityCard activities={activities} />
<RequirePermissions
userPermissions={userPermissions}
requiredPermissions={[PermissionEnum.MANAGE_ORDERS]}
>
<HomeActivityCard activities={activities} />
</RequirePermissions>
</div>
</Grid>
</Container>

View file

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

View file

@ -16,11 +16,13 @@ import ExternalLink from "@saleor/components/ExternalLink";
import Form from "@saleor/components/Form";
import Hr from "@saleor/components/Hr";
import Link from "@saleor/components/Link";
import RequirePermissions from "@saleor/components/RequirePermissions";
import SingleAutocompleteSelectField from "@saleor/components/SingleAutocompleteSelectField";
import Skeleton from "@saleor/components/Skeleton";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
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 { SearchCustomers_search_edges_node } from "../../../containers/SearchCustomers/types/SearchCustomers";
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;
users?: SearchCustomers_search_edges_node[];
loading?: boolean;
@ -72,6 +76,7 @@ const OrderCustomer = withStyles(styles, { name: "OrderCustomer" })(
loading,
order,
users,
userPermissions,
onCustomerEdit,
onBillingAddressEdit,
onFetchMore: onFetchMoreUsers,
@ -81,6 +86,7 @@ const OrderCustomer = withStyles(styles, { name: "OrderCustomer" })(
const intl = useIntl();
const user = maybe(() => order.user);
const userEmail = maybe(()=>order.userEmail)
const [userDisplayName, setUserDisplayName] = useStateFromProps(
maybe(() => user.email, "")
@ -100,14 +106,19 @@ const OrderCustomer = withStyles(styles, { name: "OrderCustomer" })(
})}
toolbar={
!!canEditCustomer && (
<Button
color="primary"
variant="text"
disabled={!onCustomerEdit}
onClick={toggleEditMode}
<RequirePermissions
userPermissions={userPermissions}
requiredPermissions={[PermissionEnum.MANAGE_USERS]}
>
{intl.formatMessage(buttonMessages.edit)}
</Button>
<Button
color="primary"
variant="text"
disabled={!onCustomerEdit}
onClick={toggleEditMode}
>
{intl.formatMessage(buttonMessages.edit)}
</Button>
</RequirePermissions>
)
}
/>
@ -155,26 +166,35 @@ const OrderCustomer = withStyles(styles, { name: "OrderCustomer" })(
}}
</Form>
) : user === null ? (
<Typography>
<FormattedMessage defaultMessage="Anonymous user" />
</Typography>
userEmail === null ? (
<Typography>
<FormattedMessage defaultMessage="Anonymous user" />
</Typography>
) : (
<Typography className={classes.userEmail}>{userEmail}</Typography>
)
) : (
<>
<Typography className={classes.userEmail}>
{user.email}
</Typography>
<div>
<Link
underline={false}
href={createHref(customerUrl(user.id))}
onClick={onProfileView}
>
<FormattedMessage
defaultMessage="View Profile"
description="link"
/>
</Link>
</div>
<RequirePermissions
userPermissions={userPermissions}
requiredPermissions={[PermissionEnum.MANAGE_USERS]}
>
<div>
<Link
underline={false}
href={createHref(customerUrl(user.id))}
onClick={onProfileView}
>
<FormattedMessage
defaultMessage="View Profile"
description="link"
/>
</Link>
</div>
</RequirePermissions>
{/* TODO: Uncomment it after adding ability to filter
orders by customer */}
{/* <div>
@ -187,36 +207,40 @@ const OrderCustomer = withStyles(styles, { name: "OrderCustomer" })(
</>
)}
</CardContent>
<Hr />
<CardContent>
<div className={classes.sectionHeader}>
<Typography className={classes.sectionHeaderTitle}>
<FormattedMessage
defaultMessage="Contact Information"
description="subheader"
/>
</Typography>
</div>
{!!user && (
<>
<Hr />
<CardContent>
<div className={classes.sectionHeader}>
<Typography className={classes.sectionHeaderTitle}>
<FormattedMessage
defaultMessage="Contact Information"
description="subheader"
/>
</Typography>
</div>
{maybe(() => order.userEmail) === undefined ? (
<Skeleton />
) : order.userEmail === null ? (
<Typography>
<FormattedMessage
defaultMessage="Not set"
description="customer is not set in draft order"
id="orderCustomerCustomerNotSet"
/>
</Typography>
) : (
<ExternalLink
href={`mailto:${maybe(() => order.userEmail)}`}
typographyProps={{ color: "primary" }}
>
{maybe(() => order.userEmail)}
</ExternalLink>
)}
</CardContent>
{maybe(() => order.userEmail) === undefined ? (
<Skeleton />
) : order.userEmail === null ? (
<Typography>
<FormattedMessage
defaultMessage="Not set"
description="customer is not set in draft order"
id="orderCustomerCustomerNotSet"
/>
</Typography>
) : (
<ExternalLink
href={`mailto:${maybe(() => order.userEmail)}`}
typographyProps={{ color: "primary" }}
>
{maybe(() => order.userEmail)}
</ExternalLink>
)}
</CardContent>
</>
)}
<Hr />
<CardContent>
<div className={classes.sectionHeader}>

View file

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

View file

@ -18,7 +18,7 @@ import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar";
import Skeleton from "@saleor/components/Skeleton";
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 { maybe } from "../../../misc";
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;
order: OrderDetails_order;
users: SearchCustomers_search_edges_node[];
@ -90,7 +92,8 @@ const OrderDraftPage = withStyles(styles, { name: "OrderDraftPage" })(
onProfileView,
order,
users,
usersLoading
usersLoading,
userPermissions
}: OrderDraftPageProps & WithStyles<typeof styles>) => {
const intl = useIntl();
@ -147,6 +150,7 @@ const OrderDraftPage = withStyles(styles, { name: "OrderDraftPage" })(
loading={usersLoading}
order={order}
users={users}
userPermissions={userPermissions}
onBillingAddressEdit={onBillingAddressEdit}
onCustomerEdit={onCustomerEdit}
onFetchMore={onFetchMore}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
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 { StaffMemberDetails_user } from "./types/StaffMemberDetails";
@ -145,49 +145,5 @@ export const staffMember: StaffMemberDetails_user = {
id: "VXNlcjoyMQ==",
isActive: true,
lastName: "Smith",
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
}))
permissions
};

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 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 { shop as shopFixture } from "../../../home/fixtures";
import Decorator from "../../Decorator";
@ -21,7 +23,8 @@ const homePageProps: Omit<HomePageProps, "classes"> = {
productsOutOfStock: shop.productsOutOfStock.totalCount,
sales: shop.salesToday.gross,
topProducts: shop.productTopToday.edges.map(edge => edge.node),
userName: "admin@example.com"
userName: "admin@example.com",
userPermissions: permissions
};
storiesOf("Views / HomePage", module)
@ -42,4 +45,23 @@ storiesOf("Views / HomePage", module)
))
.add("no data", () => (
<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 React from "react";
import { permissions } from "@saleor/fixtures";
import OrderCustomer, {
OrderCustomerProps
} from "../../../orders/components/OrderCustomer";
@ -19,6 +20,7 @@ const props: Omit<OrderCustomerProps, "classes"> = {
onProfileView: () => undefined,
onShippingAddressEdit: undefined,
order,
userPermissions: permissions,
users: clients
};
@ -37,4 +39,10 @@ storiesOf("Orders / OrderCustomer", module)
))
.add("editable", () => (
<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 placeholderImage from "@assets/images/placeholder60x60.png";
import { permissions } from "@saleor/fixtures";
import OrderDetailsPage, {
OrderDetailsPageProps
} from "../../../orders/components/OrderDetailsPage";
@ -32,7 +33,8 @@ const props: Omit<OrderDetailsPageProps, "classes"> = {
onProductClick: undefined,
onProfileView: () => undefined,
onShippingAddressEdit: undefined,
order
order,
userPermissions: permissions
};
storiesOf("Views / Orders / Order details", module)

View file

@ -3,7 +3,7 @@ import { storiesOf } from "@storybook/react";
import React from "react";
import placeholderImage from "@assets/images/placeholder60x60.png";
import { fetchMoreProps } from "@saleor/fixtures";
import { fetchMoreProps, permissions } from "@saleor/fixtures";
import OrderDraftPage, {
OrderDraftPageProps
} from "../../../orders/components/OrderDraftPage";
@ -32,6 +32,7 @@ const props: Omit<OrderDraftPageProps, "classes"> = {
onShippingMethodEdit: undefined,
order,
saveButtonBarState: "default",
userPermissions: permissions,
users: clients,
usersLoading: false
};
@ -44,4 +45,7 @@ storiesOf("Views / Orders / Order draft", module)
))
.add("without 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 React from "react";
import { listActionsProps, pageListProps } from "../../../fixtures";
import {
listActionsProps,
pageListProps,
permissions
} from "../../../fixtures";
import ShippingZonesListPage, {
ShippingZonesListPageProps
} from "../../../shipping/components/ShippingZonesListPage";
@ -17,7 +21,8 @@ const props: ShippingZonesListPageProps = {
onBack: () => undefined,
onRemove: () => undefined,
onSubmit: () => undefined,
shippingZones
shippingZones,
userPermissions: permissions
};
storiesOf("Views / Shipping / Shipping zones list", module)
@ -30,6 +35,7 @@ storiesOf("Views / Shipping / Shipping zones list", module)
shippingZones={undefined}
/>
))
.add("no data", () => (
<ShippingZonesListPage {...props} shippingZones={[]} />
.add("no data", () => <ShippingZonesListPage {...props} shippingZones={[]} />)
.add("no site settings permissions", () => (
<ShippingZonesListPage {...props} userPermissions={[]} />
));

View file

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

View file

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