Permission groups section (#406)

* Add permission groups 👪
This commit is contained in:
Krzysztof Wolski 2020-04-23 17:43:08 +02:00 committed by GitHub
parent 7878067c9c
commit 082a12e0b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
117 changed files with 16289 additions and 3180 deletions

View file

@ -44,6 +44,7 @@ All notable, unreleased changes to this project will be documented in this file.
- Reset pagination when guest change the sorting of the list - #474 by @gabmartinez
- Filter column ids before send it to GridAttributes operation - #476 by @gabmartinez
- Display Is Published column correctly in main Product Listing - #475 by @gabmartinez
- Add Permission Groups section - #406 by @krzysztofwolski
## 2.0.0

View file

@ -16,6 +16,9 @@
"configurationMenuPages": {
"string": "Manage and add additional pages"
},
"configurationMenuPermissionGroups": {
"string": "Manage your permission groups and their permissions"
},
"configurationMenuProductTypes": {
"string": "Define types of products you sell"
},
@ -902,16 +905,24 @@
"context": "dialog title",
"string": "Delete collections"
},
"src_dot_components_dot_AccountPermissions_dot_1848599267": {
"context": "checkbox label",
"string": "User has full access to the store"
"src_dot_components_dot_AccountPermissionGroups_dot_2836633439": {
"string": "Permission groups"
},
"src_dot_components_dot_AccountPermissions_dot_2690176844": {
"context": "dialog header",
"string": "Permissions"
},
"src_dot_components_dot_AccountPermissions_dot_3639008725": {
"string": "Expand or restrict user's permissions to access certain part of saleor system."
"src_dot_components_dot_AccountPermissions_dot_2731975416": {
"context": "card section description",
"string": "Available permissions"
},
"src_dot_components_dot_AccountPermissions_dot_2853621891": {
"context": "exceeded permissions description",
"string": "This groups permissions exceeds your own. You are able only to manage permissions that you have."
},
"src_dot_components_dot_AccountPermissions_dot_3674526441": {
"context": "permission list item description",
"string": "This group is last source of that permission"
},
"src_dot_components_dot_AccountStatus_dot_2183517419": {
"context": "section header",
@ -1193,6 +1204,10 @@
"context": "button",
"string": "Create Order"
},
"src_dot_components_dot_Navigator_dot_modes_dot_createPermissionGroup": {
"context": "button",
"string": "Create Permission Group"
},
"src_dot_components_dot_Navigator_dot_modes_dot_createProduct": {
"context": "button",
"string": "Create Product"
@ -2930,6 +2945,144 @@
"context": "payment status",
"string": "Partially refunded"
},
"src_dot_permissionGroups": {
"context": "permission groups section name",
"string": "Permission Groups"
},
"src_dot_permissionGroups_dot_components_dot_AssignMembersDialog_dot_1004218338": {
"context": "staff member status",
"string": "Inactive"
},
"src_dot_permissionGroups_dot_components_dot_AssignMembersDialog_dot_1731102929": {
"context": "button",
"string": "Assign"
},
"src_dot_permissionGroups_dot_components_dot_AssignMembersDialog_dot_2986043376": {
"context": "dialog header",
"string": "Assign Staff Members"
},
"src_dot_permissionGroups_dot_components_dot_AssignMembersDialog_dot_3111990517": {
"string": "Search by name, email, etc..."
},
"src_dot_permissionGroups_dot_components_dot_AssignMembersDialog_dot_3247064221": {
"context": "staff member status",
"string": "Active"
},
"src_dot_permissionGroups_dot_components_dot_AssignMembersDialog_dot_3532084010": {
"string": "Search Staff Members"
},
"src_dot_permissionGroups_dot_components_dot_MembersErrorDialog_dot_3005518110": {
"context": "dialog title",
"string": "Unassign users"
},
"src_dot_permissionGroups_dot_components_dot_MembersErrorDialog_dot_334280454": {
"context": "dialog content",
"string": "You are not able to modify this group members. Solve this problem to continue with request."
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupCreatePage_dot_3765873075": {
"context": "checkbox label",
"string": "Group has full access to the store"
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupCreatePage_dot_3920591233": {
"context": "card description",
"string": "Expand or restrict group's permissions to access certain part of saleor system."
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupDeleteDialog_dot_2297471173": {
"context": "dialog content",
"string": "Are you sure you want to delete {name}?"
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupDeleteDialog_dot_3528974446": {
"context": "dialog title",
"string": "Delete permission group"
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupDetailsPage_dot_3765873075": {
"context": "checkbox label",
"string": "Group has full access to the store"
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupDetailsPage_dot_3920591233": {
"context": "card description",
"string": "Expand or restrict group's permissions to access certain part of saleor system."
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupInfo_dot_2081191949": {
"context": "text field label",
"string": "Group name"
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupListPage_dot_613749311": {
"context": "button",
"string": "create permission group"
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupList_dot_2011544055": {
"context": "permission group name",
"string": "Permission Group Name"
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupList_dot_2734377620": {
"string": "Members"
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupList_dot_4190792473": {
"string": "Actions"
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupList_dot_425941108": {
"string": "No permission groups found"
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupMemberList_dot_1004218338": {
"context": "staff member status",
"string": "Inactive"
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupMemberList_dot_136112637": {
"context": "empty list message",
"string": "You havent assigned any member to this permission group yet."
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupMemberList_dot_1512872240": {
"string": "No members found"
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupMemberList_dot_1789607185": {
"string": "Email Address"
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupMemberList_dot_2457011428": {
"context": "button",
"string": "Assign members"
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupMemberList_dot_2652621342": {
"context": "empty list message",
"string": "Please use Assign Members button to do so."
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupMemberList_dot_3247064221": {
"context": "staff member status",
"string": "Active"
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupMemberList_dot_3397233744": {
"context": "header",
"string": "Group members"
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupMemberList_dot_4190792473": {
"string": "Actions"
},
"src_dot_permissionGroups_dot_components_dot_PermissionGroupMemberList_dot_636461959": {
"context": "staff member full name",
"string": "Name"
},
"src_dot_permissionGroups_dot_components_dot_UnassignMembersDialog_dot_3005518110": {
"context": "dialog title",
"string": "Unassign users"
},
"src_dot_permissionGroups_dot_components_dot_UnassignMembersDialog_dot_778652830": {
"context": "dialog content",
"string": "Are you sure you want to unassign {counter,plural,one{this member} other{{displayQuantity} members}}?"
},
"src_dot_permissionGroups_dot_views_dot_PermissionGroupCreate_dot_1140231710": {
"context": "window title",
"string": "Create category"
},
"src_dot_permissionGroups_dot_views_dot_PermissionGroupCreate_dot_3930588328": {
"string": "Permission group created"
},
"src_dot_permissionGroups_dot_views_dot_PermissionGroupDetails_dot_870815507": {
"context": "button title",
"string": "Unassign"
},
"src_dot_permissionGroups_dot_views_dot_PermissionGroupList_dot_92979760": {
"string": "Permission Group Deleted"
},
"src_dot_plugins": {
"context": "plugins section name",
"string": "Plugins"
@ -3698,6 +3851,10 @@
"context": "service accounts section name",
"string": "Service Accounts"
},
"src_dot_services_dot_components_dot_ServiceCreatePage_dot_1848599267": {
"context": "checkbox label",
"string": "User has full access to the store"
},
"src_dot_services_dot_components_dot_ServiceCreatePage_dot_248507553": {
"context": "header",
"string": "Create New Account"
@ -3706,6 +3863,10 @@
"context": "checkbox label",
"string": "Service account is active"
},
"src_dot_services_dot_components_dot_ServiceCreatePage_dot_3639008725": {
"context": "card description",
"string": "Expand or restrict user's permissions to access certain part of saleor system."
},
"src_dot_services_dot_components_dot_ServiceDefaultToken_dot_1336855942": {
"string": "Generated Token"
},
@ -3731,10 +3892,18 @@
"context": "delete service account",
"string": "Are you sure you want to delete {name}?"
},
"src_dot_services_dot_components_dot_ServiceDetailsPage_dot_1848599267": {
"context": "checkbox label",
"string": "User has full access to the store"
},
"src_dot_services_dot_components_dot_ServiceDetailsPage_dot_27827485": {
"context": "checkbox label",
"string": "Service account is active"
},
"src_dot_services_dot_components_dot_ServiceDetailsPage_dot_3639008725": {
"context": "card description",
"string": "Expand or restrict user's permissions to access certain part of saleor system."
},
"src_dot_services_dot_components_dot_ServiceInfo_dot_3789449123": {
"context": "service account",
"string": "Account Name"
@ -4222,19 +4391,18 @@
"context": "dialog header",
"string": "Invite Staff Member"
},
"src_dot_staff_dot_components_dot_StaffAddMemberDialog_dot_1570990296": {
"string": "User has full access"
},
"src_dot_staff_dot_components_dot_StaffAddMemberDialog_dot_2690176844": {
"string": "Permissions"
},
"src_dot_staff_dot_components_dot_StaffAddMemberDialog_dot_351138560": {
"string": "Expand or restrict users permissions to access certain part of saleor system."
},
"src_dot_staff_dot_components_dot_StaffAddMemberDialog_dot_449055697": {
"context": "button",
"string": "Send invite"
},
"src_dot_staff_dot_components_dot_StaffDetailsPage_dot_22572205": {
"context": "card description",
"string": "User is assigned to:"
},
"src_dot_staff_dot_components_dot_StaffDetailsPage_dot_2690176844": {
"context": "dialog header",
"string": "Permissions"
},
"src_dot_staff_dot_components_dot_StaffDetailsPage_dot_881953347": {
"context": "checkbox label",
"string": "User is active"
@ -4721,6 +4889,9 @@
"context": "add authorization key error",
"string": "Authorization key with this type already exists"
},
"src_dot_utils_dot_errors_dot_assignNonStaffMember": {
"string": "Only staff members can be assigned"
},
"src_dot_utils_dot_errors_dot_attributeAlreadyAssigned": {
"string": "This attribute has already been assigned to this product type"
},
@ -4734,6 +4905,9 @@
"context": "error message",
"string": "Billing address is not set"
},
"src_dot_utils_dot_errors_dot_cannotAddAndRemove": {
"string": "Cannot add and remove group the same time"
},
"src_dot_utils_dot_errors_dot_cannotCancelFulfillment": {
"context": "error message",
"string": "This fulfillment cannot be cancelled"
@ -4750,6 +4924,9 @@
"context": "error message",
"string": "Manual payments can not be refunded"
},
"src_dot_utils_dot_errors_dot_cannotRemoveFromLastGroup": {
"string": "Cannot remove user from last group"
},
"src_dot_utils_dot_errors_dot_cannotVoid": {
"context": "error message",
"string": "Only pre-authorized payments can be voided"
@ -4775,6 +4952,12 @@
"context": "error message",
"string": "Only draft orders can be edited"
},
"src_dot_utils_dot_errors_dot_outOfScopeGroup": {
"string": "Group is out of your permissions scope"
},
"src_dot_utils_dot_errors_dot_outOfScopeUser": {
"string": "User is out of your permissions scope"
},
"src_dot_utils_dot_errors_dot_passwordNumeric": {
"string": "Password cannot be entirely numeric"
},
@ -4782,6 +4965,9 @@
"context": "error message",
"string": "There's no payment associated with the order"
},
"src_dot_utils_dot_errors_dot_permissionOutOfScope": {
"string": "Those permissions are out of your scope"
},
"src_dot_utils_dot_errors_dot_shippingNotApplicable": {
"context": "error message",
"string": "Shipping method is not valid for chosen shipping address"
@ -4803,6 +4989,9 @@
"src_dot_utils_dot_errors_dot_tooSimilar": {
"string": "These passwords are too similar"
},
"src_dot_utils_dot_errors_dot_unique": {
"string": "This needs to be unique"
},
"src_dot_utils_dot_errors_dot_unknownError": {
"string": "Unknown error"
},

File diff suppressed because it is too large Load diff

View file

@ -2,4 +2,4 @@ import { PermissionEnum } from "../types/globalTypes";
import { User } from "./types/User";
export const hasPermission = (permission: PermissionEnum, user: User) =>
user.permissions.map(perm => perm.code).includes(permission);
user.userPermissions.map(perm => perm.code).includes(permission);

View file

@ -1,6 +1,6 @@
import gql from "graphql-tag";
import { accountFragmentError } from "@saleor/customers/mutations";
import { accountErrorFragment } from "@saleor/customers/mutations";
import { TypedMutation } from "../mutations";
import {
RequestPasswordReset,
@ -16,7 +16,7 @@ export const fragmentUser = gql`
email
firstName
lastName
permissions {
userPermissions {
code
name
}
@ -65,7 +65,7 @@ export const TypedVerifyTokenMutation = TypedMutation<
>(tokenVerifyMutation);
export const requestPasswordReset = gql`
${accountFragmentError}
${accountErrorFragment}
mutation RequestPasswordReset($email: String!, $redirectUrl: String!) {
requestPasswordReset(email: $email, redirectUrl: $redirectUrl) {
errors: accountErrors {
@ -80,7 +80,7 @@ export const RequestPasswordResetMutation = TypedMutation<
>(requestPasswordReset);
export const setPassword = gql`
${accountFragmentError}
${accountErrorFragment}
${fragmentUser}
mutation SetPassword($email: String!, $password: String!, $token: String!) {
setPassword(email: $email, password: $password, token: $token) {

View file

@ -14,8 +14,8 @@ export interface SetPassword_setPassword_errors {
field: string | null;
}
export interface SetPassword_setPassword_user_permissions {
__typename: "Permission";
export interface SetPassword_setPassword_user_userPermissions {
__typename: "UserPermission";
code: PermissionEnum;
name: string;
}
@ -31,7 +31,7 @@ export interface SetPassword_setPassword_user {
email: string;
firstName: string;
lastName: string;
permissions: (SetPassword_setPassword_user_permissions | null)[] | null;
userPermissions: (SetPassword_setPassword_user_userPermissions | null)[] | null;
avatar: SetPassword_setPassword_user_avatar | null;
}

View file

@ -14,8 +14,8 @@ export interface TokenAuth_tokenCreate_errors {
message: string | null;
}
export interface TokenAuth_tokenCreate_user_permissions {
__typename: "Permission";
export interface TokenAuth_tokenCreate_user_userPermissions {
__typename: "UserPermission";
code: PermissionEnum;
name: string;
}
@ -31,7 +31,7 @@ export interface TokenAuth_tokenCreate_user {
email: string;
firstName: string;
lastName: string;
permissions: (TokenAuth_tokenCreate_user_permissions | null)[] | null;
userPermissions: (TokenAuth_tokenCreate_user_userPermissions | null)[] | null;
avatar: TokenAuth_tokenCreate_user_avatar | null;
}

View file

@ -8,8 +8,8 @@ import { PermissionEnum } from "./../../types/globalTypes";
// GraphQL fragment: User
// ====================================================
export interface User_permissions {
__typename: "Permission";
export interface User_userPermissions {
__typename: "UserPermission";
code: PermissionEnum;
name: string;
}
@ -25,6 +25,6 @@ export interface User {
email: string;
firstName: string;
lastName: string;
permissions: (User_permissions | null)[] | null;
userPermissions: (User_userPermissions | null)[] | null;
avatar: User_avatar | null;
}

View file

@ -8,8 +8,8 @@ import { PermissionEnum } from "./../../types/globalTypes";
// GraphQL mutation operation: VerifyToken
// ====================================================
export interface VerifyToken_tokenVerify_user_permissions {
__typename: "Permission";
export interface VerifyToken_tokenVerify_user_userPermissions {
__typename: "UserPermission";
code: PermissionEnum;
name: string;
}
@ -25,7 +25,7 @@ export interface VerifyToken_tokenVerify_user {
email: string;
firstName: string;
lastName: string;
permissions: (VerifyToken_tokenVerify_user_permissions | null)[] | null;
userPermissions: (VerifyToken_tokenVerify_user_userPermissions | null)[] | null;
avatar: VerifyToken_tokenVerify_user_avatar | null;
}

View file

@ -0,0 +1,79 @@
import React from "react";
import { useIntl } from "react-intl";
import Typography from "@material-ui/core/Typography";
import { FormChange } from "@saleor/hooks/useForm";
import { SearchPermissionGroups_search_edges_node } from "@saleor/searches/types/SearchPermissionGroups";
import { FetchMoreProps, SearchPageProps } from "@saleor/types";
import { getFormErrors } from "@saleor/utils/errors";
import { StaffErrorFragment } from "@saleor/staff/types/StaffErrorFragment";
import getStaffErrorMessage from "@saleor/utils/errors/staff";
import MultiAutocompleteSelectField, {
MultiAutocompleteChoiceType
} from "../MultiAutocompleteSelectField";
interface AccountPermissionGroupsProps extends FetchMoreProps, SearchPageProps {
formData: {
permissionGroups: string[];
};
disabled: boolean;
errors: StaffErrorFragment[];
availablePermissionGroups: SearchPermissionGroups_search_edges_node[];
onChange: FormChange;
displayValues: MultiAutocompleteChoiceType[];
}
const AccountPermissionGroups: React.FC<AccountPermissionGroupsProps> = props => {
const {
availablePermissionGroups,
disabled,
displayValues,
errors,
formData,
hasMore,
loading,
onChange,
onFetchMore,
onSearchChange
} = props;
const intl = useIntl();
const choices = availablePermissionGroups?.map(pg => ({
label: pg.name,
value: pg.id
}));
const formErrors = getFormErrors(["addGroups", "removeGroups"], errors);
return (
<>
<MultiAutocompleteSelectField
displayValues={displayValues}
label={intl.formatMessage({
defaultMessage: "Permission groups"
})}
choices={disabled ? [] : choices}
name="permissionGroups"
value={formData?.permissionGroups}
onChange={onChange}
fetchChoices={onSearchChange}
data-tc="permissionGroups"
onFetchMore={onFetchMore}
hasMore={hasMore}
loading={loading}
/>
{!!formErrors.addGroups && (
<Typography color="error">
{getStaffErrorMessage(formErrors.addGroups, intl)}
</Typography>
)}
{!!formErrors.removeGroups && (
<Typography color="error">
{getStaffErrorMessage(formErrors.removeGroups, intl)}
</Typography>
)}
</>
);
};
AccountPermissionGroups.displayName = "AccountPermissionGroups";
export default AccountPermissionGroups;

View file

@ -0,0 +1,2 @@
export { default } from "./AccountPermissionGroups";
export * from "./AccountPermissionGroups";

View file

@ -1,14 +1,20 @@
import React from "react";
import { useIntl } from "react-intl";
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle";
import { ControlledCheckbox } from "@saleor/components/ControlledCheckbox";
import { ShopInfo_shop_permissions } from "@saleor/components/Shop/types/ShopInfo";
import Skeleton from "@saleor/components/Skeleton";
import {
List,
ListItem,
ListItemText,
ListItemIcon,
Checkbox
} from "@material-ui/core";
import useUser from "@saleor/hooks/useUser";
import { PermissionData } from "@saleor/permissionGroups/components/PermissionGroupDetailsPage/PermissionGroupDetailsPage";
const useStyles = makeStyles(
theme => ({
@ -27,40 +33,60 @@ const useStyles = makeStyles(
);
interface AccountPermissionsProps {
permissions: ShopInfo_shop_permissions[];
permissions: PermissionData[];
permissionsExceeded: boolean;
data: {
hasFullAccess: boolean;
permissions: string[];
};
disabled: boolean;
description: string;
errorMessage: string;
fullAccessLabel: string;
onChange: (event: React.ChangeEvent<any>, cb?: () => void) => void;
}
const AccountPermissions: React.FC<AccountPermissionsProps> = props => {
const { data, disabled, permissions, onChange } = props;
const {
data,
disabled,
permissions,
permissionsExceeded,
onChange,
description,
fullAccessLabel,
errorMessage
} = props;
const classes = useStyles(props);
const intl = useIntl();
const { user } = useUser();
const handleFullAccessChange = (event: React.ChangeEvent<any>) =>
onChange(event, () =>
onChange({
target: {
name: "permissions",
value: event.target.value ? permissions.map(perm => perm.code) : []
}
} as any)
);
const handlePermissionChange = (event: React.ChangeEvent<any>) => {
const handleFullAccessChange = () => {
onChange({
target: {
name: "permissions",
value: event.target.value
? data.permissions.concat([event.target.name])
: data.permissions.filter(perm => perm !== event.target.name)
value: !data.hasFullAccess ? permissions.map(perm => perm.code) : []
}
} as any);
onChange({
target: {
name: "hasFullAccess",
value: !data.hasFullAccess
}
} as any);
};
const handlePermissionChange = (key, value) => () => {
onChange({
target: {
name: "permissions",
value: !value
? data.permissions.concat([key])
: data.permissions.filter(perm => perm !== key)
}
} as any);
};
return (
<Card>
<CardTitle
@ -69,49 +95,121 @@ const AccountPermissions: React.FC<AccountPermissionsProps> = props => {
description: "dialog header"
})}
/>
<CardContent>
<Typography>
<FormattedMessage defaultMessage="Expand or restrict user's permissions to access certain part of saleor system." />
</Typography>
<div className={classes.checkboxContainer}>
<ControlledCheckbox
checked={data.hasFullAccess}
disabled={disabled}
label={intl.formatMessage({
defaultMessage: "User has full access to the store",
description: "checkbox label"
})}
name="hasFullAccess"
onChange={handleFullAccessChange}
/>
</div>
</CardContent>
{!data.hasFullAccess && (
{permissionsExceeded && (
<>
<CardContent>
<Typography>
{intl.formatMessage({
defaultMessage:
"This groups permissions exceeds your own. You are able only to manage permissions that you have.",
description: "exceeded permissions description"
})}
</Typography>
</CardContent>
<hr className={classes.hr} />
<CardContent>
{permissions === undefined ? (
<Skeleton />
) : (
permissions.map(perm => (
<div key={perm.code}>
<ControlledCheckbox
checked={
data.permissions.filter(
userPerm => userPerm === perm.code
).length === 1
}
disabled={disabled}
label={perm.name.replace(/\./, "")}
name={perm.code}
onChange={handlePermissionChange}
/>
</div>
))
)}
<Typography>
{intl.formatMessage({
defaultMessage: "Available permissions",
description: "card section description"
})}
</Typography>
<List dense={true}>
{user.userPermissions.map(perm => (
<ListItem key={perm.code}>
<ListItemText primary={`- ${perm.name}`} />
</ListItem>
))}
</List>
</CardContent>
</>
)}
{!permissionsExceeded && (
<>
<CardContent>
<Typography>{description}</Typography>
<ListItem
role={undefined}
dense
button
onClick={handleFullAccessChange}
>
<ListItemIcon>
<Checkbox
edge="start"
checked={data.hasFullAccess}
disabled={disabled}
tabIndex={-1}
disableRipple
inputProps={{ "aria-labelledby": "fullAccess" }}
/>
</ListItemIcon>
<ListItemText primary={fullAccessLabel} />
</ListItem>
</CardContent>
{!data.hasFullAccess && (
<>
<hr className={classes.hr} />
<CardContent>
{permissions === undefined ? (
<Skeleton />
) : (
permissions.map(perm => (
<ListItem
key={perm.code}
disabled={perm.disabled}
role={undefined}
dense
button
onClick={handlePermissionChange(
perm.code,
data.permissions.filter(
userPerm => userPerm === perm.code
).length === 1
)}
>
<ListItemIcon>
<Checkbox
edge="start"
checked={
data.permissions.filter(
userPerm => userPerm === perm.code
).length === 1
}
tabIndex={-1}
disableRipple
name={perm.code}
inputProps={{ "aria-labelledby": perm.code }}
/>
</ListItemIcon>
<ListItemText
id={perm.code}
primary={perm.name.replace(/\./, "")}
secondary={
perm.lastSource &&
intl.formatMessage({
defaultMessage:
"This group is last source of that permission",
description: "permission list item description"
})
}
/>
</ListItem>
))
)}
</CardContent>
</>
)}
{!!errorMessage && (
<>
<hr className={classes.hr} />
<CardContent>
<Typography color="error">{errorMessage}</Typography>
</CardContent>
</>
)}
</>
)}
</Card>
);
};

View file

@ -0,0 +1,30 @@
import { User_userPermissions } from "@saleor/auth/types/User";
import { ShopInfo_shop_permissions } from "@saleor/components/Shop/types/ShopInfo";
import { PermissionGroupDetails_user_userPermissions } from "@saleor/permissionGroups/types/PermissionGroupDetails";
export const getLastSourcesOfPermission = (
groupId: string,
userPermissions: PermissionGroupDetails_user_userPermissions[]
) =>
userPermissions
.filter(
perm =>
perm.sourcePermissionGroups.length === 1 &&
perm.sourcePermissionGroups[0].id === groupId
)
.map(perm => perm.code);
export const getPermissionsComponentChoices = (
userPermissions: User_userPermissions[],
shopPermissions: ShopInfo_shop_permissions[],
lastSourcesOfPermissionIds: string[]
) => {
const userCodes = userPermissions.map(p => p.code) || [];
return shopPermissions.map(perm => ({
...perm,
__typename: "PermissionData",
disabled: !userCodes.includes(perm.code),
lastSource: lastSourcesOfPermissionIds.includes(perm.code)
}));
};

View file

@ -25,7 +25,6 @@ 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 ErrorPage from "../ErrorPage";
@ -314,7 +313,7 @@ const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
const menuStructure = createMenuStructure(intl);
const configurationMenu = createConfigurationMenu(intl);
const userPermissions = maybe(() => user.permissions, []);
const userPermissions = user?.userPermissions || [];
const renderConfigure = configurationMenu.some(section =>
section.menuItems.some(

View file

@ -195,7 +195,7 @@ const MenuList: React.FC<MenuListProps> = props => {
const configutationMenu = createConfigurationMenu(intl).map(menu => {
menu.menuItems.map(item =>
user.permissions.map(perm => perm.code).includes(item.permission)
user.userPermissions.map(perm => perm.code).includes(item.permission)
);
});
@ -238,7 +238,9 @@ const MenuList: React.FC<MenuListProps> = props => {
if (
menuItem.permission &&
!user.permissions.map(perm => perm.code).includes(menuItem.permission)
!user.userPermissions
.map(perm => perm.code)
.includes(menuItem.permission)
) {
return null;
}

View file

@ -2,12 +2,14 @@ import IconButton from "@material-ui/core/IconButton";
import { makeStyles } from "@material-ui/core/styles";
import TableCell from "@material-ui/core/TableCell";
import React from "react";
import classNames from "classnames";
import { stopPropagation } from "../../misc";
import { ICONBUTTON_SIZE } from "../../theme";
export interface IconButtonTableCellProps {
children: React.ReactNode;
className?: string;
disabled?: boolean;
onClick: () => void;
}
@ -25,17 +27,12 @@ const useStyles = makeStyles(
{ name: "IconButtonTableCell" }
);
const IconButtonTableCell: React.FC<IconButtonTableCellProps> = props => {
const {
children,
disabled,
onClick
} = props;
const { children, className, disabled, onClick } = props;
const classes = useStyles(props);
return (
<TableCell className={classes.root}>
<TableCell className={classNames(classes.root, className)}>
<IconButton
color="primary"
disabled={disabled}

View file

@ -50,6 +50,20 @@ const useStyles = makeStyles(
container: {
flexGrow: 1,
position: "relative"
},
disabledChipInner: {
"& svg": {
color: theme.palette.secondary.contrastText
},
alignItems: "center",
background: fade(theme.palette.secondary.main, 0.8),
borderRadius: 18,
color: theme.palette.primary.contrastText,
display: "flex",
justifyContent: "space-between",
margin: theme.spacing(1, 0),
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(1)
}
}),
{ name: "MultiAutocompleteSelectField" }
@ -172,16 +186,22 @@ const MultiAutocompleteSelectFieldComponent: React.FC<MultiAutocompleteSelectFie
<div className={classes.chipContainer}>
{displayValues.map(value => (
<div className={classes.chip} key={value.value}>
<div className={classes.chipInner}>
<div
className={
!value.disabled ? classes.chipInner : classes.disabledChipInner
}
>
<Typography className={classes.chipLabel}>
{value.label}
</Typography>
<IconButton
className={classes.chipClose}
onClick={() => handleSelect(value.value)}
>
<CloseIcon fontSize="small" />
</IconButton>
{!value.disabled && (
<IconButton
className={classes.chipClose}
onClick={() => handleSelect(value.value)}
>
<CloseIcon fontSize="small" />
</IconButton>
)}
</div>
</div>
))}

View file

@ -1,8 +1,3 @@
import CircularProgress from "@material-ui/core/CircularProgress";
import MenuItem from "@material-ui/core/MenuItem";
import Paper from "@material-ui/core/Paper";
import { makeStyles } from "@material-ui/core/styles";
import AddIcon from "@material-ui/icons/Add";
import classNames from "classnames";
import { GetItemPropsOptions } from "downshift";
import React from "react";
@ -10,11 +5,17 @@ import SVG from "react-inlinesvg";
import { FormattedMessage } from "react-intl";
import chevronDown from "@assets/images/ChevronDown.svg";
import CircularProgress from "@material-ui/core/CircularProgress";
import MenuItem from "@material-ui/core/MenuItem";
import Paper from "@material-ui/core/Paper";
import { makeStyles } from "@material-ui/core/styles";
import AddIcon from "@material-ui/icons/Add";
import Checkbox from "@saleor/components/Checkbox";
import useElementScroll, {
isScrolledToBottom
} from "@saleor/hooks/useElementScroll";
import { FetchMoreProps } from "@saleor/types";
import Hr from "../Hr";
const menuItemHeight = 46;
@ -24,6 +25,7 @@ const offset = 24;
export interface MultiAutocompleteChoiceType {
label: string;
value: any;
disabled?: boolean;
}
export interface MultiAutocompleteSelectFieldContentProps
extends Partial<FetchMoreProps> {
@ -66,7 +68,7 @@ const useStyles = makeStyles(
},
content: {
maxHeight: menuItemHeight * maxMenuItems + theme.spacing(2),
overflow: "scroll",
overflowY: "scroll",
padding: 8
},
hide: {
@ -141,9 +143,7 @@ function getChoiceIndex(
return choiceIndex;
}
const MultiAutocompleteSelectFieldContent: React.FC<
MultiAutocompleteSelectFieldContentProps
> = props => {
const MultiAutocompleteSelectFieldContent: React.FC<MultiAutocompleteSelectFieldContentProps> = props => {
const {
choices,
displayCustomValue,
@ -210,6 +210,7 @@ const MultiAutocompleteSelectFieldContent: React.FC<
className={classes.menuItem}
key={value.value}
selected={true}
disabled={value.disabled}
component="div"
{...getItemProps({
item: value.value
@ -219,6 +220,7 @@ const MultiAutocompleteSelectFieldContent: React.FC<
<Checkbox
className={classes.checkbox}
checked={true}
disabled={value.disabled}
disableRipple
/>
<span className={classes.menuItemLabel}>{value.label}</span>
@ -239,6 +241,7 @@ const MultiAutocompleteSelectFieldContent: React.FC<
className={classes.menuItem}
key={suggestion.value}
selected={highlightedIndex === choiceIndex}
disabled={suggestion.disabled}
component="div"
{...getItemProps({
index: choiceIndex,

View file

@ -2,6 +2,7 @@ import { score } from "fuzzaldrin";
import { IntlShape } from "react-intl";
import { categoryAddUrl } from "@saleor/categories/urls";
import { permissionGroupAddUrl } from "@saleor/permissionGroups/urls";
import { collectionAddUrl } from "@saleor/collections/urls";
import { customerAddUrl } from "@saleor/customers/urls";
import { voucherAddUrl } from "@saleor/discounts/urls";
@ -49,6 +50,13 @@ export function searchInCommands(
return false;
}
},
{
label: intl.formatMessage(messages.createPermissionGroup),
onClick: () => {
navigate(permissionGroupAddUrl);
return false;
}
},
{
label: intl.formatMessage(messages.createCustomer),
onClick: () => {

View file

@ -31,6 +31,10 @@ const messages = defineMessages({
defaultMessage: "Create Order",
description: "button"
},
createPermissionGroup: {
defaultMessage: "Create Permission Group",
description: "button"
},
createProduct: {
defaultMessage: "Create Product",
description: "button"

View file

@ -1,10 +1,10 @@
import React from "react";
import { User_permissions } from "@saleor/auth/types/User";
import { User_userPermissions } from "@saleor/auth/types/User";
import { PermissionEnum } from "@saleor/types/globalTypes";
export function hasPermissions(
userPermissions: User_permissions[],
userPermissions: User_userPermissions[],
requiredPermissions: PermissionEnum[]
): boolean {
return requiredPermissions.reduce(
@ -17,7 +17,7 @@ export function hasPermissions(
export interface RequirePermissionsProps {
children: React.ReactNode | React.ReactNodeArray;
requiredPermissions: PermissionEnum[];
userPermissions: User_permissions[];
userPermissions: User_userPermissions[];
}
const RequirePermissions: React.FC<RequirePermissionsProps> = ({

View file

@ -32,6 +32,7 @@ export interface AppListViewSettings {
[ListViews.SALES_LIST]: ListSettings;
[ListViews.SHIPPING_METHODS_LIST]: ListSettings;
[ListViews.STAFF_MEMBERS_LIST]: ListSettings;
[ListViews.PERMISSION_GROUP_LIST]: ListSettings;
[ListViews.VOUCHER_LIST]: ListSettings;
[ListViews.WEBHOOK_LIST]: ListSettings;
}
@ -73,6 +74,9 @@ export const defaultListSettings: AppListViewSettings = {
[ListViews.STAFF_MEMBERS_LIST]: {
rowNumber: PAGINATE_BY
},
[ListViews.PERMISSION_GROUP_LIST]: {
rowNumber: PAGINATE_BY
},
[ListViews.VOUCHER_LIST]: {
rowNumber: PAGINATE_BY
},

View file

@ -14,6 +14,7 @@ import ProductTypes from "@saleor/icons/ProductTypes";
import ShippingMethods from "@saleor/icons/ShippingMethods";
import SiteSettings from "@saleor/icons/SiteSettings";
import StaffMembers from "@saleor/icons/StaffMembers";
import PermissionGroups from "@saleor/icons/PermissionGroups";
import Taxes from "@saleor/icons/Taxes";
import Webhooks from "@saleor/icons/Webhooks";
import { sectionNames } from "@saleor/intl";
@ -26,6 +27,7 @@ import { serviceListUrl } from "@saleor/services/urls";
import { shippingZonesListUrl } from "@saleor/shipping/urls";
import { siteSettingsUrl } from "@saleor/siteSettings/urls";
import { staffListUrl } from "@saleor/staff/urls";
import { permissionGroupListUrl } from "@saleor/permissionGroups/urls";
import { taxSection } from "@saleor/taxes/urls";
import { PermissionEnum } from "@saleor/types/globalTypes";
import { webhookListUrl } from "@saleor/webhooks/urls";
@ -101,6 +103,17 @@ export function createConfigurationMenu(intl: IntlShape): MenuSection[] {
permission: PermissionEnum.MANAGE_STAFF,
title: intl.formatMessage(sectionNames.staff),
url: staffListUrl()
},
{
description: intl.formatMessage({
defaultMessage:
"Manage your permission groups and their permissions",
id: "configurationMenuPermissionGroups"
}),
icon: <PermissionGroups fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_STAFF,
title: intl.formatMessage(sectionNames.permissionGroups),
url: permissionGroupListUrl()
}
]
},

View file

@ -36,7 +36,7 @@ import {
UpdateCustomerAddressVariables
} from "./types/UpdateCustomerAddress";
export const accountFragmentError = gql`
export const accountErrorFragment = gql`
fragment AccountErrorFragment on AccountError {
code
field
@ -44,7 +44,7 @@ export const accountFragmentError = gql`
`;
const updateCustomer = gql`
${accountFragmentError}
${accountErrorFragment}
${customerDetailsFragment}
mutation UpdateCustomer($id: ID!, $input: CustomerInput!) {
customerUpdate(id: $id, input: $input) {
@ -63,7 +63,7 @@ export const TypedUpdateCustomerMutation = TypedMutation<
>(updateCustomer);
const createCustomer = gql`
${accountFragmentError}
${accountErrorFragment}
mutation CreateCustomer($input: UserCreateInput!) {
customerCreate(input: $input) {
errors: accountErrors {
@ -81,7 +81,7 @@ export const TypedCreateCustomerMutation = TypedMutation<
>(createCustomer);
const removeCustomer = gql`
${accountFragmentError}
${accountErrorFragment}
mutation RemoveCustomer($id: ID!) {
customerDelete(id: $id) {
errors: accountErrors {
@ -96,7 +96,7 @@ export const TypedRemoveCustomerMutation = TypedMutation<
>(removeCustomer);
const setCustomerDefaultAddress = gql`
${accountFragmentError}
${accountErrorFragment}
${customerAddressesFragment}
mutation SetCustomerDefaultAddress(
$addressId: ID!
@ -119,7 +119,7 @@ export const TypedSetCustomerDefaultAddressMutation = TypedMutation<
>(setCustomerDefaultAddress);
const createCustomerAddress = gql`
${accountFragmentError}
${accountErrorFragment}
${customerAddressesFragment}
${fragmentAddress}
mutation CreateCustomerAddress($id: ID!, $input: AddressInput!) {
@ -142,7 +142,7 @@ export const TypedCreateCustomerAddressMutation = TypedMutation<
>(createCustomerAddress);
const updateCustomerAddress = gql`
${accountFragmentError}
${accountErrorFragment}
${fragmentAddress}
mutation UpdateCustomerAddress($id: ID!, $input: AddressInput!) {
addressUpdate(id: $id, input: $input) {
@ -161,7 +161,7 @@ export const TypedUpdateCustomerAddressMutation = TypedMutation<
>(updateCustomerAddress);
const removeCustomerAddress = gql`
${accountFragmentError}
${accountErrorFragment}
${customerAddressesFragment}
mutation RemoveCustomerAddress($id: ID!) {
addressDelete(id: $id) {
@ -180,7 +180,7 @@ export const TypedRemoveCustomerAddressMutation = TypedMutation<
>(removeCustomerAddress);
export const bulkRemoveCustomers = gql`
${accountFragmentError}
${accountErrorFragment}
mutation BulkRemoveCustomers($ids: [ID]!) {
customerBulkDelete(ids: $ids) {
errors: accountErrors {

View file

@ -9,6 +9,7 @@ import {
TabPageProps
} from "./types";
import { PermissionEnum } from "./types/globalTypes";
import { User_userPermissions } from "./auth/types/User";
const pageInfo = {
hasNextPage: true,
@ -381,3 +382,106 @@ export const date = {
from: "2019-12-09",
to: "2019-12-38"
};
export const adminUserPermissions: User_userPermissions[] = [
{
__typename: "UserPermission",
code: PermissionEnum.MANAGE_CHECKOUTS,
name: "Manage checkouts"
},
{
__typename: "UserPermission",
code: PermissionEnum.MANAGE_CHECKOUTS,
name: "Manage checkouts"
},
{
__typename: "UserPermission",
code: PermissionEnum.MANAGE_DISCOUNTS,
name: "Manage sales and vouchers."
},
{
__typename: "UserPermission",
code: PermissionEnum.MANAGE_GIFT_CARD,
name: "Manage gift cards."
},
{
__typename: "UserPermission",
code: PermissionEnum.MANAGE_GIFT_CARD,
name: "Manage gift cards."
},
{
__typename: "UserPermission",
code: PermissionEnum.MANAGE_MENUS,
name: "Manage navigation."
},
{
__typename: "UserPermission",
code: PermissionEnum.MANAGE_ORDERS,
name: "Manage orders."
},
{
__typename: "UserPermission",
code: PermissionEnum.MANAGE_ORDERS,
name: "Manage orders."
},
{
__typename: "UserPermission",
code: PermissionEnum.MANAGE_PAGES,
name: "Manage pages."
},
{
__typename: "UserPermission",
code: PermissionEnum.MANAGE_PLUGINS,
name: "Manage plugins"
},
{
__typename: "UserPermission",
code: PermissionEnum.MANAGE_PRODUCTS,
name: "Manage products."
},
{
__typename: "UserPermission",
code: PermissionEnum.MANAGE_SERVICE_ACCOUNTS,
name: "Manage service account"
},
{
__typename: "UserPermission",
code: PermissionEnum.MANAGE_SERVICE_ACCOUNTS,
name: "Manage service account"
},
{
__typename: "UserPermission",
code: PermissionEnum.MANAGE_SETTINGS,
name: "Manage settings."
},
{
__typename: "UserPermission",
code: PermissionEnum.MANAGE_SHIPPING,
name: "Manage shipping."
},
{
__typename: "UserPermission",
code: PermissionEnum.MANAGE_STAFF,
name: "Manage staff."
},
{
__typename: "UserPermission",
code: PermissionEnum.MANAGE_STAFF,
name: "Manage staff."
},
{
__typename: "UserPermission",
code: PermissionEnum.MANAGE_TRANSLATIONS,
name: "Manage translations."
},
{
__typename: "UserPermission",
code: PermissionEnum.MANAGE_USERS,
name: "Manage customers."
},
{
__typename: "UserPermission",
code: PermissionEnum.MANAGE_USERS,
name: "Manage customers."
}
];

View file

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

View file

@ -0,0 +1,16 @@
import createSvgIcon from "@material-ui/icons/utils/createSvgIcon";
import React from "react";
const PermissionGroups = createSvgIcon(
<>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0 0H22.7652L30.4497 7.70511V16.7623C32.4381 16.9409 33.8497 17.6711 34.6541 18.9799C35.5326 20.4092 35.4932 22.2483 35.212 23.9721C34.9657 25.4823 34.4931 27.1133 34.0547 28.6263C33.9837 28.8716 33.9135 29.1138 33.8454 29.3518C33.4479 30.7401 33.1267 31.9645 33.0062 32.9688H40V40H20H18.8281H0V0ZM31.8926 28.7927C31.4864 30.2113 31.089 31.679 30.964 32.9688H27.982C27.8555 31.6772 27.4528 30.2082 27.0405 28.7889C26.9647 28.5282 26.8882 28.2678 26.8119 28.0082L26.811 28.0052L26.8106 28.0036C26.3664 26.4922 25.9303 25.0083 25.699 23.6394C25.4224 22.0022 25.4937 20.8052 25.9593 20.0393C26.3673 19.3682 27.2649 18.75 29.434 18.75C31.6024 18.75 32.5084 19.3679 32.9236 20.0435C33.3956 20.8115 33.4741 22.0095 33.2073 23.6451C32.9824 25.0238 32.5494 26.5201 32.1087 28.0431L32.1086 28.0434C32.0365 28.2926 31.9642 28.5425 31.8926 28.7927ZM25.0899 29.3556C25.4929 30.743 25.8176 31.9661 25.9395 32.9688H18.8281V37.9688H2.03125V2.03125H21.3281V9.14062H28.4184V16.7623C26.4311 16.941 25.0216 17.6715 24.2236 18.9841C23.3534 20.4155 23.4052 22.2556 23.6961 23.9778C23.9494 25.4766 24.4257 27.0953 24.8681 28.5988L24.8681 28.5988C24.9434 28.8548 25.0178 29.1075 25.0899 29.3556ZM23.3594 3.47223L26.9868 7.10938H23.3594V3.47223ZM14.0625 16.4844H4.6875V14.4531H14.0625V16.4844ZM18.5938 21.25H4.76562V19.2188H18.5938V21.25ZM37.9688 35V37.9688H20.8594V35H37.9688ZM10 31.6406C10 32.5467 9.26547 33.2812 8.35938 33.2812C7.45328 33.2812 6.71875 32.5467 6.71875 31.6406C6.71875 30.7345 7.45328 30 8.35938 30C9.26547 30 10 30.7345 10 31.6406ZM12.0312 31.6406C12.0312 33.6685 10.3873 35.3125 8.35938 35.3125C6.33145 35.3125 4.6875 33.6685 4.6875 31.6406C4.6875 29.6127 6.33145 27.9688 8.35938 27.9688C10.3873 27.9688 12.0312 29.6127 12.0312 31.6406Z"
fill="#06847B"
/>
</>,
"PermissionGroups"
);
export default PermissionGroups;

View file

@ -45,6 +45,7 @@ import PageSection from "./pages";
import PluginsSection from "./plugins";
import ProductSection from "./products";
import ProductTypesSection from "./productTypes";
import PermissionGroupSection from "./permissionGroups";
import ServiceSection from "./services";
import { serviceSection } from "./services/urls";
import ShippingSection from "./shipping";
@ -217,6 +218,11 @@ const Routes: React.FC = () => {
path="/staff"
component={StaffSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_STAFF]}
path="/permission-groups"
component={PermissionGroupSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_SETTINGS]}
path="/site-settings"

View file

@ -176,6 +176,10 @@ export const sectionNames = defineMessages({
defaultMessage: "Pages",
description: "pages section name"
},
permissionGroups: {
defaultMessage: "Permission Groups",
description: "permission groups section name"
},
plugins: {
defaultMessage: "Plugins",
description: "plugins section name"

View file

@ -192,7 +192,7 @@ export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => {
() => data.order.availableShippingMethods,
[]
)}
userPermissions={maybe(() => user.permissions, [])}
userPermissions={user?.userPermissions || []}
onOrderCancel={() => openModal("cancel")}
onOrderFulfill={() => openModal("fulfill")}
onFulfillmentCancel={fulfillmentId =>
@ -450,7 +450,7 @@ export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => {
onProfileView={() =>
navigate(customerUrl(order.user.id))
}
userPermissions={maybe(() => user.permissions, [])}
userPermissions={user?.userPermissions || []}
/>
<OrderDraftCancelDialog
confirmButtonState={orderDraftCancel.opts.status}

View file

@ -0,0 +1,41 @@
import { storiesOf } from "@storybook/react";
import React from "react";
import AssignMembersDialog, {
AssignMembersDialogProps
} from "@saleor/permissionGroups/components/AssignMembersDialog";
import Decorator from "@saleor/storybook/Decorator";
import { users } from "../../fixtures";
const props: AssignMembersDialogProps = {
confirmButtonState: "default",
disabled: false,
hasMore: true,
initialSearch: "",
loading: false,
onClose: () => undefined,
onFetchMore: () => undefined,
onSearchChange: () => undefined,
onSubmit: () => undefined,
open: true,
staffMembers: users
};
storiesOf(
"Views / Permission Groups / Permission Group User Assignment",
module
)
.addDecorator(Decorator)
.add("submitting loading", () => (
<AssignMembersDialog
{...props}
confirmButtonState={"loading"}
loading={false}
disabled={true}
staffMembers={[]}
/>
))
.add("search loading", () => (
<AssignMembersDialog {...props} loading={true} staffMembers={[]} />
))
.add("default", () => <AssignMembersDialog {...props} />);

View file

@ -0,0 +1,302 @@
import { FormattedMessage, useIntl } from "react-intl";
import InfiniteScroll from "react-infinite-scroller";
import classNames from "classnames";
import ConfirmButton, {
ConfirmButtonTransitionState
} from "@saleor/components/ConfirmButton";
import { buttonMessages } from "@saleor/intl";
import { makeStyles } from "@material-ui/core/styles";
import { getUserName, getUserInitials } from "@saleor/misc";
import { DialogProps, FetchMoreProps, SearchPageProps } from "@saleor/types";
import useElementScroll from "@saleor/hooks/useElementScroll";
import Button from "@material-ui/core/Button";
import CircularProgress from "@material-ui/core/CircularProgress";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import React from "react";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import TextField from "@material-ui/core/TextField";
import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { SearchStaffMembers_search_edges_node } from "@saleor/searches/types/SearchStaffMembers";
import { Checkbox, Typography } from "@material-ui/core";
import Skeleton from "@saleor/components/Skeleton";
import CardSpacer from "@saleor/components/CardSpacer";
const useStyles = makeStyles(
theme => ({
avatar: {
alignItems: "center",
borderRadius: "100%",
display: "grid",
float: "left",
height: 32,
justifyContent: "center",
overflow: "hidden",
width: 32
},
avatarCell: {
padding: 0,
width: 32
},
avatarDefault: {
"& p": {
color: "#fff",
lineHeight: "47px"
},
background: theme.palette.primary.main,
height: 32,
textAlign: "center",
width: 32
},
avatarImage: {
pointerEvents: "none",
width: "100%"
},
checkboxCell: {
"&&:not(first-child)": {
paddingLeft: 0,
paddingRight: 0,
width: 48
}
},
colActions: {
textAlign: "right"
},
colName: {
paddingLeft: theme.spacing()
},
dropShadow: {
boxShadow: `0px -5px 10px 0px ${theme.palette.divider}`
},
inputContainer: {
overflowY: "visible"
},
loadMoreLoaderContainer: {
alignItems: "center",
display: "flex",
gridColumnEnd: "span 3",
height: theme.spacing(4),
justifyContent: "center"
},
overflow: {
overflowY: "visible"
},
scrollArea: {
maxHeight: 400,
overflowY: "scroll",
paddingTop: 0
},
statusText: {
color: "#9E9D9D"
},
wideCell: {
width: "80%"
}
}),
{ name: "AssignStaffMembersDialog" }
);
export interface AssignMembersDialogProps
extends DialogProps,
FetchMoreProps,
SearchPageProps {
confirmButtonState: ConfirmButtonTransitionState;
disabled: boolean;
staffMembers: SearchStaffMembers_search_edges_node[];
hasMore: boolean;
onFetchMore: () => void;
onSubmit: (data: SearchStaffMembers_search_edges_node[]) => void;
}
function handleStaffMemberAssign(
member: SearchStaffMembers_search_edges_node,
isSelected: boolean,
selectedMembers: SearchStaffMembers_search_edges_node[],
setSelectedMembers: (data: SearchStaffMembers_search_edges_node[]) => void
) {
if (isSelected) {
setSelectedMembers(
selectedMembers.filter(selectedMember => selectedMember.id !== member.id)
);
} else {
setSelectedMembers([...selectedMembers, member]);
}
}
const AssignMembersDialog: React.FC<AssignMembersDialogProps> = ({
confirmButtonState,
disabled,
loading,
onClose,
onFetchMore,
hasMore,
onSearchChange,
onSubmit,
open,
staffMembers
}) => {
const intl = useIntl();
const classes = useStyles({});
const [query, onQueryChange] = useSearchQuery(onSearchChange);
const [selectedMembers, setSelectedMembers] = React.useState<
SearchStaffMembers_search_edges_node[]
>([]);
const anchor = React.useRef<HTMLDivElement>();
const scrollPosition = useElementScroll(anchor);
const dropShadow =
anchor.current && scrollPosition
? scrollPosition.y + anchor.current.clientHeight <
anchor.current.scrollHeight
: false;
return (
<Dialog onClose={onClose} open={open} maxWidth="sm" fullWidth>
<DialogTitle>
<FormattedMessage
defaultMessage="Assign Staff Members"
description="dialog header"
/>
</DialogTitle>
<DialogContent className={classes.inputContainer}>
<TextField
name="query"
value={query}
onChange={onQueryChange}
label={intl.formatMessage({
defaultMessage: "Search Staff Members"
})}
placeholder={intl.formatMessage({
defaultMessage: "Search by name, email, etc..."
})}
fullWidth
InputProps={{
autoComplete: "off",
endAdornment: loading && <CircularProgress size={16} />
}}
disabled={disabled}
/>
</DialogContent>
<DialogContent className={classes.scrollArea}>
<InfiniteScroll
pageStart={0}
loadMore={onFetchMore}
hasMore={hasMore}
useWindow={false}
threshold={100}
key="infinite-scroll"
>
<ResponsiveTable>
<TableBody>
{staffMembers &&
staffMembers.map(member => {
const isSelected = selectedMembers.some(
selectedMember => selectedMember.id === member.id
);
return (
<TableRow key={member.id}>
<TableCell
padding="checkbox"
className={classes.checkboxCell}
>
<Checkbox
color="primary"
checked={isSelected}
onChange={() =>
handleStaffMemberAssign(
member,
isSelected,
selectedMembers,
setSelectedMembers
)
}
/>
</TableCell>
<TableCell className={classes.avatarCell}>
<div className={classes.avatar}>
{!!member?.avatar?.url ? (
<img
className={classes.avatarImage}
src={member.avatar.url}
/>
) : (
<div className={classes.avatarDefault}>
<Typography>{getUserInitials(member)}</Typography>
</div>
)}
</div>
</TableCell>
<TableCell className={classes.colName}>
<Typography>
{getUserName(member) || <Skeleton />}
</Typography>
<Typography
variant={"caption"}
className={classes.statusText}
>
{!!member ? (
member.isActive ? (
intl.formatMessage({
defaultMessage: "Active",
description: "staff member status"
})
) : (
intl.formatMessage({
defaultMessage: "Inactive",
description: "staff member status"
})
)
) : (
<Skeleton />
)}
</Typography>
</TableCell>
</TableRow>
);
})}
</TableBody>
</ResponsiveTable>
{loading && (
<>
{staffMembers?.length > 0 && <CardSpacer />}
<div className={classes.loadMoreLoaderContainer}>
<CircularProgress size={24} />
</div>
</>
)}
</InfiniteScroll>
</DialogContent>
<DialogActions
className={classNames({
[classes.dropShadow]: dropShadow
})}
>
<Button onClick={onClose}>
<FormattedMessage {...buttonMessages.back} />
</Button>
<ConfirmButton
color="primary"
variant="contained"
type="submit"
transitionState={confirmButtonState}
onClick={() => {
onSubmit(selectedMembers);
}}
>
<FormattedMessage defaultMessage="Assign" description="button" />
</ConfirmButton>
</DialogActions>
</Dialog>
);
};
AssignMembersDialog.displayName = "AssignMembersDialog";
export default AssignMembersDialog;

View file

@ -0,0 +1,2 @@
export { default } from "./AssignMembersDialog";
export * from "./AssignMembersDialog";

View file

@ -0,0 +1,20 @@
import { storiesOf } from "@storybook/react";
import React from "react";
import Decorator from "@saleor/storybook/Decorator";
import MembersErrorDialog, {
MembersErrorDialogProps
} from "./MembersErrorDialog";
const props: MembersErrorDialogProps = {
confirmButtonState: "default",
onClose: () => undefined,
onConfirm: () => undefined,
open: true
};
storiesOf(
"Views / Permission Groups / Permission Group Unassign Error Modal",
module
)
.addDecorator(Decorator)
.add("Unassign member", () => <MembersErrorDialog {...props} />);

View file

@ -0,0 +1,45 @@
import DialogContentText from "@material-ui/core/DialogContentText";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import ActionDialog from "@saleor/components/ActionDialog";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
export interface MembersErrorDialogProps {
confirmButtonState: ConfirmButtonTransitionState;
open: boolean;
onConfirm: () => void;
onClose: () => void;
}
const MembersErrorDialog: React.FC<MembersErrorDialogProps> = ({
confirmButtonState,
onClose,
onConfirm,
open
}) => {
const intl = useIntl();
return (
<ActionDialog
open={open}
confirmButtonState={confirmButtonState}
onClose={onClose}
onConfirm={onConfirm}
title={intl.formatMessage({
defaultMessage: "Unassign users",
description: "dialog title"
})}
variant="default"
>
<DialogContentText>
<FormattedMessage
defaultMessage="You are not able to modify this group members. Solve this problem to continue with request."
description="dialog content"
/>
</DialogContentText>
</ActionDialog>
);
};
MembersErrorDialog.displayName = "MembersErrorDialog";
export default MembersErrorDialog;

View file

@ -0,0 +1,2 @@
export { default } from "./MembersErrorDialog";
export * from "./MembersErrorDialog";

View file

@ -0,0 +1,31 @@
import { storiesOf } from "@storybook/react";
import React from "react";
import { permissions } from "@saleor/fixtures";
import PermissionGroupCreatePage, {
PermissionGroupCreatePageProps
} from "@saleor/permissionGroups/components/PermissionGroupCreatePage";
import Decorator from "@saleor/storybook/Decorator";
import { errorsOfPermissionGroupCreate } from "@saleor/permissionGroups/fixtures";
const props: PermissionGroupCreatePageProps = {
disabled: false,
errors: [],
onBack: () => undefined,
onSubmit: () => undefined,
permissions,
saveButtonBarState: undefined
};
storiesOf("Views / Permission Groups / Permission Group Create", module)
.addDecorator(Decorator)
.add("default", () => <PermissionGroupCreatePage {...props} />)
.add("loading", () => (
<PermissionGroupCreatePage {...props} disabled={true} />
))
.add("errors", () => (
<PermissionGroupCreatePage
{...props}
errors={errorsOfPermissionGroupCreate}
/>
));

View file

@ -0,0 +1,107 @@
import React from "react";
import { useIntl } from "react-intl";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import { PermissionEnum } from "@saleor/types/globalTypes";
import AccountPermissions from "@saleor/components/AccountPermissions";
import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form";
import Grid from "@saleor/components/Grid";
import SaveButtonBar from "@saleor/components/SaveButtonBar";
import AppHeader from "@saleor/components/AppHeader";
import { sectionNames } from "@saleor/intl";
import { PermissionGroupErrorFragment } from "@saleor/permissionGroups/types/PermissionGroupErrorFragment";
import { getFormErrors } from "@saleor/utils/errors";
import getPermissionGroupErrorMessage from "@saleor/utils/errors/permissionGroups";
import PermissionGroupInfo from "../PermissionGroupInfo";
import { PermissionData } from "../PermissionGroupDetailsPage";
export interface PermissionGroupCreatePageFormData {
name: string;
hasFullAccess: boolean;
isActive: boolean;
permissions: PermissionEnum[];
}
const initialForm: PermissionGroupCreatePageFormData = {
hasFullAccess: false,
isActive: false,
name: "",
permissions: []
};
export interface PermissionGroupCreatePageProps {
disabled: boolean;
errors: PermissionGroupErrorFragment[];
permissions: PermissionData[];
saveButtonBarState: ConfirmButtonTransitionState;
onBack: () => void;
onSubmit(data: PermissionGroupCreatePageFormData);
}
const PermissionGroupCreatePage: React.FC<PermissionGroupCreatePageProps> = ({
disabled,
permissions,
onBack,
onSubmit,
saveButtonBarState,
errors
}) => {
const intl = useIntl();
const formErrors = getFormErrors(["addPermissions"], errors || []);
const permissionsError = getPermissionGroupErrorMessage(
formErrors.addPermissions,
intl
);
return (
<Form initial={initialForm} onSubmit={onSubmit} confirmLeave>
{({ data, change, submit, hasChanged }) => (
<Container>
<AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.permissionGroups)}
</AppHeader>
<Grid>
<div>
<PermissionGroupInfo
data={data}
errors={errors}
onChange={change}
disabled={disabled}
/>
</div>
<div>
<AccountPermissions
permissionsExceeded={false}
data={data}
errorMessage={permissionsError}
disabled={disabled}
permissions={permissions}
onChange={change}
fullAccessLabel={intl.formatMessage({
defaultMessage: "Group has full access to the store",
description: "checkbox label"
})}
description={intl.formatMessage({
defaultMessage:
"Expand or restrict group's permissions to access certain part of saleor system.",
description: "card description"
})}
/>
</div>
</Grid>
<div>
<SaveButtonBar
onCancel={onBack}
onSave={submit}
state={saveButtonBarState}
disabled={disabled || !hasChanged}
/>
</div>
</Container>
)}
</Form>
);
};
PermissionGroupCreatePage.displayName = "PermissionGroupCreatePage";
export default PermissionGroupCreatePage;

View file

@ -0,0 +1,2 @@
export { default } from "./PermissionGroupCreatePage";
export * from "./PermissionGroupCreatePage";

View file

@ -0,0 +1,19 @@
import { storiesOf } from "@storybook/react";
import React from "react";
import PermissionGroupDeleteDialog, {
PermissionDeleteDialogProps
} from "@saleor/permissionGroups/components/PermissionGroupDeleteDialog";
import Decorator from "@saleor/storybook/Decorator";
const props: PermissionDeleteDialogProps = {
confirmButtonState: "default",
name: "Full Access",
onClose: () => undefined,
onConfirm: () => undefined,
open: true
};
storiesOf("Views / Permission Groups / Permission Group Delete", module)
.addDecorator(Decorator)
.add("remove single", () => <PermissionGroupDeleteDialog {...props} />);

View file

@ -0,0 +1,50 @@
import DialogContentText from "@material-ui/core/DialogContentText";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import ActionDialog from "@saleor/components/ActionDialog";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
export interface PermissionDeleteDialogProps {
confirmButtonState: ConfirmButtonTransitionState;
open: boolean;
name: string;
onConfirm: () => void;
onClose: () => void;
}
const PermissionGroupDeleteDialog: React.FC<PermissionDeleteDialogProps> = ({
confirmButtonState,
name,
onClose,
onConfirm,
open
}) => {
const intl = useIntl();
return (
<ActionDialog
open={open}
confirmButtonState={confirmButtonState}
onClose={onClose}
onConfirm={onConfirm}
title={intl.formatMessage({
defaultMessage: "Delete permission group",
description: "dialog title"
})}
variant="delete"
>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to delete {name}?"
description="dialog content"
values={{
name: <strong>{name}</strong>
}}
/>
</DialogContentText>
</ActionDialog>
);
};
PermissionGroupDeleteDialog.displayName = "PermissionGroupDeleteDialog";
export default PermissionGroupDeleteDialog;

View file

@ -0,0 +1,2 @@
export { default } from "./PermissionGroupDeleteDialog";
export * from "./PermissionGroupDeleteDialog";

View file

@ -0,0 +1,53 @@
import React from "react";
import { permissions } from "@saleor/fixtures";
import PermissionGroupDetailsPage, {
PermissionGroupDetailsPageProps
} from "@saleor/permissionGroups/components/PermissionGroupDetailsPage";
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import {
emptyPermissionGroup,
permissionGroup,
users
} from "@saleor/permissionGroups/fixtures";
const props: PermissionGroupDetailsPageProps = {
disabled: false,
errors: [],
isChecked: () => false,
members: users,
membersModified: false,
onAssign: () => undefined,
onBack: () => undefined,
onSort: () => undefined,
onSubmit: () => undefined,
onUnassign: () => undefined,
permissionGroup,
permissions,
permissionsExceeded: false,
saveButtonBarState: undefined,
selected: 0,
sort: null,
toggle: () => undefined,
toggleAll: () => undefined,
toolbar: null
};
storiesOf("Views / Permission Groups / Permission Group Details", module)
.addDecorator(Decorator)
.add("default", () => <PermissionGroupDetailsPage {...props} />)
.add("no members", () => (
<PermissionGroupDetailsPage
{...props}
members={[]}
permissionGroup={emptyPermissionGroup}
/>
))
.add("loading", () => (
<PermissionGroupDetailsPage
{...props}
disabled={true}
permissionGroup={undefined}
permissions={undefined}
/>
));

View file

@ -0,0 +1,149 @@
import React from "react";
import { useIntl } from "react-intl";
import FormSpacer from "@saleor/components/FormSpacer";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import { PermissionEnum } from "@saleor/types/globalTypes";
import { ShopInfo_shop_permissions } from "@saleor/components/Shop/types/ShopInfo";
import { ListActions, SortPage } from "@saleor/types";
import AccountPermissions from "@saleor/components/AccountPermissions";
import AppHeader from "@saleor/components/AppHeader";
import PageHeader from "@saleor/components/PageHeader";
import { sectionNames } from "@saleor/intl";
import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form";
import Grid from "@saleor/components/Grid";
import SaveButtonBar from "@saleor/components/SaveButtonBar";
import {
isGroupFullAccess,
extractPermissionCodes
} from "@saleor/permissionGroups/utils";
import { MembersListUrlSortField } from "@saleor/permissionGroups/urls";
import { PermissionGroupErrorFragment } from "@saleor/permissionGroups/types/PermissionGroupErrorFragment";
import { getFormErrors } from "@saleor/utils/errors";
import getPermissionGroupErrorMessage from "@saleor/utils/errors/permissionGroups";
import PermissionGroupInfo from "../PermissionGroupInfo";
import {
PermissionGroupDetails_permissionGroup,
PermissionGroupDetails_permissionGroup_users
} from "../../types/PermissionGroupDetails";
import PermissionGroupMemberList from "../PermissionGroupMemberList";
export interface PermissionGroupDetailsPageFormData {
name: string;
hasFullAccess: boolean;
isActive: boolean;
permissions: PermissionEnum[];
users: PermissionGroupDetails_permissionGroup_users[];
}
export interface PermissionData extends ShopInfo_shop_permissions {
lastSource?: boolean;
disabled?: boolean;
}
export interface PermissionGroupDetailsPageProps
extends ListActions,
SortPage<MembersListUrlSortField> {
disabled: boolean;
errors: PermissionGroupErrorFragment[];
members: PermissionGroupDetails_permissionGroup_users[];
membersModified: boolean;
permissionGroup: PermissionGroupDetails_permissionGroup;
permissions: PermissionData[];
permissionsExceeded: boolean;
saveButtonBarState: ConfirmButtonTransitionState;
onAssign: () => void;
onBack: () => void;
onUnassign: (ids: string[]) => void;
onSubmit(data: PermissionGroupDetailsPageFormData);
}
const PermissionGroupDetailsPage: React.FC<PermissionGroupDetailsPageProps> = ({
disabled,
errors,
members,
membersModified,
onBack,
onSubmit,
permissionGroup,
permissions,
permissionsExceeded,
saveButtonBarState,
...listProps
}) => {
const intl = useIntl();
const initialForm: PermissionGroupDetailsPageFormData = {
hasFullAccess: isGroupFullAccess(permissionGroup, permissions),
isActive: false,
name: permissionGroup?.name || "",
permissions: extractPermissionCodes(permissionGroup),
users: members
};
const formErrors = getFormErrors(["addPermissions"], errors);
const permissionsError = getPermissionGroupErrorMessage(
formErrors.addPermissions,
intl
);
return (
<Form initial={initialForm} onSubmit={onSubmit} confirmLeave>
{({ data, change, submit, hasChanged }) => (
<Container>
<AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.permissionGroups)}
</AppHeader>
<PageHeader title={permissionGroup?.name} />
<Grid>
<div>
<PermissionGroupInfo
data={data}
disabled={disabled}
errors={errors}
onChange={change}
/>
<FormSpacer />
<PermissionGroupMemberList
disabled={disabled}
{...listProps}
users={data?.users || []}
/>
</div>
<div>
<AccountPermissions
permissionsExceeded={permissionsExceeded}
data={data}
disabled={disabled}
permissions={permissions}
onChange={change}
errorMessage={permissionsError}
fullAccessLabel={intl.formatMessage({
defaultMessage: "Group has full access to the store",
description: "checkbox label"
})}
description={intl.formatMessage({
defaultMessage:
"Expand or restrict group's permissions to access certain part of saleor system.",
description: "card description"
})}
/>
</div>
</Grid>
<div>
<SaveButtonBar
onCancel={onBack}
onSave={submit}
state={saveButtonBarState}
disabled={disabled || !(hasChanged || membersModified)}
/>
</div>
</Container>
)}
</Form>
);
};
PermissionGroupDetailsPage.displayName = "PermissionGroupDetailsPage";
export default PermissionGroupDetailsPage;

View file

@ -0,0 +1,2 @@
export { default } from "./PermissionGroupDetailsPage";
export * from "./PermissionGroupDetailsPage";

View file

@ -0,0 +1,58 @@
import React from "react";
import { useIntl } from "react-intl";
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import TextField from "@material-ui/core/TextField";
import CardTitle from "@saleor/components/CardTitle";
import { FormChange } from "@saleor/hooks/useForm";
import { commonMessages } from "@saleor/intl";
import { PermissionGroupErrorFragment } from "@saleor/permissionGroups/types/PermissionGroupErrorFragment";
import { getFieldError, getFormErrors } from "@saleor/utils/errors";
import getPermissionGroupErrorMessage from "@saleor/utils/errors/permissionGroups";
export interface PermissionGroupInfoProps {
disabled: boolean;
errors: PermissionGroupErrorFragment[];
onChange: FormChange;
data: {
name: string;
};
}
const PermissionGroupInfo: React.FC<PermissionGroupInfoProps> = ({
disabled,
onChange,
data,
errors
}) => {
const intl = useIntl();
const formErrors = getFormErrors(["name"], errors);
return (
<Card>
<CardTitle
title={intl.formatMessage(commonMessages.generalInformations)}
></CardTitle>
<CardContent>
<TextField
name="name"
label={intl.formatMessage({
defaultMessage: "Group name",
description: "text field label"
})}
value={data.name}
onChange={onChange}
disabled={disabled}
error={!!getFieldError(errors, "name")}
helperText={getPermissionGroupErrorMessage(formErrors.name, intl)}
fullWidth
/>
</CardContent>
</Card>
);
};
PermissionGroupInfo.displayName = "PermissionGroupInfo";
export default PermissionGroupInfo;

View file

@ -0,0 +1,2 @@
export { default } from "./PermissionGroupInfo";
export * from "./PermissionGroupInfo";

View file

@ -0,0 +1,182 @@
import { makeStyles } from "@material-ui/core/styles";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableFooter from "@material-ui/core/TableFooter";
import TableRow from "@material-ui/core/TableRow";
import DeleteIcon from "@material-ui/icons/Delete";
import EditIcon from "@material-ui/icons/Edit";
import IconButton from "@material-ui/core/IconButton";
import { TableHead } from "@material-ui/core";
import React from "react";
import { FormattedMessage } from "react-intl";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import Skeleton from "@saleor/components/Skeleton";
import TablePagination from "@saleor/components/TablePagination";
import { getArrowDirection } from "@saleor/utils/sort";
import TableCellHeader from "@saleor/components/TableCellHeader";
import { ListProps, SortPage } from "@saleor/types";
import { maybe, renderCollection, stopPropagation } from "@saleor/misc";
import { PermissionGroupList_permissionGroups_edges_node } from "@saleor/permissionGroups/types/PermissionGroupList";
import { PermissionGroupListUrlSortField } from "@saleor/permissionGroups/urls";
const useStyles = makeStyles(
theme => ({
[theme.breakpoints.up("lg")]: {
colActions: {
width: 180
},
colMembers: {
width: 180
},
colName: {
width: "auto"
}
},
colActions: {
paddingRight: theme.spacing(),
textAlign: "right"
},
colActionsHeader: {
textAlign: "right"
},
colMembers: {
textAlign: "right"
},
colName: {
paddingLeft: 0
},
link: {
cursor: "pointer"
}
}),
{ name: "PermissionGroupList" }
);
const numberOfColumns = 3;
interface PermissionGroupListProps
extends ListProps,
SortPage<PermissionGroupListUrlSortField> {
permissionGroups: PermissionGroupList_permissionGroups_edges_node[];
onDelete: (id: string) => void;
}
const PermissionGroupList: React.FC<PermissionGroupListProps> = props => {
const {
disabled,
permissionGroups,
pageInfo,
onDelete,
onNextPage,
onPreviousPage,
onRowClick,
onSort,
sort
} = props;
const classes = useStyles(props);
return (
<ResponsiveTable>
<TableHead>
<TableRow>
<TableCellHeader
direction={
sort.sort === PermissionGroupListUrlSortField.name
? getArrowDirection(sort.asc)
: undefined
}
arrowPosition="right"
onClick={() => onSort(PermissionGroupListUrlSortField.name)}
className={classes.colName}
>
<FormattedMessage
defaultMessage="Permission Group Name"
description="permission group name"
/>
</TableCellHeader>
<TableCellHeader className={classes.colMembers} textAlign="right">
<FormattedMessage defaultMessage="Members" />
</TableCellHeader>
<TableCell className={classes.colActionsHeader}>
<FormattedMessage defaultMessage="Actions" />
</TableCell>
</TableRow>
</TableHead>
<TableFooter>
<TableRow>
<TablePagination
colSpan={numberOfColumns}
hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false}
onNextPage={onNextPage}
hasPreviousPage={
pageInfo && !disabled ? pageInfo.hasPreviousPage : false
}
onPreviousPage={onPreviousPage}
/>
</TableRow>
</TableFooter>
<TableBody>
{renderCollection(
permissionGroups,
permissionGroup => (
<TableRow
className={!!permissionGroup ? classes.link : undefined}
hover={!!permissionGroup}
key={permissionGroup ? permissionGroup.id : "skeleton"}
onClick={
permissionGroup ? onRowClick(permissionGroup.id) : undefined
}
data-tc="id"
data-tc-id={maybe(() => permissionGroup.id)}
>
<TableCell className={classes.colName}>
{permissionGroup ? (
<span data-tc="name">{permissionGroup.name}</span>
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colMembers}>
{permissionGroup ? (
<span data-tc="members">{permissionGroup.users.length}</span>
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colActions}>
{permissionGroup ? (
<>
<IconButton
color="primary"
onClick={stopPropagation(() =>
onDelete(permissionGroup.id)
)}
>
<DeleteIcon />
</IconButton>
<IconButton color="primary">
<EditIcon />
</IconButton>
</>
) : (
<Skeleton />
)}
</TableCell>
</TableRow>
),
() => (
<TableRow>
<TableCell colSpan={numberOfColumns}>
<FormattedMessage defaultMessage="No permission groups found" />
</TableCell>
</TableRow>
)
)}
</TableBody>
</ResponsiveTable>
);
};
PermissionGroupList.displayName = "PermissionGroupList";
export default PermissionGroupList;

View file

@ -0,0 +1,2 @@
export { default } from "./PermissionGroupList";
export * from "./PermissionGroupList";

View file

@ -0,0 +1,42 @@
import { storiesOf } from "@storybook/react";
import React from "react";
import {
listActionsProps,
pageListProps,
sortPageProps
} from "@saleor/fixtures";
import PermissionGroupListPage, {
PermissionGroupListPageProps
} from "@saleor/permissionGroups/components/PermissionGroupListPage";
import Decorator from "@saleor/storybook/Decorator";
import { PermissionGroupListUrlSortField } from "@saleor/permissionGroups/urls";
import { permissionGroups } from "@saleor/permissionGroups/fixtures";
const props: PermissionGroupListPageProps = {
permissionGroups,
...listActionsProps,
...pageListProps.default,
...sortPageProps,
disabled: false,
onBack: () => undefined,
onDelete: () => undefined,
sort: {
...sortPageProps.sort,
sort: PermissionGroupListUrlSortField.name
}
};
storiesOf("Views / Permission Groups / Permission Group List", module)
.addDecorator(Decorator)
.add("default", () => <PermissionGroupListPage {...props} />)
.add("loading", () => (
<PermissionGroupListPage
{...props}
permissionGroups={undefined}
disabled={true}
/>
))
.add("no data", () => (
<PermissionGroupListPage {...props} permissionGroups={[]} disabled={true} />
));

View file

@ -0,0 +1,51 @@
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import AppHeader from "@saleor/components/AppHeader";
import Container from "@saleor/components/Container";
import PageHeader from "@saleor/components/PageHeader";
import { sectionNames } from "@saleor/intl";
import { PermissionGroupListUrlSortField } from "../../urls";
import { PageListProps, SortPage } from "../../../types";
import { PermissionGroupList_permissionGroups_edges_node } from "../../types/PermissionGroupList";
import PermissionGroupList from "../PermissionGroupList";
export interface PermissionGroupListPageProps
extends PageListProps,
SortPage<PermissionGroupListUrlSortField> {
permissionGroups: PermissionGroupList_permissionGroups_edges_node[];
onBack: () => void;
onDelete: (id: string) => void;
onRowClick: (id: string) => () => void;
}
const PermissionGroupListPage: React.FC<PermissionGroupListPageProps> = ({
onAdd,
onBack,
...listProps
}) => {
const intl = useIntl();
return (
<Container>
<AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.configuration)}
</AppHeader>
<PageHeader title={intl.formatMessage(sectionNames.permissionGroups)}>
<Button color="primary" variant="contained" onClick={onAdd}>
<FormattedMessage
defaultMessage="create permission group"
description="button"
/>
</Button>
</PageHeader>
<Card>
<PermissionGroupList {...listProps} />
</Card>
</Container>
);
};
PermissionGroupListPage.displayName = "PermissionGroupListPage";
export default PermissionGroupListPage;

View file

@ -0,0 +1,2 @@
export { default } from "./PermissionGroupListPage";
export * from "./PermissionGroupListPage";

View file

@ -0,0 +1,288 @@
import Card from "@material-ui/core/Card";
import { makeStyles } from "@material-ui/core/styles";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@saleor/components/TableHead";
import TableRow from "@material-ui/core/TableRow";
import Typography from "@material-ui/core/Typography";
import classNames from "classnames";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import DeleteIcon from "@material-ui/icons/Delete";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import Skeleton from "@saleor/components/Skeleton";
import {
getUserInitials,
getUserName,
stopPropagation,
renderCollection
} from "@saleor/misc";
import { ListActions, SortPage } from "@saleor/types";
import TableCellHeader from "@saleor/components/TableCellHeader";
import Checkbox from "@saleor/components/Checkbox";
import { Button, IconButton } from "@material-ui/core";
import CardTitle from "@saleor/components/CardTitle";
import { PermissionGroupDetails_permissionGroup_users } from "@saleor/permissionGroups/types/PermissionGroupDetails";
import { MembersListUrlSortField } from "@saleor/permissionGroups/urls";
import { getArrowDirection } from "@saleor/utils/sort";
import { sortMembers } from "@saleor/permissionGroups/sort";
const useStyles = makeStyles(
theme => ({
[theme.breakpoints.up("lg")]: {
colActions: {
width: 120
},
colEmail: {
width: 300
},
colName: {
width: "auto"
}
},
avatar: {
alignItems: "center",
borderRadius: "100%",
display: "grid",
float: "left",
height: 47,
justifyContent: "center",
marginRight: theme.spacing(1),
overflow: "hidden",
width: 47
},
avatarDefault: {
"& p": {
color: "#fff",
lineHeight: "47px"
},
background: theme.palette.primary.main,
height: 47,
textAlign: "center",
width: 47
},
avatarImage: {
pointerEvents: "none",
width: "100%"
},
colActions: {
paddingRight: theme.spacing(),
textAlign: "right"
},
helperText: {
marginBottom: theme.spacing(3),
marginTop: theme.spacing(3),
textAlign: "center"
},
statusText: {
color: "#9E9D9D"
},
tableRow: {}
}),
{ name: "PermissionGroup" }
);
const numberOfColumns = 4;
interface PermissionGroupProps
extends ListActions,
SortPage<MembersListUrlSortField> {
users: PermissionGroupDetails_permissionGroup_users[];
disabled: boolean;
onUnassign: (ida: string[]) => void;
onAssign: () => void;
}
const PermissionGroupMemberList: React.FC<PermissionGroupProps> = props => {
const {
disabled,
users,
onUnassign,
onAssign,
onSort,
toggle,
toolbar,
isChecked,
selected,
toggleAll,
sort
} = props;
const classes = useStyles(props);
const intl = useIntl();
const members = users?.sort(sortMembers(sort?.sort, sort?.asc));
return (
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Group members",
description: "header"
})}
toolbar={
<Button
color={disabled ? "secondary" : "primary"}
onClick={onAssign}
disabled={disabled}
>
<FormattedMessage
defaultMessage="Assign members"
description="button"
/>
</Button>
}
/>
{members?.length === 0 ? (
<div className={classNames(classes.helperText)}>
<Typography color="textSecondary">
<FormattedMessage
defaultMessage="You havent assigned any member to this permission group yet."
description="empty list message"
/>
</Typography>
<Typography color="textSecondary">
<FormattedMessage
defaultMessage="Please use Assign Members button to do so."
description="empty list message"
/>
</Typography>
</div>
) : (
<ResponsiveTable>
<TableHead
colSpan={numberOfColumns}
selected={selected}
disabled={disabled}
items={members}
toggleAll={toggleAll}
toolbar={toolbar}
>
<TableCellHeader
className={classes.colName}
arrowPosition="right"
onClick={() => onSort(MembersListUrlSortField.name)}
direction={
sort?.sort === MembersListUrlSortField.name
? getArrowDirection(sort.asc)
: undefined
}
>
<FormattedMessage
defaultMessage="Name"
description="staff member full name"
/>
</TableCellHeader>
<TableCellHeader
className={classes.colEmail}
arrowPosition="right"
onClick={() => onSort(MembersListUrlSortField.email)}
direction={
sort?.sort === MembersListUrlSortField.email
? getArrowDirection(sort.asc)
: undefined
}
>
<FormattedMessage defaultMessage="Email Address" />
</TableCellHeader>
<TableCellHeader textAlign="right">
<FormattedMessage defaultMessage="Actions" />
</TableCellHeader>
</TableHead>
<TableBody>
{renderCollection(
members,
user => {
const isSelected = user ? isChecked(user.id) : false;
return (
<TableRow
className={classNames({
[classes.tableRow]: !!user
})}
hover={!!user}
selected={isSelected}
key={user ? user.id : "skeleton"}
>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
disabled={disabled}
disableClickPropagation
onChange={() => toggle(user.id)}
/>
</TableCell>
<TableCell className={classes.colName}>
<div className={classes.avatar}>
{user?.avatar?.url ? (
<img
className={classes.avatarImage}
src={user?.avatar?.url}
/>
) : (
<div className={classes.avatarDefault}>
<Typography>{getUserInitials(user)}</Typography>
</div>
)}
</div>
<Typography>
{getUserName(user) || <Skeleton />}
</Typography>
<Typography
variant={"caption"}
className={classes.statusText}
>
{!user ? (
<Skeleton />
) : user.isActive ? (
intl.formatMessage({
defaultMessage: "Active",
description: "staff member status"
})
) : (
intl.formatMessage({
defaultMessage: "Inactive",
description: "staff member status"
})
)}
</Typography>
</TableCell>
<TableCell className={classes.colEmail}>
{user?.email || <Skeleton />}
</TableCell>
<TableCell className={classes.colActions}>
{user ? (
<>
<IconButton
disabled={disabled}
color="primary"
onClick={stopPropagation(() =>
onUnassign([user.id])
)}
>
<DeleteIcon />
</IconButton>
</>
) : (
<Skeleton />
)}
</TableCell>
</TableRow>
);
},
() => (
<TableRow>
<TableCell colSpan={numberOfColumns}>
<FormattedMessage defaultMessage="No members found" />
</TableCell>
</TableRow>
)
)}
</TableBody>
</ResponsiveTable>
)}
</Card>
);
};
PermissionGroupMemberList.displayName = "PermissionGroupMemberList";
export default PermissionGroupMemberList;

View file

@ -0,0 +1,2 @@
export { default } from "./PermissionGroupMemberList";
export * from "./PermissionGroupMemberList";

View file

@ -0,0 +1,21 @@
import { storiesOf } from "@storybook/react";
import React from "react";
import UnassignMembersDialog, {
UnassignMembersDialogProps
} from "@saleor/permissionGroups/components/UnassignMembersDialog";
import Decorator from "@saleor/storybook/Decorator";
const props: UnassignMembersDialogProps = {
confirmButtonState: "default",
onClose: () => undefined,
onConfirm: () => undefined,
open: true,
quantity: 3
};
storiesOf(
"Views / Permission Groups / Permission Group Unassign Member",
module
)
.addDecorator(Decorator)
.add("Unassign members", () => <UnassignMembersDialog {...props} />);

View file

@ -0,0 +1,51 @@
import DialogContentText from "@material-ui/core/DialogContentText";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import ActionDialog from "@saleor/components/ActionDialog";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
export interface UnassignMembersDialogProps {
confirmButtonState: ConfirmButtonTransitionState;
quantity: number;
open: boolean;
onConfirm: () => void;
onClose: () => void;
}
const UnassignMembersDialog: React.FC<UnassignMembersDialogProps> = ({
confirmButtonState,
quantity,
onClose,
onConfirm,
open
}) => {
const intl = useIntl();
return (
<ActionDialog
open={open}
confirmButtonState={confirmButtonState}
onClose={onClose}
onConfirm={onConfirm}
title={intl.formatMessage({
defaultMessage: "Unassign users",
description: "dialog title"
})}
variant="delete"
>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to unassign {counter,plural,one{this member} other{{displayQuantity} members}}?"
description="dialog content"
values={{
counter: quantity,
displayQuantity: <strong>{quantity}</strong>
}}
/>
</DialogContentText>
</ActionDialog>
);
};
UnassignMembersDialog.displayName = "UnassignMembersDialog";
export default UnassignMembersDialog;

View file

@ -0,0 +1,2 @@
export { default } from "./UnassignMembersDialog";
export * from "./UnassignMembersDialog";

View file

@ -0,0 +1,229 @@
import * as avatarImg from "@assets/images/avatars/avatar1.png";
import { SearchStaffMembers_search_edges_node } from "@saleor/searches/types/SearchStaffMembers";
/* eslint-disable sort-keys */
import {
PermissionEnum,
PermissionGroupErrorCode
} from "@saleor/types/globalTypes";
import { StaffMemberDetails_user_permissionGroups } from "@saleor/staff/types/StaffMemberDetails";
import { PermissionGroupDetails_permissionGroup } from "./types/PermissionGroupDetails";
import { PermissionGroupList_permissionGroups_edges_node } from "./types/PermissionGroupList";
import { PermissionGroupErrorFragment } from "./types/PermissionGroupErrorFragment";
export const permissionGroups: PermissionGroupList_permissionGroups_edges_node[] = [
{
node: {
id: "R3JvdXA6Mg==",
name: "Customer Support",
users: [
{
id: "VXNlcjoyMQ==",
firstName: "",
lastName: "",
__typename: "User" as "User"
}
],
__typename: "Group" as "Group"
},
__typename: "GroupCountableEdge" as "GroupCountableEdge"
},
{
node: {
id: "R3JvdXA6MQ==",
name: "Full Access",
users: [
{
id: "VXNlcjoyMQ==",
firstName: "",
lastName: "",
__typename: "User" as "User"
}
],
__typename: "Group" as "Group"
},
__typename: "GroupCountableEdge" as "GroupCountableEdge"
},
{
node: {
id: "R3JvdXA6NA==",
name: "Management",
users: [],
__typename: "Group" as "Group"
},
__typename: "GroupCountableEdge" as "GroupCountableEdge"
},
{
node: {
id: "R3JvdXA6Mw==",
name: "Editors",
users: [
{
id: "VXNlcjoyMw==",
firstName: "Bryan",
lastName: "Rodgers",
__typename: "User" as "User"
},
{
id: "VXNlcjoyMg==",
firstName: "Joshua",
lastName: "Mitchell",
__typename: "User" as "User"
}
],
__typename: "Group" as "Group"
},
__typename: "GroupCountableEdge" as "GroupCountableEdge"
},
{
node: {
id: "R3JvdXA6NQ==",
name: "Publishers",
users: [],
__typename: "Group" as "Group"
},
__typename: "GroupCountableEdge" as "GroupCountableEdge"
}
].map(edge => edge.node);
export const userPermissionGroups: StaffMemberDetails_user_permissionGroups[] = [
{
id: "R3JvdXA6MQ==",
name: "Full Access",
userCanManage: false,
__typename: "Group"
},
{
id: "R3JvdXA6Mg==",
name: "Customer Support",
userCanManage: true,
__typename: "Group"
}
];
export const emptyPermissionGroup: PermissionGroupDetails_permissionGroup = {
id: "R3JvdXA6Mw==",
name: "Editors",
users: [],
__typename: "Group",
permissions: [
{
code: PermissionEnum.MANAGE_PAGES,
name: "Manage pages.",
__typename: "Permission"
}
]
};
export const errorsOfPermissionGroupCreate: PermissionGroupErrorFragment[] = [
{
field: "name",
code: PermissionGroupErrorCode.UNIQUE,
__typename: "PermissionGroupError"
},
{
field: "permissions",
code: PermissionGroupErrorCode.OUT_OF_SCOPE_PERMISSION,
__typename: "PermissionGroupError"
}
];
export const permissionGroup: PermissionGroupDetails_permissionGroup = {
id: "R3JvdXA6Mw==",
name: "Editors",
users: [
{
id: "VXNlcjoyMg==",
firstName: "Joshua",
lastName: "Mitchell",
__typename: "User",
email: "joshua.mitchell@example.com",
isActive: true,
avatar: null
},
{
id: "VXNlcjoyMw==",
firstName: "Bryan",
lastName: "Rodgers",
__typename: "User",
email: "bryan.rodgers@example.com",
isActive: true,
avatar: null
}
],
__typename: "Group",
permissions: [
{
code: PermissionEnum.MANAGE_PAGES,
name: "Manage pages.",
__typename: "Permission"
}
]
};
export const users: SearchStaffMembers_search_edges_node[] = [
{
node: {
id: "VXNlcjoyMQ==",
email: "admin@example.com",
firstName: "",
lastName: "",
isActive: true,
avatar: {
alt: null,
url: avatarImg,
__typename: "Image" as "Image"
},
__typename: "User" as "User"
},
__typename: "UserCountableEdge" as "UserCountableEdge"
},
{
node: {
id: "VXNlcjoyMw==",
email: "bryan.rodgers@example.com",
firstName: "Bryan",
lastName: "Rodgers",
isActive: true,
avatar: {
alt: null,
url: avatarImg,
__typename: "Image" as "Image"
},
__typename: "User" as "User"
},
__typename: "UserCountableEdge" as "UserCountableEdge"
},
{
node: {
id: "VXNlcjoyMg==",
email: "joshua.mitchell@example.com",
firstName: "Joshua",
lastName: "Mitchell",
isActive: true,
avatar: {
alt: null,
url: avatarImg,
__typename: "Image" as "Image"
},
__typename: "User" as "User"
},
__typename: "UserCountableEdge" as "UserCountableEdge"
},
{
node: {
id: "VXNlcjoyMg==",
email: "joshua.mitchell@example.com",
firstName: "Joshua",
lastName: "Mitchell",
isActive: true,
avatar: {
alt: null,
url: avatarImg,
__typename: "Image" as "Image"
},
__typename: "User" as "User"
},
__typename: "UserCountableEdge" as "UserCountableEdge"
}
].map(edge => edge.node);

View file

@ -0,0 +1,79 @@
import { parse as parseQs } from "qs";
import React from "react";
import { useIntl } from "react-intl";
import { Route, RouteComponentProps, Switch } from "react-router-dom";
import { sectionNames } from "@saleor/intl";
import { asSortParams } from "@saleor/utils/sort";
import { WindowTitle } from "../components/WindowTitle";
import {
permissionGroupListPath,
permissionGroupDetailsPath,
permissionGroupAddPath,
PermissionGroupListUrlQueryParams,
PermissionGroupListUrlSortField,
PermissionGroupDetailsUrlQueryParams,
MembersListUrlSortField
} from "./urls";
import PermissionGroupListComponent from "./views/PermissionGroupList";
import PermissionGroupDetailsComponent from "./views/PermissionGroupDetails";
import PermissionGroupCreate from "./views/PermissionGroupCreate";
const permissionGroupList: React.FC<RouteComponentProps<{}>> = ({
location
}) => {
const qs = parseQs(location.search.substr(1));
const params: PermissionGroupListUrlQueryParams = asSortParams(
qs,
PermissionGroupListUrlSortField
);
return <PermissionGroupListComponent params={params} />;
};
interface PermissionGroupDetailsRouteProps {
id: string;
}
const PermissionGroupDetails: React.FC<RouteComponentProps<
PermissionGroupDetailsRouteProps
>> = ({ match }) => {
const qs = parseQs(location.search.substr(1));
const params: PermissionGroupDetailsUrlQueryParams = asSortParams(
qs,
MembersListUrlSortField
);
return (
<PermissionGroupDetailsComponent
id={decodeURIComponent(match.params.id)}
params={params}
/>
);
};
const Component = () => {
const intl = useIntl();
return (
<>
<WindowTitle title={intl.formatMessage(sectionNames.permissionGroups)} />
<Switch>
<Route
exact
path={permissionGroupListPath}
component={permissionGroupList}
/>
<Route
path={permissionGroupAddPath}
component={PermissionGroupCreate}
/>
<Route
path={permissionGroupDetailsPath(":id")}
component={PermissionGroupDetails}
/>
</Switch>
</>
);
};
export default Component;

View file

@ -0,0 +1,84 @@
import gql from "graphql-tag";
import makeMutation from "@saleor/hooks/makeMutation";
import { permissionGroupDetailsFragment } from "./queries";
import {
PermissionGroupCreate,
PermissionGroupCreateVariables
} from "./types/PermissionGroupCreate";
import {
PermissionGroupDelete,
PermissionGroupDeleteVariables
} from "./types/PermissionGroupDelete";
import {
PermissionGroupUpdate,
PermissionGroupUpdateVariables
} from "./types/PermissionGroupUpdate";
export const permissionGroupErrorFragment = gql`
fragment PermissionGroupErrorFragment on PermissionGroupError {
code
field
}
`;
export const permissionGroupDelete = gql`
${permissionGroupErrorFragment}
mutation PermissionGroupDelete($id: ID!) {
permissionGroupDelete(id: $id) {
errors: permissionGroupErrors {
...PermissionGroupErrorFragment
}
}
}
`;
export const usePermissionGroupDelete = makeMutation<
PermissionGroupDelete,
PermissionGroupDeleteVariables
>(permissionGroupDelete);
export const permissionGroupCreate = gql`
${permissionGroupDetailsFragment}
${permissionGroupErrorFragment}
mutation PermissionGroupCreate($input: PermissionGroupCreateInput!) {
permissionGroupCreate(input: $input) {
errors: permissionGroupErrors {
...PermissionGroupErrorFragment
}
group {
...PermissionGroupDetailsFragment
}
}
}
`;
export const usePermissionGroupCreate = makeMutation<
PermissionGroupCreate,
PermissionGroupCreateVariables
>(permissionGroupCreate);
export const permissionGroupUpdate = gql`
${permissionGroupDetailsFragment}
${permissionGroupErrorFragment}
mutation PermissionGroupUpdate(
$id: ID!
$input: PermissionGroupUpdateInput!
) {
permissionGroupUpdate(id: $id, input: $input) {
errors: permissionGroupErrors {
...PermissionGroupErrorFragment
}
group {
...PermissionGroupDetailsFragment
}
}
}
`;
export const usePermissionGroupUpdate = makeMutation<
PermissionGroupUpdate,
PermissionGroupUpdateVariables
>(permissionGroupUpdate);

View file

@ -0,0 +1,106 @@
import gql from "graphql-tag";
import makeQuery from "@saleor/hooks/makeQuery";
import { staffMemberFragment } from "@saleor/staff/queries";
import { pageInfoFragment } from "../queries";
import {
PermissionGroupList,
PermissionGroupListVariables
} from "./types/PermissionGroupList";
import {
PermissionGroupDetails,
PermissionGroupDetailsVariables
} from "./types/PermissionGroupDetails";
export const permissionGroupFragment = gql`
fragment PermissionGroupFragment on Group {
id
name
users {
id
firstName
lastName
}
}
`;
export const permissionFragment = gql`
fragment PermissionFragment on Permission {
code
name
}
`;
export const permissionGroupDetailsFragment = gql`
${permissionGroupFragment}
${permissionFragment}
${staffMemberFragment}
fragment PermissionGroupDetailsFragment on Group {
...PermissionGroupFragment
permissions {
...PermissionFragment
}
users {
...StaffMemberFragment
}
}
`;
export const permissionGroupListQuery = gql`
${pageInfoFragment}
${permissionGroupFragment}
query PermissionGroupList(
$after: String
$before: String
$first: Int
$last: Int
$filter: PermissionGroupFilterInput
$sort: PermissionGroupSortingInput
) {
permissionGroups(
after: $after
before: $before
first: $first
last: $last
filter: $filter
sortBy: $sort
) {
edges {
node {
...PermissionGroupFragment
}
}
pageInfo {
...PageInfoFragment
}
}
}
`;
export const usePermissionGroupListQuery = makeQuery<
PermissionGroupList,
PermissionGroupListVariables
>(permissionGroupListQuery);
export const permissionGroupDetailsQuery = gql`
${permissionGroupDetailsFragment}
query PermissionGroupDetails($id: ID!, $userId: ID!) {
permissionGroup(id: $id) {
...PermissionGroupDetailsFragment
}
user(id: $userId) {
editableGroups {
id
}
userPermissions {
code
sourcePermissionGroups(userId: $userId) {
id
}
}
}
}
`;
export const usePermissionGroupDetailsQuery = makeQuery<
PermissionGroupDetails,
PermissionGroupDetailsVariables
>(permissionGroupDetailsQuery);

View file

@ -0,0 +1,25 @@
import { getUserName } from "@saleor/misc";
import { PermissionGroupDetails_permissionGroup_users } from "./types/PermissionGroupDetails";
import { MembersListUrlSortField } from "./urls";
export const sortMembers = (sort: string, asc: boolean) => (
a: PermissionGroupDetails_permissionGroup_users,
b: PermissionGroupDetails_permissionGroup_users
) => {
let valueA;
let valueB;
switch (sort) {
case MembersListUrlSortField.name:
valueA = getUserName(a);
valueB = getUserName(b);
break;
case MembersListUrlSortField.email:
valueA = a.email;
valueB = b.email;
break;
}
return asc
? ("" + valueA).localeCompare(valueB)
: ("" + valueA).localeCompare(valueB) * -1;
};

View file

@ -0,0 +1,15 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { PermissionEnum } from "./../../types/globalTypes";
// ====================================================
// GraphQL fragment: PermissionFragment
// ====================================================
export interface PermissionFragment {
__typename: "Permission";
code: PermissionEnum;
name: string;
}

View file

@ -0,0 +1,58 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { PermissionGroupCreateInput, PermissionGroupErrorCode, PermissionEnum } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: PermissionGroupCreate
// ====================================================
export interface PermissionGroupCreate_permissionGroupCreate_errors {
__typename: "PermissionGroupError";
code: PermissionGroupErrorCode;
field: string | null;
}
export interface PermissionGroupCreate_permissionGroupCreate_group_users_avatar {
__typename: "Image";
url: string;
}
export interface PermissionGroupCreate_permissionGroupCreate_group_users {
__typename: "User";
id: string;
firstName: string;
lastName: string;
email: string;
isActive: boolean;
avatar: PermissionGroupCreate_permissionGroupCreate_group_users_avatar | null;
}
export interface PermissionGroupCreate_permissionGroupCreate_group_permissions {
__typename: "Permission";
code: PermissionEnum;
name: string;
}
export interface PermissionGroupCreate_permissionGroupCreate_group {
__typename: "Group";
id: string;
name: string;
users: (PermissionGroupCreate_permissionGroupCreate_group_users | null)[] | null;
permissions: (PermissionGroupCreate_permissionGroupCreate_group_permissions | null)[] | null;
}
export interface PermissionGroupCreate_permissionGroupCreate {
__typename: "PermissionGroupCreate";
errors: PermissionGroupCreate_permissionGroupCreate_errors[];
group: PermissionGroupCreate_permissionGroupCreate_group | null;
}
export interface PermissionGroupCreate {
permissionGroupCreate: PermissionGroupCreate_permissionGroupCreate | null;
}
export interface PermissionGroupCreateVariables {
input: PermissionGroupCreateInput;
}

View file

@ -0,0 +1,28 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { PermissionGroupErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: PermissionGroupDelete
// ====================================================
export interface PermissionGroupDelete_permissionGroupDelete_errors {
__typename: "PermissionGroupError";
code: PermissionGroupErrorCode;
field: string | null;
}
export interface PermissionGroupDelete_permissionGroupDelete {
__typename: "PermissionGroupDelete";
errors: PermissionGroupDelete_permissionGroupDelete_errors[];
}
export interface PermissionGroupDelete {
permissionGroupDelete: PermissionGroupDelete_permissionGroupDelete | null;
}
export interface PermissionGroupDeleteVariables {
id: string;
}

View file

@ -0,0 +1,70 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { PermissionEnum } from "./../../types/globalTypes";
// ====================================================
// GraphQL query operation: PermissionGroupDetails
// ====================================================
export interface PermissionGroupDetails_permissionGroup_users_avatar {
__typename: "Image";
url: string;
}
export interface PermissionGroupDetails_permissionGroup_users {
__typename: "User";
id: string;
firstName: string;
lastName: string;
email: string;
isActive: boolean;
avatar: PermissionGroupDetails_permissionGroup_users_avatar | null;
}
export interface PermissionGroupDetails_permissionGroup_permissions {
__typename: "Permission";
code: PermissionEnum;
name: string;
}
export interface PermissionGroupDetails_permissionGroup {
__typename: "Group";
id: string;
name: string;
users: (PermissionGroupDetails_permissionGroup_users | null)[] | null;
permissions: (PermissionGroupDetails_permissionGroup_permissions | null)[] | null;
}
export interface PermissionGroupDetails_user_editableGroups {
__typename: "Group";
id: string;
}
export interface PermissionGroupDetails_user_userPermissions_sourcePermissionGroups {
__typename: "Group";
id: string;
}
export interface PermissionGroupDetails_user_userPermissions {
__typename: "UserPermission";
code: PermissionEnum;
sourcePermissionGroups: PermissionGroupDetails_user_userPermissions_sourcePermissionGroups[] | null;
}
export interface PermissionGroupDetails_user {
__typename: "User";
editableGroups: (PermissionGroupDetails_user_editableGroups | null)[] | null;
userPermissions: (PermissionGroupDetails_user_userPermissions | null)[] | null;
}
export interface PermissionGroupDetails {
permissionGroup: PermissionGroupDetails_permissionGroup | null;
user: PermissionGroupDetails_user | null;
}
export interface PermissionGroupDetailsVariables {
id: string;
userId: string;
}

View file

@ -0,0 +1,38 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { PermissionEnum } from "./../../types/globalTypes";
// ====================================================
// GraphQL fragment: PermissionGroupDetailsFragment
// ====================================================
export interface PermissionGroupDetailsFragment_users_avatar {
__typename: "Image";
url: string;
}
export interface PermissionGroupDetailsFragment_users {
__typename: "User";
id: string;
firstName: string;
lastName: string;
email: string;
isActive: boolean;
avatar: PermissionGroupDetailsFragment_users_avatar | null;
}
export interface PermissionGroupDetailsFragment_permissions {
__typename: "Permission";
code: PermissionEnum;
name: string;
}
export interface PermissionGroupDetailsFragment {
__typename: "Group";
id: string;
name: string;
users: (PermissionGroupDetailsFragment_users | null)[] | null;
permissions: (PermissionGroupDetailsFragment_permissions | null)[] | null;
}

View file

@ -0,0 +1,15 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { PermissionGroupErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL fragment: PermissionGroupErrorFragment
// ====================================================
export interface PermissionGroupErrorFragment {
__typename: "PermissionGroupError";
code: PermissionGroupErrorCode;
field: string | null;
}

View file

@ -0,0 +1,21 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL fragment: PermissionGroupFragment
// ====================================================
export interface PermissionGroupFragment_users {
__typename: "User";
id: string;
firstName: string;
lastName: string;
}
export interface PermissionGroupFragment {
__typename: "Group";
id: string;
name: string;
users: (PermissionGroupFragment_users | null)[] | null;
}

View file

@ -0,0 +1,55 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { PermissionGroupFilterInput, PermissionGroupSortingInput } from "./../../types/globalTypes";
// ====================================================
// GraphQL query operation: PermissionGroupList
// ====================================================
export interface PermissionGroupList_permissionGroups_edges_node_users {
__typename: "User";
id: string;
firstName: string;
lastName: string;
}
export interface PermissionGroupList_permissionGroups_edges_node {
__typename: "Group";
id: string;
name: string;
users: (PermissionGroupList_permissionGroups_edges_node_users | null)[] | null;
}
export interface PermissionGroupList_permissionGroups_edges {
__typename: "GroupCountableEdge";
node: PermissionGroupList_permissionGroups_edges_node;
}
export interface PermissionGroupList_permissionGroups_pageInfo {
__typename: "PageInfo";
endCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
}
export interface PermissionGroupList_permissionGroups {
__typename: "GroupCountableConnection";
edges: PermissionGroupList_permissionGroups_edges[];
pageInfo: PermissionGroupList_permissionGroups_pageInfo;
}
export interface PermissionGroupList {
permissionGroups: PermissionGroupList_permissionGroups | null;
}
export interface PermissionGroupListVariables {
after?: string | null;
before?: string | null;
first?: number | null;
last?: number | null;
filter?: PermissionGroupFilterInput | null;
sort?: PermissionGroupSortingInput | null;
}

View file

@ -0,0 +1,59 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { PermissionGroupUpdateInput, PermissionGroupErrorCode, PermissionEnum } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: PermissionGroupUpdate
// ====================================================
export interface PermissionGroupUpdate_permissionGroupUpdate_errors {
__typename: "PermissionGroupError";
code: PermissionGroupErrorCode;
field: string | null;
}
export interface PermissionGroupUpdate_permissionGroupUpdate_group_users_avatar {
__typename: "Image";
url: string;
}
export interface PermissionGroupUpdate_permissionGroupUpdate_group_users {
__typename: "User";
id: string;
firstName: string;
lastName: string;
email: string;
isActive: boolean;
avatar: PermissionGroupUpdate_permissionGroupUpdate_group_users_avatar | null;
}
export interface PermissionGroupUpdate_permissionGroupUpdate_group_permissions {
__typename: "Permission";
code: PermissionEnum;
name: string;
}
export interface PermissionGroupUpdate_permissionGroupUpdate_group {
__typename: "Group";
id: string;
name: string;
users: (PermissionGroupUpdate_permissionGroupUpdate_group_users | null)[] | null;
permissions: (PermissionGroupUpdate_permissionGroupUpdate_group_permissions | null)[] | null;
}
export interface PermissionGroupUpdate_permissionGroupUpdate {
__typename: "PermissionGroupUpdate";
errors: PermissionGroupUpdate_permissionGroupUpdate_errors[];
group: PermissionGroupUpdate_permissionGroupUpdate_group | null;
}
export interface PermissionGroupUpdate {
permissionGroupUpdate: PermissionGroupUpdate_permissionGroupUpdate | null;
}
export interface PermissionGroupUpdateVariables {
id: string;
input: PermissionGroupUpdateInput;
}

View file

@ -0,0 +1,59 @@
import { stringify as stringifyQs } from "qs";
import urlJoin from "url-join";
import {
BulkAction,
Dialog,
Pagination,
TabActionDialog,
Sort,
SingleAction
} from "@saleor/types";
const permissionGroupSection = "/permission-groups/";
export const permissionGroupListPath = permissionGroupSection;
export type PermissionGroupListUrlDialog = "remove" | TabActionDialog;
export enum PermissionGroupListUrlSortField {
name = "name"
}
export type PermissionGroupListUrlSort = Sort<PermissionGroupListUrlSortField>;
export type PermissionGroupListUrlQueryParams = Dialog<
PermissionGroupListUrlDialog
> &
Pagination &
PermissionGroupListUrlSort &
SingleAction;
export const permissionGroupListUrl = (
params?: PermissionGroupListUrlQueryParams
) => permissionGroupListPath + "?" + stringifyQs(params);
export const permissionGroupAddPath = urlJoin(permissionGroupSection, "add");
export const permissionGroupAddUrl = permissionGroupAddPath;
export enum MembersListUrlSortField {
name = "name",
email = "email"
}
export type MembersListUrlSort = Sort<MembersListUrlSortField>;
export const permissionGroupDetailsPath = (id: string) =>
urlJoin(permissionGroupSection, id);
export type PermissionGroupDetailsUrlDialog =
| "remove"
| "assign"
| "unassign"
| "unassignError";
export type PermissionGroupDetailsUrlQueryParams = BulkAction &
Pagination &
MembersListUrlSort &
Dialog<PermissionGroupDetailsUrlDialog>;
export const permissionGroupDetailsUrl = (
id: string,
params?: PermissionGroupDetailsUrlQueryParams
) =>
permissionGroupDetailsPath(encodeURIComponent(id)) +
"?" +
stringifyQs(params);

View file

@ -0,0 +1,81 @@
import difference from "lodash-es/difference";
import { ShopInfo_shop_permissions } from "@saleor/components/Shop/types/ShopInfo";
import { User } from "@saleor/auth/types/User";
import { PermissionGroupDetails_permissionGroup } from "./types/PermissionGroupDetails";
import { PermissionGroupDetailsPageFormData } from "./components/PermissionGroupDetailsPage";
/**
* Will return true if group has all permissions available in shop assigned.
*/
export const isGroupFullAccess = (
permissionGroup: PermissionGroupDetails_permissionGroup,
shopPermissions: ShopInfo_shop_permissions[]
) => {
const assignedCodes = extractPermissionCodes(permissionGroup);
if (assignedCodes.length !== shopPermissions?.length) {
return false;
}
for (const permission of shopPermissions) {
if (assignedCodes.indexOf(permission.code) === undefined) {
return false;
}
}
return true;
};
/**
* Return list of codes which are assigned to the permission group.
*/
export const extractPermissionCodes = (
permissionGroup: PermissionGroupDetails_permissionGroup
) =>
permissionGroup?.permissions
? permissionGroup.permissions.map(perm => perm.code)
: [];
/**
* Return lists of permissions which have to be added and removed from group.
*/
export const permissionsDiff = (
permissionGroup: PermissionGroupDetails_permissionGroup,
formData: PermissionGroupDetailsPageFormData
) => {
const newPermissions = formData.permissions;
const oldPermissions = extractPermissionCodes(permissionGroup);
return {
addPermissions: difference(newPermissions, oldPermissions),
removePermissions: difference(oldPermissions, newPermissions)
};
};
/**
* Return lists of users which have to be added and removed from group.
*/
export const usersDiff = (
permissionGroup: PermissionGroupDetails_permissionGroup,
formData: PermissionGroupDetailsPageFormData
) => {
const newUsers = formData.users.map(u => u.id);
const oldUsers = permissionGroup?.users.map(u => u.id);
return {
addUsers: difference(newUsers, oldUsers),
removeUsers: difference(oldUsers, newUsers)
};
};
/**
* Permissions are exceeded when group has permission which is not handled by user
*/
export const arePermissionsExceeded = (
permissionGroup: PermissionGroupDetails_permissionGroup,
user: User
) => {
const groupPermissions = extractPermissionCodes(permissionGroup);
const userPermissions = user.userPermissions.map(p => p.code);
return difference(groupPermissions, userPermissions).length > 0;
};

View file

@ -0,0 +1,88 @@
import React from "react";
import { useIntl } from "react-intl";
import { WindowTitle } from "@saleor/components/WindowTitle";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import useShop from "@saleor/hooks/useShop";
import useUser from "@saleor/hooks/useUser";
import { PermissionData } from "@saleor/permissionGroups/components/PermissionGroupDetailsPage";
import { PermissionGroupCreate } from "../../types/PermissionGroupCreate";
import { permissionGroupListUrl, permissionGroupDetailsUrl } from "../../urls";
import { usePermissionGroupCreate } from "../../mutations";
import PermissionGroupCreatePage from "../../components/PermissionGroupCreatePage";
const PermissionGroupCreateView: React.FC = () => {
const navigate = useNavigator();
const notify = useNotifier();
const intl = useIntl();
const shop = useShop();
const user = useUser();
const handleSuccess = (data: PermissionGroupCreate) => {
if (data?.permissionGroupCreate?.errors.length === 0) {
notify({
text: intl.formatMessage({
defaultMessage: "Permission group created"
})
});
navigate(permissionGroupDetailsUrl(data.permissionGroupCreate.group.id));
}
};
const [
createPermissionGroup,
createPermissionGroupResult
] = usePermissionGroupCreate({
onCompleted: handleSuccess
});
const errors =
createPermissionGroupResult?.data?.permissionGroupCreate?.errors || [];
const onSubmit = formData =>
createPermissionGroup({
variables: {
input: {
addPermissions: formData.hasFullAccess
? shop.permissions.map(perm => perm.code)
: formData.permissions,
addUsers: [],
name: formData.name
}
}
});
const userPermissions = user?.user.userPermissions.map(p => p.code) || [];
const permissions: PermissionData[] =
shop?.permissions.map(
p =>
({
...p,
disabled: !userPermissions.includes(p.code),
lastSource: false
} as PermissionData)
) || [];
return (
<>
<WindowTitle
title={intl.formatMessage({
defaultMessage: "Create category",
description: "window title"
})}
/>
<PermissionGroupCreatePage
errors={errors}
disabled={createPermissionGroupResult.loading}
permissions={permissions}
saveButtonBarState={createPermissionGroupResult.status}
onSubmit={onSubmit}
onBack={() => navigate(permissionGroupListUrl())}
/>
</>
);
};
PermissionGroupCreateView.displayName = "PermissionGroupCreateView";
export default PermissionGroupCreateView;

View file

@ -0,0 +1,2 @@
export { default } from "./PermissionGroupCreate";
export * from "./PermissionGroupCreate";

View file

@ -0,0 +1,216 @@
import React, { useState } from "react";
import useNavigator from "@saleor/hooks/useNavigator";
import useShop from "@saleor/hooks/useShop";
import useBulkActions from "@saleor/hooks/useBulkActions";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import { useIntl } from "react-intl";
import useNotifier from "@saleor/hooks/useNotifier";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
import { Button } from "@material-ui/core";
import { commonMessages } from "@saleor/intl";
import { getSortParams } from "@saleor/utils/sort";
import createSortHandler from "@saleor/utils/handlers/sortHandler";
import {
arePermissionsExceeded,
permissionsDiff,
usersDiff
} from "@saleor/permissionGroups/utils";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
import useStaffMemberSearch from "@saleor/searches/useStaffMemberSearch";
import useUser from "@saleor/hooks/useUser";
import MembersErrorDialog from "@saleor/permissionGroups/components/MembersErrorDialog";
import PermissionGroupDetailsPage from "../../components/PermissionGroupDetailsPage";
import AssignMembersDialog from "../../components/AssignMembersDialog";
import UnassignMembersDialog from "../../components/UnassignMembersDialog";
import { usePermissionGroupDetailsQuery } from "../../queries";
import { usePermissionGroupUpdate } from "../../mutations";
import {
permissionGroupDetailsUrl,
PermissionGroupDetailsUrlQueryParams,
PermissionGroupDetailsUrlDialog,
permissionGroupListUrl
} from "../../urls";
import { PermissionGroupUpdate } from "../../types/PermissionGroupUpdate";
interface PermissionGroupDetailsProps {
id: string;
params: PermissionGroupDetailsUrlQueryParams;
}
export const PermissionGroupDetails: React.FC<PermissionGroupDetailsProps> = ({
id,
params
}) => {
const navigate = useNavigator();
const shop = useShop();
const notify = useNotifier();
const intl = useIntl();
const user = useUser();
const { data, loading, refetch } = usePermissionGroupDetailsQuery({
displayLoader: true,
variables: { id, userId: user?.user.id }
});
const [membersList, setMembersList] = useStateFromProps(
data?.permissionGroup.users
);
const [membersModified, setMembersModified] = useState(false);
const { search, result: searchResult, loadMore } = useStaffMemberSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const handleUpdateSuccess = (data: PermissionGroupUpdate) => {
if (data.permissionGroupUpdate.errors.length === 0) {
notify({
text: intl.formatMessage(commonMessages.savedChanges)
});
refetch();
closeModal();
} else if (
data.permissionGroupUpdate.errors.some(e => e.field === "removeUsers")
) {
openModal("unassignError");
}
};
const { isSelected, listElements, toggle, toggleAll } = useBulkActions(
params.ids
);
const [
permissionGroupUpdate,
permissionGroupUpdateResult
] = usePermissionGroupUpdate({
onCompleted: handleUpdateSuccess
});
const [openModal, closeModal] = createDialogActionHandlers<
PermissionGroupDetailsUrlDialog,
PermissionGroupDetailsUrlQueryParams
>(navigate, params => permissionGroupDetailsUrl(id, params), params);
const handleSort = createSortHandler(
navigate,
params => permissionGroupDetailsUrl(id, params),
params
);
const unassignMembers = () => {
setMembersList(membersList?.filter(m => !listElements.includes(m.id)));
setMembersModified(true);
closeModal();
};
const isGroupEditable =
(data?.user.editableGroups || []).filter(g => g.id === id).length > 0;
const lastSourcesOfPermission = (data?.user.userPermissions || [])
.filter(
perm =>
perm.sourcePermissionGroups.length === 1 &&
perm.sourcePermissionGroups[0].id === id
)
.map(perm => perm.code);
const userPermissions = user?.user.userPermissions.map(p => p.code) || [];
const permissions = (shop?.permissions || []).map(perm => ({
...perm,
disabled: !userPermissions.includes(perm.code),
lastSource: lastSourcesOfPermission.includes(perm.code)
}));
const permissionsExceeded = arePermissionsExceeded(
data?.permissionGroup,
user.user
);
const disabled = loading || !isGroupEditable || permissionsExceeded;
return (
<>
<PermissionGroupDetailsPage
permissionGroup={data?.permissionGroup}
permissionsExceeded={permissionsExceeded}
members={membersList || []}
membersModified={membersModified}
onBack={() => navigate(permissionGroupListUrl())}
onAssign={() => openModal("assign")}
onUnassign={ids => openModal("unassign", { ids })}
errors={
permissionGroupUpdateResult?.data?.permissionGroupUpdate.errors || []
}
onSubmit={formData =>
permissionGroupUpdate({
variables: {
id,
input: {
name: formData.name,
...permissionsDiff(data?.permissionGroup, formData),
...usersDiff(data?.permissionGroup, formData)
}
}
})
}
permissions={permissions}
saveButtonBarState={permissionGroupUpdateResult.status}
disabled={disabled}
toggle={toggle}
toggleAll={toggleAll}
isChecked={isSelected}
selected={listElements.length}
sort={getSortParams(params)}
toolbar={
<Button
color="primary"
onClick={() => openModal("unassign", { ids: listElements })}
>
{intl.formatMessage({
defaultMessage: "Unassign",
description: "button title"
})}
</Button>
}
onSort={handleSort}
/>
<AssignMembersDialog
loading={searchResult.loading}
staffMembers={searchResult?.data?.search.edges.map(edge => edge.node)}
onSearchChange={search}
onFetchMore={loadMore}
disabled={disabled}
hasMore={searchResult?.data?.search.pageInfo.hasNextPage}
initialSearch=""
confirmButtonState={permissionGroupUpdateResult.status}
open={params.action === "assign"}
onClose={closeModal}
onSubmit={formData => {
setMembersList([
...membersList,
...formData.filter(member => !membersList.includes(member))
]);
setMembersModified(true);
closeModal();
}}
/>
<UnassignMembersDialog
onConfirm={unassignMembers}
confirmButtonState={permissionGroupUpdateResult.status}
quantity={listElements.length}
open={params.action === "unassign"}
onClose={closeModal}
/>
<MembersErrorDialog
onConfirm={closeModal}
confirmButtonState={permissionGroupUpdateResult.status}
open={params.action === "unassignError"}
onClose={closeModal}
/>
</>
);
};
export default PermissionGroupDetails;

View file

@ -0,0 +1,2 @@
export { default } from "./PermissionGroupDetails";
export * from "./PermissionGroupDetails";

View file

@ -0,0 +1,129 @@
import React from "react";
import { useIntl } from "react-intl";
import useListSettings from "@saleor/hooks/useListSettings";
import useNavigator from "@saleor/hooks/useNavigator";
import usePaginator, {
createPaginationState
} from "@saleor/hooks/usePaginator";
import { configurationMenuUrl } from "@saleor/configuration";
import useNotifier from "@saleor/hooks/useNotifier";
import { ListViews } from "@saleor/types";
import { getSortParams } from "@saleor/utils/sort";
import createSortHandler from "@saleor/utils/handlers/sortHandler";
import PermissionGroupDeleteDialog from "@saleor/permissionGroups/components/PermissionGroupDeleteDialog";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import { usePermissionGroupListQuery } from "@saleor/permissionGroups/queries";
import { PermissionGroupDelete } from "@saleor/permissionGroups/types/PermissionGroupDelete";
import { usePermissionGroupDelete } from "@saleor/permissionGroups/mutations";
import { getStringOrPlaceholder } from "@saleor/misc";
import PermissionGroupListPage from "../../components/PermissionGroupListPage";
import {
permissionGroupListUrl,
permissionGroupAddUrl,
PermissionGroupListUrlQueryParams,
permissionGroupDetailsUrl,
PermissionGroupListUrlDialog
} from "../../urls";
import { getSortQueryVariables } from "./sort";
interface PermissionGroupListProps {
params: PermissionGroupListUrlQueryParams;
}
export const PermissionGroupList: React.FC<PermissionGroupListProps> = ({
params
}) => {
const navigate = useNavigator();
const paginate = usePaginator();
const notify = useNotifier();
const intl = useIntl();
const { updateListSettings, settings } = useListSettings(
ListViews.STAFF_MEMBERS_LIST
);
const paginationState = createPaginationState(settings.rowNumber, params);
const queryVariables = React.useMemo(
() => ({
...paginationState,
sort: getSortQueryVariables(params)
}),
[params]
);
const { data, loading, refetch } = usePermissionGroupListQuery({
displayLoader: true,
variables: queryVariables
});
const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
data?.permissionGroups.pageInfo,
paginationState,
params
);
const handleSort = createSortHandler(
navigate,
permissionGroupListUrl,
params
);
const [openModal, closeModal] = createDialogActionHandlers<
PermissionGroupListUrlDialog,
PermissionGroupListUrlQueryParams
>(navigate, permissionGroupListUrl, params);
const permissionGroups = data?.permissionGroups?.edges.map(edge => edge.node);
const handleDeleteSuccess = (data: PermissionGroupDelete) => {
if (data.permissionGroupDelete.errors.length === 0) {
notify({
text: intl.formatMessage({
defaultMessage: "Permission Group Deleted"
})
});
refetch();
closeModal();
}
};
const [permissionGroupDelete] = usePermissionGroupDelete({
onCompleted: handleDeleteSuccess
});
return (
<>
<PermissionGroupListPage
disabled={loading}
settings={settings}
pageInfo={pageInfo}
sort={getSortParams(params)}
permissionGroups={permissionGroups}
onAdd={() => navigate(permissionGroupAddUrl)}
onBack={() => navigate(configurationMenuUrl)}
onDelete={id => openModal("remove", { id })}
onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage}
onUpdateListSettings={updateListSettings}
onRowClick={id => () => navigate(permissionGroupDetailsUrl(id))}
onSort={handleSort}
/>
<PermissionGroupDeleteDialog
onConfirm={() =>
permissionGroupDelete({
variables: {
id: params.id
}
})
}
name={getStringOrPlaceholder(
permissionGroups?.find(group => group.id === params.id)?.name
)}
confirmButtonState={"default"}
open={params.action === "remove"}
onClose={closeModal}
/>
</>
);
};
export default PermissionGroupList;

View file

@ -0,0 +1,2 @@
export { default } from "./PermissionGroupList";
export * from "./PermissionGroupList";

View file

@ -0,0 +1,18 @@
import { PermissionGroupListUrlSortField } from "@saleor/permissionGroups/urls";
import { PermissionGroupSortField } from "@saleor/types/globalTypes";
import { createGetSortQueryVariables } from "@saleor/utils/sort";
export function getSortQueryField(
sort: PermissionGroupListUrlSortField
): PermissionGroupSortField {
switch (sort) {
case PermissionGroupListUrlSortField.name:
return PermissionGroupSortField.NAME;
default:
return undefined;
}
}
export const getSortQueryVariables = createGetSortQueryVariables(
getSortQueryField
);

View file

@ -0,0 +1,43 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: SearchPermissionGroups
// ====================================================
export interface SearchPermissionGroups_search_edges_node {
__typename: "Group";
id: string;
name: string;
userCanManage: boolean;
}
export interface SearchPermissionGroups_search_edges {
__typename: "GroupCountableEdge";
node: SearchPermissionGroups_search_edges_node;
}
export interface SearchPermissionGroups_search_pageInfo {
__typename: "PageInfo";
endCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
}
export interface SearchPermissionGroups_search {
__typename: "GroupCountableConnection";
edges: SearchPermissionGroups_search_edges[];
pageInfo: SearchPermissionGroups_search_pageInfo;
}
export interface SearchPermissionGroups {
search: SearchPermissionGroups_search | null;
}
export interface SearchPermissionGroupsVariables {
after?: string | null;
first: number;
query: string;
}

View file

@ -0,0 +1,52 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: SearchStaffMembers
// ====================================================
export interface SearchStaffMembers_search_edges_node_avatar {
__typename: "Image";
alt: string | null;
url: string;
}
export interface SearchStaffMembers_search_edges_node {
__typename: "User";
id: string;
email: string;
firstName: string;
lastName: string;
isActive: boolean;
avatar: SearchStaffMembers_search_edges_node_avatar | null;
}
export interface SearchStaffMembers_search_edges {
__typename: "UserCountableEdge";
node: SearchStaffMembers_search_edges_node;
}
export interface SearchStaffMembers_search_pageInfo {
__typename: "PageInfo";
endCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
}
export interface SearchStaffMembers_search {
__typename: "UserCountableConnection";
edges: SearchStaffMembers_search_edges[];
pageInfo: SearchStaffMembers_search_pageInfo;
}
export interface SearchStaffMembers {
search: SearchStaffMembers_search | null;
}
export interface SearchStaffMembersVariables {
after?: string | null;
first: number;
query: string;
}

View file

@ -0,0 +1,35 @@
import gql from "graphql-tag";
import makeTopLevelSearch from "@saleor/hooks/makeTopLevelSearch";
import { pageInfoFragment } from "@saleor/queries";
import {
SearchPermissionGroups,
SearchPermissionGroupsVariables
} from "./types/SearchPermissionGroups";
export const searchPermissionGroups = gql`
${pageInfoFragment}
query SearchPermissionGroups($after: String, $first: Int!, $query: String!) {
search: permissionGroups(
after: $after
first: $first
filter: { search: $query }
) {
edges {
node {
id
name
userCanManage
}
}
pageInfo {
...PageInfoFragment
}
}
}
`;
export default makeTopLevelSearch<
SearchPermissionGroups,
SearchPermissionGroupsVariables
>(searchPermissionGroups);

View file

@ -0,0 +1,41 @@
import gql from "graphql-tag";
import makeTopLevelSearch from "@saleor/hooks/makeTopLevelSearch";
import { pageInfoFragment } from "@saleor/queries";
import {
SearchStaffMembers,
SearchStaffMembersVariables
} from "./types/SearchStaffMembers";
export const searchStaffMembers = gql`
${pageInfoFragment}
query SearchStaffMembers($after: String, $first: Int!, $query: String!) {
search: staffUsers(
after: $after
first: $first
filter: { search: $query }
) {
edges {
node {
id
email
firstName
lastName
isActive
avatar {
alt
url
}
}
}
pageInfo {
...PageInfoFragment
}
}
}
`;
export default makeTopLevelSearch<
SearchStaffMembers,
SearchStaffMembersVariables
>(searchStaffMembers);

View file

@ -15,6 +15,8 @@ import { ShopInfo_shop_permissions } from "@saleor/components/Shop/types/ShopInf
import { sectionNames } from "@saleor/intl";
import { PermissionEnum } from "@saleor/types/globalTypes";
import { AccountErrorFragment } from "@saleor/customers/types/AccountErrorFragment";
import { getFormErrors } from "@saleor/utils/errors";
import getAccountErrorMessage from "@saleor/utils/errors/account";
import ServiceInfo from "../ServiceInfo";
export interface ServiceCreatePageFormData {
@ -49,6 +51,10 @@ const ServiceCreatePage: React.FC<ServiceCreatePageProps> = props => {
name: "",
permissions: []
};
const formErrors = getFormErrors(["permissions"], errors || []);
const permissionsError = getAccountErrorMessage(formErrors.permissions, intl);
return (
<Form initial={initialForm} onSubmit={onSubmit} confirmLeave>
{({ data, change, hasChanged, submit }) => (
@ -73,9 +79,20 @@ const ServiceCreatePage: React.FC<ServiceCreatePageProps> = props => {
</div>
<AccountPermissions
data={data}
errorMessage={permissionsError}
disabled={disabled}
permissions={permissions}
permissionsExceeded={false}
onChange={change}
fullAccessLabel={intl.formatMessage({
defaultMessage: "User has full access to the store",
description: "checkbox label"
})}
description={intl.formatMessage({
defaultMessage:
"Expand or restrict user's permissions to access certain part of saleor system.",
description: "card description"
})}
/>
<CardSpacer />
<AccountStatus

View file

@ -17,6 +17,8 @@ import { maybe } from "@saleor/misc";
import { ServiceDetails_serviceAccount } from "@saleor/services/types/ServiceDetails";
import { PermissionEnum } from "@saleor/types/globalTypes";
import { AccountErrorFragment } from "@saleor/customers/types/AccountErrorFragment";
import { getFormErrors } from "@saleor/utils/errors";
import getAccountErrorMessage from "@saleor/utils/errors/account";
import ServiceDefaultToken from "../ServiceDefaultToken";
import ServiceInfo from "../ServiceInfo";
import ServiceTokens from "../ServiceTokens";
@ -63,6 +65,9 @@ const ServiceDetailsPage: React.FC<ServiceDetailsPageProps> = props => {
} = props;
const intl = useIntl();
const formErrors = getFormErrors(["permissions"], errors || []);
const permissionsError = getAccountErrorMessage(formErrors.permissions, intl);
const initialForm: ServiceDetailsPageFormData = {
hasFullAccess: maybe(
() =>
@ -115,9 +120,20 @@ const ServiceDetailsPage: React.FC<ServiceDetailsPageProps> = props => {
<div>
<AccountPermissions
data={data}
errorMessage={permissionsError}
disabled={disabled}
permissions={permissions}
permissionsExceeded={false}
onChange={change}
fullAccessLabel={intl.formatMessage({
defaultMessage: "User has full access to the store",
description: "checkbox label"
})}
description={intl.formatMessage({
defaultMessage:
"Expand or restrict user's permissions to access certain part of saleor system.",
description: "card description"
})}
/>
<CardSpacer />
<AccountStatus

View file

@ -1,4 +1,5 @@
import { PermissionEnum } from "@saleor/types/globalTypes";
import { ServiceDetails_serviceAccount } from "./types/ServiceDetails";
import { ServiceList_serviceAccounts_edges_node } from "./types/ServiceList";

View file

@ -1,6 +1,6 @@
import gql from "graphql-tag";
import { accountFragmentError } from "@saleor/customers/mutations";
import { accountErrorFragment } from "@saleor/customers/mutations";
import { TypedMutation } from "../mutations";
import { serviceDetailsFragment, serviceFragment } from "./queries";
import { ServiceCreate, ServiceCreateVariables } from "./types/ServiceCreate";
@ -16,7 +16,7 @@ import {
import { ServiceUpdate, ServiceUpdateVariables } from "./types/ServiceUpdate";
const serviceCreateMutation = gql`
${accountFragmentError}
${accountErrorFragment}
${serviceFragment}
mutation ServiceCreate($input: ServiceAccountInput!) {
serviceAccountCreate(input: $input) {
@ -37,7 +37,7 @@ export const ServiceCreateMutation = TypedMutation<
>(serviceCreateMutation);
const serviceDeleteMutation = gql`
${accountFragmentError}
${accountErrorFragment}
mutation ServiceDelete($id: ID!) {
serviceAccountDelete(id: $id) {
errors: accountErrors {
@ -52,7 +52,7 @@ export const ServiceDeleteMutation = TypedMutation<
>(serviceDeleteMutation);
const serviceUpdateMutation = gql`
${accountFragmentError}
${accountErrorFragment}
${serviceDetailsFragment}
mutation ServiceUpdate($id: ID!, $input: ServiceAccountInput!) {
serviceAccountUpdate(id: $id, input: $input) {
@ -72,7 +72,7 @@ export const ServiceUpdateMutation = TypedMutation<
>(serviceUpdateMutation);
const serviceTokenCreate = gql`
${accountFragmentError}
${accountErrorFragment}
mutation ServiceTokenCreate($input: ServiceAccountTokenInput!) {
serviceAccountTokenCreate(input: $input) {
authToken
@ -88,7 +88,7 @@ export const ServiceTokenCreateMutation = TypedMutation<
>(serviceTokenCreate);
const serviceTokenDelete = gql`
${accountFragmentError}
${accountErrorFragment}
mutation ServiceTokenDelete($id: ID!) {
serviceAccountTokenDelete(id: $id) {
errors: accountErrors {

View file

@ -177,7 +177,7 @@ export const ShippingZonesList: React.FC<ShippingZonesListProps> = ({
<DeleteIcon />
</IconButton>
}
userPermissions={maybe(() => user.permissions, [])}
userPermissions={user?.userPermissions || []}
/>
<ActionDialog

View file

@ -1,3 +1,6 @@
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
@ -5,9 +8,6 @@ import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import { makeStyles } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import ConfirmButton, {
ConfirmButtonTransitionState
} from "@saleor/components/ConfirmButton";
@ -17,20 +17,22 @@ import { buttonMessages, commonMessages } from "@saleor/intl";
import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors";
import { getFormErrors } from "@saleor/utils/errors";
import { StaffErrorFragment } from "@saleor/staff/types/StaffErrorFragment";
import { SearchPermissionGroups_search_edges_node } from "@saleor/searches/types/SearchPermissionGroups";
import { FetchMoreProps, SearchPageProps } from "@saleor/types";
import getStaffErrorMessage from "@saleor/utils/errors/staff";
export interface FormData {
export interface AddMemberFormData {
email: string;
firstName: string;
fullAccess: boolean;
lastName: string;
permissionGroups: string[];
}
const initialForm: FormData = {
const initialForm: AddMemberFormData = {
email: "",
firstName: "",
fullAccess: false,
lastName: ""
lastName: "",
permissionGroups: []
};
const useStyles = makeStyles(
@ -55,21 +57,23 @@ const useStyles = makeStyles(
{ name: "StaffAddMemberDialog" }
);
interface StaffAddMemberDialogProps {
interface StaffAddMemberDialogProps extends SearchPageProps {
availablePermissionGroups: SearchPermissionGroups_search_edges_node[];
confirmButtonState: ConfirmButtonTransitionState;
disabled: boolean;
errors: StaffErrorFragment[];
fetchMorePermissionGroups: FetchMoreProps;
open: boolean;
onClose: () => void;
onConfirm: (data: FormData) => void;
onConfirm: (data: AddMemberFormData) => void;
}
const StaffAddMemberDialog: React.FC<StaffAddMemberDialogProps> = props => {
const { confirmButtonState, errors, open, onClose, onConfirm } = props;
const { confirmButtonState, errors, onClose, onConfirm, open } = props;
const classes = useStyles(props);
const dialogErrors = useModalDialogErrors(errors, open);
const intl = useIntl();
const formErrors = getFormErrors(
["firstName", "lastName", "email"],
dialogErrors
@ -78,7 +82,7 @@ const StaffAddMemberDialog: React.FC<StaffAddMemberDialogProps> = props => {
return (
<Dialog onClose={onClose} open={open}>
<Form initial={initialForm} onSubmit={onConfirm}>
{({ change, data, hasChanged }) => (
{({ change, data: formData, hasChanged }) => (
<>
<DialogTitle>
<FormattedMessage
@ -90,20 +94,26 @@ const StaffAddMemberDialog: React.FC<StaffAddMemberDialogProps> = props => {
<div className={classes.textFieldGrid}>
<TextField
error={!!formErrors.firstName}
helperText={getStaffErrorMessage(formErrors.firstName, intl)}
helperText={
!!formErrors.firstName &&
getStaffErrorMessage(formErrors.firstName, intl)
}
label={intl.formatMessage(commonMessages.firstName)}
name="firstName"
type="text"
value={data.firstName}
value={formData.firstName}
onChange={change}
/>
<TextField
error={!!formErrors.lastName}
helperText={getStaffErrorMessage(formErrors.lastName, intl)}
helperText={
!!formErrors.lastName &&
getStaffErrorMessage(formErrors.lastName, intl)
}
label={intl.formatMessage(commonMessages.lastName)}
name="lastName"
type="text"
value={data.lastName}
value={formData.lastName}
onChange={change}
/>
</div>
@ -111,14 +121,18 @@ const StaffAddMemberDialog: React.FC<StaffAddMemberDialogProps> = props => {
<TextField
error={!!formErrors.email}
fullWidth
helperText={getStaffErrorMessage(formErrors.email, intl)}
helperText={
!!formErrors.email &&
getStaffErrorMessage(formErrors.email, intl)
}
label={intl.formatMessage(commonMessages.email)}
name="email"
type="email"
value={data.email}
value={formData.email}
onChange={change}
/>
</DialogContent>
<hr className={classes.hr} />
<DialogActions>
<Button onClick={onClose}>
<FormattedMessage {...buttonMessages.back} />

View file

@ -1,29 +1,32 @@
import { Omit } from "@material-ui/core";
import { storiesOf } from "@storybook/react";
import React from "react";
import { permissions } from "@saleor/fixtures";
import StaffDetailsPage, {
StaffDetailsPageProps
} from "../../../staff/components/StaffDetailsPage";
import { staffMember } from "../../../staff/fixtures";
import Decorator from "../../Decorator";
} from "@saleor/staff/components/StaffDetailsPage";
import { staffMember } from "@saleor/staff/fixtures";
import Decorator from "@saleor/storybook/Decorator";
import { userPermissionGroups } from "@saleor/permissionGroups/fixtures";
const props: Omit<StaffDetailsPageProps, "classes"> = {
availablePermissionGroups: [],
canEditAvatar: false,
canEditPreferences: false,
canEditStatus: true,
canRemove: true,
disabled: false,
errors: [],
fetchMorePermissionGroups: undefined,
initialSearch: "",
onBack: () => undefined,
onChangePassword: () => undefined,
onDelete: () => undefined,
onImageDelete: () => undefined,
onImageUpload: () => undefined,
onSearchChange: () => undefined,
onSubmit: () => undefined,
permissions,
saveButtonBarState: "default",
staffMember
staffMember: { ...staffMember, permissionGroups: userPermissionGroups }
};
storiesOf("Views / Staff / Staff member details", module)
@ -32,15 +35,7 @@ storiesOf("Views / Staff / Staff member details", module)
.add("loading", () => (
<StaffDetailsPage {...props} disabled={true} staffMember={undefined} />
))
.add("not admin", () => (
<StaffDetailsPage
{...props}
staffMember={{
...staffMember,
permissions: staffMember.permissions.slice(1)
}}
/>
))
.add("not admin", () => <StaffDetailsPage {...props} canEditStatus={false} />)
.add("himself", () => (
<StaffDetailsPage
{...props}

View file

@ -1,150 +1,199 @@
import React from "react";
import { useIntl } from "react-intl";
import AccountPermissions from "@saleor/components/AccountPermissions";
import { Card, CardContent, Typography } from "@material-ui/core";
import AccountPermissionGroups from "@saleor/components/AccountPermissionGroups";
import AccountStatus from "@saleor/components/AccountStatus";
import AppHeader from "@saleor/components/AppHeader";
import CardSpacer from "@saleor/components/CardSpacer";
import CardTitle from "@saleor/components/CardTitle";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form";
import Grid from "@saleor/components/Grid";
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { ShopInfo_shop_permissions } from "@saleor/components/Shop/types/ShopInfo";
import useLocale from "@saleor/hooks/useLocale";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { sectionNames } from "@saleor/intl";
import { getUserName, maybe } from "../../../misc";
import { PermissionEnum } from "../../../types/globalTypes";
import { getUserName } from "@saleor/misc";
import { SearchPermissionGroups_search_edges_node } from "@saleor/searches/types/SearchPermissionGroups";
import { FetchMoreProps, SearchPageProps } from "@saleor/types";
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
import { StaffErrorFragment } from "@saleor/staff/types/StaffErrorFragment";
import { StaffMemberDetails_user } from "../../types/StaffMemberDetails";
import StaffPassword from "../StaffPassword/StaffPassword";
import StaffPreferences from "../StaffPreferences";
import StaffProperties from "../StaffProperties/StaffProperties";
import StaffPassword from "../StaffPassword/StaffPassword";
interface FormData {
hasFullAccess: boolean;
isActive: boolean;
permissions: PermissionEnum[];
firstName: string;
lastName: string;
export interface StaffDetailsFormData {
email: string;
firstName: string;
isActive: boolean;
lastName: string;
permissionGroups: string[];
}
export interface StaffDetailsPageProps {
export interface StaffDetailsPageProps extends SearchPageProps {
availablePermissionGroups: SearchPermissionGroups_search_edges_node[];
canEditAvatar: boolean;
canEditPreferences: boolean;
canEditStatus: boolean;
canRemove: boolean;
disabled: boolean;
permissions: ShopInfo_shop_permissions[];
fetchMorePermissionGroups: FetchMoreProps;
saveButtonBarState: ConfirmButtonTransitionState;
staffMember: StaffMemberDetails_user;
errors: StaffErrorFragment[];
onBack: () => void;
onChangePassword: () => void;
onDelete: () => void;
onImageDelete: () => void;
onSubmit: (data: FormData) => void;
onSubmit: (data: StaffDetailsFormData) => void;
onImageUpload(file: File);
}
const StaffDetailsPage: React.FC<StaffDetailsPageProps> = ({
availablePermissionGroups,
canEditAvatar,
canEditPreferences,
canEditStatus,
canRemove,
disabled,
permissions,
saveButtonBarState,
staffMember,
errors,
fetchMorePermissionGroups,
initialSearch,
onBack,
onChangePassword,
onDelete,
onImageDelete,
onImageUpload,
onSubmit
onSearchChange,
onSubmit,
saveButtonBarState,
staffMember
}: StaffDetailsPageProps) => {
const intl = useIntl();
const { locale, setLocale } = useLocale();
const [
permissionGroupsDisplayValues,
setPermissionGroupsDisplayValues
] = useStateFromProps<MultiAutocompleteChoiceType[]>(
(staffMember?.permissionGroups || []).map(group => ({
disabled: !group.userCanManage,
label: group.name,
value: group.id
})) || []
);
const initialForm: FormData = {
email: maybe(() => staffMember.email, ""),
firstName: maybe(() => staffMember.firstName, ""),
hasFullAccess: maybe(
() =>
permissions.filter(
perm =>
maybe(() => staffMember.permissions, []).filter(
userPerm => userPerm.code === perm.code
).length === 0
).length === 0,
false
),
isActive: maybe(() => staffMember.isActive, false),
lastName: maybe(() => staffMember.lastName, ""),
permissions: maybe(() => staffMember.permissions, []).map(perm => perm.code)
const initialForm: StaffDetailsFormData = {
email: staffMember?.email || "",
firstName: staffMember?.firstName || "",
isActive: !!staffMember?.isActive,
lastName: staffMember?.lastName || "",
permissionGroups: staffMember?.permissionGroups.map(pg => pg.id) || []
};
return (
<Form initial={initialForm} onSubmit={onSubmit} confirmLeave>
{({ data, change, hasChanged, submit }) => (
<Container>
<AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.staff)}
</AppHeader>
<PageHeader title={getUserName(staffMember)} />
<Grid>
<div>
<StaffProperties
data={data}
disabled={disabled}
canEditAvatar={canEditAvatar}
staffMember={staffMember}
onChange={change}
onImageUpload={onImageUpload}
onImageDelete={onImageDelete}
/>
{canEditPreferences && (
<>
<CardSpacer />
<StaffPassword onChangePassword={onChangePassword} />
</>
)}
</div>
<div>
{canEditPreferences && (
<StaffPreferences locale={locale} onLocaleChange={setLocale} />
)}
{canEditStatus && (
<>
<AccountPermissions
data={data}
disabled={disabled}
permissions={permissions}
onChange={change}
{({ data: formData, change, hasChanged, submit, toggleValue }) => {
const permissionGroupsChange = createMultiAutocompleteSelectHandler(
toggleValue,
setPermissionGroupsDisplayValues,
permissionGroupsDisplayValues,
availablePermissionGroups?.map(group => ({
label: group.name,
value: group.id
})) || []
);
return (
<Container>
<AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.staff)}
</AppHeader>
<PageHeader title={getUserName(staffMember)} />
<Grid>
<div>
<StaffProperties
errors={errors}
data={formData}
disabled={disabled}
canEditAvatar={canEditAvatar}
staffMember={staffMember}
onChange={change}
onImageUpload={onImageUpload}
onImageDelete={onImageDelete}
/>
{canEditPreferences && (
<>
<CardSpacer />
<StaffPassword onChangePassword={onChangePassword} />
</>
)}
</div>
<div>
{canEditPreferences && (
<StaffPreferences
locale={locale}
onLocaleChange={setLocale}
/>
<CardSpacer />
<AccountStatus
data={data}
disabled={disabled}
label={intl.formatMessage({
defaultMessage: "User is active",
description: "checkbox label"
})}
onChange={change}
/>
</>
)}
</div>
</Grid>
<SaveButtonBar
disabled={disabled || !hasChanged}
state={saveButtonBarState}
onCancel={onBack}
onSave={submit}
onDelete={canRemove ? onDelete : undefined}
/>
</Container>
)}
)}
{canEditStatus && (
<>
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Permissions",
description: "dialog header"
})}
/>
<CardContent>
<Typography>
{intl.formatMessage({
defaultMessage: "User is assigned to:",
description: "card description"
})}
</Typography>
<AccountPermissionGroups
formData={formData}
disabled={disabled}
errors={errors}
initialSearch={initialSearch}
availablePermissionGroups={availablePermissionGroups}
onChange={permissionGroupsChange}
onSearchChange={onSearchChange}
displayValues={permissionGroupsDisplayValues}
{...fetchMorePermissionGroups}
/>
</CardContent>
</Card>
<CardSpacer />
<AccountStatus
data={formData}
disabled={disabled}
label={intl.formatMessage({
defaultMessage: "User is active",
description: "checkbox label"
})}
onChange={change}
/>
</>
)}
</div>
</Grid>
<SaveButtonBar
disabled={disabled || !hasChanged}
state={saveButtonBarState}
onCancel={onBack}
onSave={submit}
onDelete={canRemove ? onDelete : undefined}
/>
</Container>
);
}}
</Form>
);
};

View file

@ -9,12 +9,10 @@ import {
tabPageProps,
sortPageProps,
filterPageProps
} from "../../../fixtures";
import StaffListPage, {
StaffListPageProps
} from "../../../staff/components/StaffListPage";
import { staffMembers } from "../../../staff/fixtures";
import Decorator from "../../Decorator";
} from "@saleor/fixtures";
import { staffMembers } from "@saleor/staff/fixtures";
import Decorator from "@saleor/storybook/Decorator";
import StaffListPage, { StaffListPageProps } from ".";
const props: StaffListPageProps = {
...pageListProps.default,

View file

@ -10,6 +10,9 @@ import { FormattedMessage, useIntl } from "react-intl";
import photoIcon from "@assets/images/photo-icon.svg";
import CardTitle from "@saleor/components/CardTitle";
import { commonMessages } from "@saleor/intl";
import { getFormErrors } from "@saleor/utils/errors";
import { StaffErrorFragment } from "@saleor/staff/types/StaffErrorFragment";
import getStaffErrorMessage from "@saleor/utils/errors/staff";
import { getUserInitials, maybe } from "../../../misc";
import { StaffMemberDetails_user } from "../../types/StaffMemberDetails";
@ -100,6 +103,7 @@ interface StaffPropertiesProps {
firstName: string;
lastName: string;
};
errors: StaffErrorFragment[];
disabled: boolean;
staffMember: StaffMemberDetails_user;
onChange: (event: React.ChangeEvent<any>) => void;
@ -112,6 +116,7 @@ const StaffProperties: React.FC<StaffPropertiesProps> = props => {
canEditAvatar,
className,
data,
errors,
staffMember,
onChange,
onImageDelete,
@ -123,6 +128,7 @@ const StaffProperties: React.FC<StaffPropertiesProps> = props => {
const imgInputAnchor = React.createRef<HTMLInputElement>();
const clickImgInput = () => imgInputAnchor.current.click();
const formErrors = getFormErrors(["id"], errors || []);
return (
<Card className={className}>
@ -205,6 +211,13 @@ const StaffProperties: React.FC<StaffPropertiesProps> = props => {
</div>
</div>
</CardContent>
{!!formErrors.id && (
<CardContent>
<Typography color="error">
{getStaffErrorMessage(formErrors.id, intl)}
</Typography>
</CardContent>
)}
</Card>
);
};

View file

@ -145,5 +145,9 @@ export const staffMember: StaffMemberDetails_user = {
id: "VXNlcjoyMQ==",
isActive: true,
lastName: "Smith",
permissions
permissionGroups: [],
userPermissions: permissions.map(p => ({
...p,
__typename: "UserPermission"
}))
};

View file

@ -1,9 +1,12 @@
import gql from "graphql-tag";
import makeMutation from "@saleor/hooks/makeMutation";
import { accountFragmentError } from "@saleor/customers/mutations";
import { accountErrorFragment } from "@saleor/customers/mutations";
import { TypedMutation } from "../mutations";
import { staffMemberDetailsFragment } from "./queries";
import {
ChangeStaffPassword,
ChangeStaffPasswordVariables
} from "./types/ChangeStaffPassword";
import { StaffAvatarDelete } from "./types/StaffAvatarDelete";
import {
StaffAvatarUpdate,
@ -21,17 +24,21 @@ import {
StaffMemberUpdate,
StaffMemberUpdateVariables
} from "./types/StaffMemberUpdate";
import {
ChangeStaffPassword,
ChangeStaffPasswordVariables
} from "./types/ChangeStaffPassword";
const staffErrorFragment = gql`
export const staffErrorFragment = gql`
fragment StaffErrorFragment on StaffError {
code
field
}
`;
export const staffFragmentError = gql`
fragment StaffErrorFragment on StaffError {
code
field
}
`;
const staffMemberAddMutation = gql`
${staffErrorFragment}
${staffMemberDetailsFragment}
@ -52,7 +59,7 @@ export const TypedStaffMemberAddMutation = TypedMutation<
>(staffMemberAddMutation);
const staffMemberUpdateMutation = gql`
${accountFragmentError}
${staffErrorFragment}
${staffMemberDetailsFragment}
mutation StaffMemberUpdate($id: ID!, $input: StaffUpdateInput!) {
staffUpdate(id: $id, input: $input) {
@ -86,7 +93,7 @@ export const TypedStaffMemberDeleteMutation = TypedMutation<
>(staffMemberDeleteMutation);
const staffAvatarUpdateMutation = gql`
${accountFragmentError}
${accountErrorFragment}
mutation StaffAvatarUpdate($image: Upload!) {
userAvatarUpdate(image: $image) {
errors: accountErrors {
@ -107,7 +114,7 @@ export const TypedStaffAvatarUpdateMutation = TypedMutation<
>(staffAvatarUpdateMutation);
const staffAvatarDeleteMutation = gql`
${accountFragmentError}
${accountErrorFragment}
mutation StaffAvatarDelete {
userAvatarDelete {
errors: accountErrors {
@ -128,7 +135,7 @@ export const TypedStaffAvatarDeleteMutation = TypedMutation<
>(staffAvatarDeleteMutation);
const changeStaffPassword = gql`
${accountFragmentError}
${accountErrorFragment}
mutation ChangeStaffPassword($newPassword: String!, $oldPassword: String!) {
passwordChange(newPassword: $newPassword, oldPassword: $oldPassword) {
errors: accountErrors {

View file

@ -23,7 +23,12 @@ export const staffMemberDetailsFragment = gql`
${staffMemberFragment}
fragment StaffMemberDetailsFragment on User {
...StaffMemberFragment
permissions {
permissionGroups {
id
name
userCanManage
}
userPermissions {
code
name
}

View file

@ -19,8 +19,15 @@ export interface StaffMemberAdd_staffCreate_user_avatar {
url: string;
}
export interface StaffMemberAdd_staffCreate_user_permissions {
__typename: "Permission";
export interface StaffMemberAdd_staffCreate_user_permissionGroups {
__typename: "Group";
id: string;
name: string;
userCanManage: boolean;
}
export interface StaffMemberAdd_staffCreate_user_userPermissions {
__typename: "UserPermission";
code: PermissionEnum;
name: string;
}
@ -33,7 +40,8 @@ export interface StaffMemberAdd_staffCreate_user {
isActive: boolean;
lastName: string;
avatar: StaffMemberAdd_staffCreate_user_avatar | null;
permissions: (StaffMemberAdd_staffCreate_user_permissions | null)[] | null;
permissionGroups: (StaffMemberAdd_staffCreate_user_permissionGroups | null)[] | null;
userPermissions: (StaffMemberAdd_staffCreate_user_userPermissions | null)[] | null;
}
export interface StaffMemberAdd_staffCreate {

View file

@ -13,8 +13,15 @@ export interface StaffMemberDetails_user_avatar {
url: string;
}
export interface StaffMemberDetails_user_permissions {
__typename: "Permission";
export interface StaffMemberDetails_user_permissionGroups {
__typename: "Group";
id: string;
name: string;
userCanManage: boolean;
}
export interface StaffMemberDetails_user_userPermissions {
__typename: "UserPermission";
code: PermissionEnum;
name: string;
}
@ -27,7 +34,8 @@ export interface StaffMemberDetails_user {
isActive: boolean;
lastName: string;
avatar: StaffMemberDetails_user_avatar | null;
permissions: (StaffMemberDetails_user_permissions | null)[] | null;
permissionGroups: (StaffMemberDetails_user_permissionGroups | null)[] | null;
userPermissions: (StaffMemberDetails_user_userPermissions | null)[] | null;
}
export interface StaffMemberDetails {

View file

@ -13,8 +13,15 @@ export interface StaffMemberDetailsFragment_avatar {
url: string;
}
export interface StaffMemberDetailsFragment_permissions {
__typename: "Permission";
export interface StaffMemberDetailsFragment_permissionGroups {
__typename: "Group";
id: string;
name: string;
userCanManage: boolean;
}
export interface StaffMemberDetailsFragment_userPermissions {
__typename: "UserPermission";
code: PermissionEnum;
name: string;
}
@ -27,5 +34,6 @@ export interface StaffMemberDetailsFragment {
isActive: boolean;
lastName: string;
avatar: StaffMemberDetailsFragment_avatar | null;
permissions: (StaffMemberDetailsFragment_permissions | null)[] | null;
permissionGroups: (StaffMemberDetailsFragment_permissionGroups | null)[] | null;
userPermissions: (StaffMemberDetailsFragment_userPermissions | null)[] | null;
}

Some files were not shown because too many files have changed in this diff Show more