Assign channel to permission group (#3515)

This commit is contained in:
Paweł Chyła 2023-08-04 12:42:04 +02:00 committed by GitHub
parent eba9ee6603
commit a63af3ab73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 2121 additions and 528 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-dashboard": minor
---
Assign channel permission to permission group

View file

@ -15,6 +15,7 @@ export const MockedUserProvider: React.FC<{
requestLoginByExternalPlugin: undefined, requestLoginByExternalPlugin: undefined,
authenticating: false, authenticating: false,
authenticated: false, authenticated: false,
refetchUser: undefined,
user: { user: {
id: "0", id: "0",
email: "email@email.me", email: "email@email.me",
@ -24,6 +25,8 @@ export const MockedUserProvider: React.FC<{
userPermissions: customPermissions ?? adminUserPermissions, userPermissions: customPermissions ?? adminUserPermissions,
avatar: null, avatar: null,
__typename: "User", __typename: "User",
accessibleChannels: [],
restrictedAccessToChannels: false,
}, },
errors: [], errors: [],
}} }}

View file

@ -130685,10 +130685,7 @@
"name": "deprecated", "name": "deprecated",
"description": "Marks an element of a GraphQL schema as no longer supported.", "description": "Marks an element of a GraphQL schema as no longer supported.",
"isRepeatable": false, "isRepeatable": false,
"locations": [ "locations": ["ENUM_VALUE", "FIELD_DEFINITION"],
"ENUM_VALUE",
"FIELD_DEFINITION"
],
"args": [ "args": [
{ {
"name": "reason", "name": "reason",
@ -130738,11 +130735,7 @@
"name": "include", "name": "include",
"description": "Directs the executor to include this field or fragment only when the `if` argument is true.", "description": "Directs the executor to include this field or fragment only when the `if` argument is true.",
"isRepeatable": false, "isRepeatable": false,
"locations": [ "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"],
"FIELD",
"FRAGMENT_SPREAD",
"INLINE_FRAGMENT"
],
"args": [ "args": [
{ {
"name": "if", "name": "if",
@ -130766,11 +130759,7 @@
"name": "skip", "name": "skip",
"description": "Directs the executor to skip this field or fragment when the `if` argument is true.", "description": "Directs the executor to skip this field or fragment when the `if` argument is true.",
"isRepeatable": false, "isRepeatable": false,
"locations": [ "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"],
"FIELD",
"FRAGMENT_SPREAD",
"INLINE_FRAGMENT"
],
"args": [ "args": [
{ {
"name": "if", "name": "if",
@ -130794,12 +130783,7 @@
"name": "webhookEventsInfo", "name": "webhookEventsInfo",
"description": "Webhook events triggered by a specific location.", "description": "Webhook events triggered by a specific location.",
"isRepeatable": false, "isRepeatable": false,
"locations": [ "locations": ["FIELD", "FIELD_DEFINITION", "INPUT_OBJECT", "OBJECT"],
"FIELD",
"FIELD_DEFINITION",
"INPUT_OBJECT",
"OBJECT"
],
"args": [ "args": [
{ {
"name": "asyncEvents", "name": "asyncEvents",

View file

@ -349,6 +349,9 @@
"context": "min price in channel", "context": "min price in channel",
"string": "Min. value" "string": "Min. value"
}, },
"0HBlkO": {
"string": "Search channels"
},
"0KmZCN": { "0KmZCN": {
"context": "button", "context": "button",
"string": "Open playground" "string": "Open playground"
@ -1509,6 +1512,9 @@
"context": "modal button images upload", "context": "modal button images upload",
"string": "Upload Images" "string": "Upload Images"
}, },
"9FGTOt": {
"string": "Allow access to orders of all channels"
},
"9IWg/f": { "9IWg/f": {
"context": "button", "context": "button",
"string": "SETUP END DATE" "string": "SETUP END DATE"
@ -6248,6 +6254,9 @@
"context": "page header", "context": "page header",
"string": "Create Page" "string": "Create Page"
}, },
"grkY2V": {
"string": "You don't have access to any channels"
},
"gvOzOl": { "gvOzOl": {
"string": "Page Title" "string": "Page Title"
}, },
@ -6979,10 +6988,6 @@
"context": "add authorization key error", "context": "add authorization key error",
"string": "Authorization key with this type already exists" "string": "Authorization key with this type already exists"
}, },
"mAabef": {
"context": "checkbox label",
"string": "Group has full access to the store"
},
"mCP0UD": { "mCP0UD": {
"context": "order draft creation date", "context": "order draft creation date",
"string": "Date" "string": "Date"
@ -8323,10 +8328,16 @@
"context": "button", "context": "button",
"string": "Create category" "string": "Create category"
}, },
"vprU7C": {
"string": "Select visible order channels"
},
"vwMO04": { "vwMO04": {
"context": "draft order", "context": "draft order",
"string": "Created" "string": "Created"
}, },
"vz3yxp": {
"string": "Channels permissions"
},
"vzce9B": { "vzce9B": {
"context": "customer gift cards card subtitle", "context": "customer gift cards card subtitle",
"string": "Only five newest gift cards are shown here" "string": "Only five newest gift cards are shown here"

View file

@ -245,6 +245,7 @@ export function useAuthProvider({
authenticating: authenticating && !errors.length, authenticating: authenticating && !errors.length,
authenticated: authenticated && !!user?.isStaff && !errors.length, authenticated: authenticated && !!user?.isStaff && !errors.length,
user: userDetails.data?.me, user: userDetails.data?.me,
refetchUser: userDetails.refetch,
errors, errors,
}; };
} }

View file

@ -0,0 +1,11 @@
import { useUser } from "..";
export const useUserAccessibleChannels = () => {
const user = useUser();
if (!user?.user?.accessibleChannels) {
return [];
}
return user.user.accessibleChannels;
};

View file

@ -31,6 +31,7 @@ export const UserContext = React.createContext<Context>({
authenticating: false, authenticating: false,
authenticated: false, authenticated: false,
errors: [], errors: [],
refetchUser: undefined,
}); });
const AuthRouter: React.FC = () => ( const AuthRouter: React.FC = () => (

View file

@ -1,4 +1,5 @@
import { UserFragment } from "@dashboard/graphql"; import { ApolloQueryResult } from "@apollo/client";
import { UserDetailsQuery, UserFragment } from "@dashboard/graphql";
import { import {
GetExternalAccessTokenData, GetExternalAccessTokenData,
GetExternalAuthUrlData, GetExternalAuthUrlData,
@ -44,4 +45,5 @@ export interface UserContext {
authenticating: boolean; authenticating: boolean;
authenticated: boolean; authenticated: boolean;
errors: UserContextError[]; errors: UserContextError[];
refetchUser: () => Promise<ApolloQueryResult<UserDetailsQuery>>;
} }

View file

@ -1,42 +1,13 @@
// @ts-strict-ignore
import { useUser } from "@dashboard/auth"; import { useUser } from "@dashboard/auth";
import CardTitle from "@dashboard/components/CardTitle"; import { PermissionData } from "@dashboard/permissionGroups/components/PermissionGroupDetailsPage";
import Skeleton from "@dashboard/components/Skeleton"; import { Box, Text } from "@saleor/macaw-ui/next";
import { PermissionData } from "@dashboard/permissionGroups/components/PermissionGroupDetailsPage/PermissionGroupDetailsPage"; import React, { ChangeEvent } from "react";
import {
Card,
CardContent,
Checkbox,
List,
ListItem,
ListItemIcon,
ListItemText,
Typography,
} from "@material-ui/core";
import { makeStyles } from "@saleor/macaw-ui";
import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
const byAlphabeticalOrder = import { Header } from "./components/Header";
<T extends {}>(field: string) => import { PermissionsExceeded } from "./components/PermissionExeeded";
(a: T, b: T) => import { PermissionList } from "./components/PermissionList";
a[field].localeCompare(b[field]); import { messages } from "./messages";
const useStyles = makeStyles(
theme => ({
checkboxContainer: {
marginTop: theme.spacing(),
},
hr: {
backgroundColor: theme.palette.divider,
border: "none",
height: 1,
marginBottom: 0,
marginTop: 0,
},
}),
{ name: "AccountPermissions" },
);
interface AccountPermissionsProps { interface AccountPermissionsProps {
permissions: PermissionData[]; permissions: PermissionData[];
@ -47,7 +18,7 @@ interface AccountPermissionsProps {
}; };
disabled: boolean; disabled: boolean;
description: string; description: string;
errorMessage: string; errorMessage: string | undefined;
fullAccessLabel: string; fullAccessLabel: string;
onChange: (event: React.ChangeEvent<any>, cb?: () => void) => void; onChange: (event: React.ChangeEvent<any>, cb?: () => void) => void;
} }
@ -63,11 +34,10 @@ const AccountPermissions: React.FC<AccountPermissionsProps> = props => {
errorMessage, errorMessage,
} = props; } = props;
const permissions = Object.values(props?.permissions ?? {}).sort( const permissions = Object.values(props?.permissions ?? {}).sort((a, b) =>
byAlphabeticalOrder("name"), a.name.localeCompare(b.name),
); );
const classes = useStyles(props);
const intl = useIntl(); const intl = useIntl();
const { user } = useUser(); const { user } = useUser();
@ -75,161 +45,99 @@ const AccountPermissions: React.FC<AccountPermissionsProps> = props => {
onChange({ onChange({
target: { target: {
name: "permissions", name: "permissions",
value: !data.hasFullAccess ? permissions.map(perm => perm.code) : [], value: !data.hasFullAccess
? permissions.filter(perm => !perm.disabled).map(perm => perm.code)
: [],
}, },
} as any); } as ChangeEvent<any>);
onChange({ onChange({
target: { target: {
name: "hasFullAccess", name: "hasFullAccess",
value: !data.hasFullAccess, value: !data.hasFullAccess,
}, },
} as any); } as ChangeEvent<any>);
}; };
const handlePermissionChange = (key, value) => () => {
const handlePermissionChange = (key: string, value: boolean) => {
const updatedPersmissions = !value
? data.permissions.concat([key])
: data.permissions.filter(perm => perm !== key);
// If all permissions are selected, set hasFullAccess to true
onChange({
target: {
name: "hasFullAccess",
value: !!(
permissions.length === updatedPersmissions.length &&
!data.hasFullAccess
),
},
} as ChangeEvent<any>);
onChange({ onChange({
target: { target: {
name: "permissions", name: "permissions",
value: !value value: updatedPersmissions,
? data.permissions.concat([key])
: data.permissions.filter(perm => perm !== key),
}, },
} as any); } as ChangeEvent<any>);
}; };
return ( return (
<Card> <Box paddingX={9} paddingY={9} paddingBottom={0}>
<CardTitle <Text as="p" variant="bodyEmp" size="large" marginBottom={7}>
title={intl.formatMessage({ {intl.formatMessage(messages.title)}
id: "Fbr4Vp", </Text>
defaultMessage: "Permissions",
description: "dialog header",
})}
/>
{permissionsExceeded && ( {permissionsExceeded && (
<> <PermissionsExceeded userPermissions={user?.userPermissions ?? []} />
<CardContent style={{ paddingLeft: 0 }}>
<Typography variant="body2">
{intl.formatMessage({
id: "MVU6ol",
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>
<Typography variant="body2">
{intl.formatMessage({
id: "6cS4Rd",
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 && ( {!permissionsExceeded && (
<> <>
<CardContent> <Header
<Typography variant="body2">{description}</Typography>
<ListItem
role={undefined}
dense
button
onClick={handleFullAccessChange}
>
<ListItemIcon>
<Checkbox
data-test-id="full-access"
color="secondary"
edge="start"
checked={data.hasFullAccess}
disabled={disabled} disabled={disabled}
tabIndex={-1} description={description}
disableRipple fullAccessLabel={fullAccessLabel}
inputProps={{ "aria-labelledby": "fullAccess" }} hasFullAccess={data.hasFullAccess}
onFullAccessChange={handleFullAccessChange}
/> />
</ListItemIcon>
<ListItemText primary={fullAccessLabel} /> <Box
</ListItem> width="100%"
</CardContent> borderBottomStyle="solid"
{!data.hasFullAccess && ( borderBottomWidth={1}
<> borderColor="neutralPlain"
<hr className={classes.hr} /> height={1}
<CardContent> margin={0}
{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
color="secondary"
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 <PermissionList
id={perm.code} disabled={disabled}
primary={perm.name.replace(/\./, "")} permissions={permissions}
secondary={ onPermissionChange={handlePermissionChange}
perm.lastSource selectedPermissions={data.permissions}
? intl.formatMessage({
id: "VmMDLN",
defaultMessage:
"This group is last source of that permission",
description: "permission list item description",
})
: perm.code
}
/> />
</ListItem>
))
)}
</CardContent>
</>
)}
{!!errorMessage && ( {!!errorMessage && (
<> <>
<hr className={classes.hr} /> <Box
<CardContent> width="100%"
<Typography variant="body2" color="error"> borderBottomStyle="solid"
borderBottomWidth={1}
borderColor="neutralPlain"
height={1}
marginTop={6}
marginBottom={6}
/>
<Text as="p" variant="body" color="textCriticalSubdued">
{errorMessage} {errorMessage}
</Typography> </Text>
</CardContent>
</> </>
)} )}
</> </>
)} )}
</Card> </Box>
); );
}; };

View file

@ -0,0 +1,32 @@
import { Box, Checkbox, CheckedState, Text } from "@saleor/macaw-ui/next";
import React from "react";
interface HeaderProps {
description: string;
fullAccessLabel: string;
disabled: boolean;
hasFullAccess: boolean;
onFullAccessChange: (checked: CheckedState) => void;
}
export const Header = ({
description,
disabled,
hasFullAccess,
fullAccessLabel,
onFullAccessChange,
}: HeaderProps) => (
<Box>
<Text variant="body">{description}</Text>
<Box marginTop={6} marginBottom={7}>
<Checkbox
disabled={disabled}
checked={hasFullAccess}
onCheckedChange={onFullAccessChange}
tabIndex={-1}
>
<Text variant="body">{fullAccessLabel}</Text>
</Checkbox>
</Box>
</Box>
);

View file

@ -0,0 +1 @@
export * from "./Header";

View file

@ -0,0 +1,54 @@
import { UserPermissionFragment } from "@dashboard/graphql";
import { Box, List, Text } from "@saleor/macaw-ui/next";
import React from "react";
import { useIntl } from "react-intl";
import { messages } from "../../messages";
interface PermissionsExceededProps {
userPermissions: UserPermissionFragment[];
}
export const PermissionsExceeded = ({
userPermissions,
}: PermissionsExceededProps) => {
const intl = useIntl();
return (
<>
<Text as="p" variant="body">
{intl.formatMessage(messages.exeededPermission)}
</Text>
<Box
width="100%"
borderBottomStyle="solid"
borderBottomWidth={1}
borderColor="neutralPlain"
height={1}
marginTop={6}
marginBottom={6}
/>
<Text variant="body">
{intl.formatMessage(messages.availablePermissions)}
</Text>
<List>
{userPermissions.map(perm => (
<List.Item
key={perm.code}
marginY={4}
marginLeft={4}
cursor="text"
backgroundColor={{
hover: "interactiveNeutralHighlightDefault",
}}
>
<Text variant="caption" size="large">{`- ${perm.name}`}</Text>
</List.Item>
))}
</List>
</>
);
};

View file

@ -0,0 +1 @@
export * from "./PermissionExeeded";

View file

@ -0,0 +1,79 @@
import Skeleton from "@dashboard/components/Skeleton";
import { PermissionData } from "@dashboard/permissionGroups/components/PermissionGroupDetailsPage";
import {
Checkbox,
ListItem,
ListItemIcon,
ListItemText,
} from "@material-ui/core";
import { Box } from "@saleor/macaw-ui/next";
import React from "react";
import { useIntl } from "react-intl";
import { messages } from "../../messages";
import { hasPermissionSelected } from "../../utils";
interface PermissionListProps {
permissions: PermissionData[];
selectedPermissions: string[];
disabled?: boolean;
onPermissionChange: (key: string, value: boolean) => void;
}
export const PermissionList = ({
permissions,
onPermissionChange,
selectedPermissions,
disabled,
}: PermissionListProps) => {
const intl = useIntl();
if (permissions === undefined) {
return (
<Box>
<Skeleton />
</Box>
);
}
return (
<Box>
{permissions.map(perm => (
<ListItem
key={perm.code}
disabled={disabled || perm.disabled}
role={undefined}
dense
button
onClick={() =>
onPermissionChange(
perm.code,
hasPermissionSelected(selectedPermissions, perm.code),
)
}
>
<ListItemIcon>
<Checkbox
color="secondary"
edge="start"
checked={hasPermissionSelected(selectedPermissions, perm.code)}
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(messages.permissionListItemDescipription)
: perm.code
}
/>
</ListItem>
))}
</Box>
);
};

View file

@ -0,0 +1 @@
export * from "./PermissionList";

View file

@ -0,0 +1,25 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
title: {
id: "Fbr4Vp",
defaultMessage: "Permissions",
description: "dialog header",
},
exeededPermission: {
id: "MVU6ol",
defaultMessage:
"This groups permissions exceeds your own. You are able only to manage permissions that you have.",
description: "exceeded permissions description",
},
availablePermissions: {
id: "6cS4Rd",
defaultMessage: "Available permissions",
description: "card section description",
},
permissionListItemDescipription: {
id: "VmMDLN",
defaultMessage: "This group is last source of that permission",
description: "permission list item description",
},
});

View file

@ -1,25 +1,27 @@
// @ts-strict-ignore
import { import {
PermissionGroupDetailsQuery, PermissionEnum,
ShopInfoQuery, PermissionFragment,
UserDetailsQuery, UserPermissionFragment,
UserUserPermissionWithSourcePermissionGroupsFragment,
} from "@dashboard/graphql"; } from "@dashboard/graphql";
export const getLastSourcesOfPermission = ( export const getLastSourcesOfPermission = (
groupId: string, groupId: string,
userPermissions: PermissionGroupDetailsQuery["user"]["userPermissions"], userPermissions: Array<
NonNullable<UserUserPermissionWithSourcePermissionGroupsFragment>
>,
) => ) =>
userPermissions userPermissions
.filter( .filter(
perm => perm =>
perm.sourcePermissionGroups.length === 1 && perm.sourcePermissionGroups?.length === 1 &&
perm.sourcePermissionGroups[0].id === groupId, perm.sourcePermissionGroups[0]?.id === groupId,
) )
.map(perm => perm.code); .map(perm => perm.code);
export const getPermissionsComponentChoices = ( export const getPermissionsComponentChoices = (
userPermissions: UserDetailsQuery["me"]["userPermissions"], userPermissions: UserPermissionFragment[],
shopPermissions: ShopInfoQuery["shop"]["permissions"], shopPermissions: PermissionFragment[],
lastSourcesOfPermissionIds: string[], lastSourcesOfPermissionIds: string[],
) => { ) => {
const userCodes = userPermissions.map(p => p.code) || []; const userCodes = userPermissions.map(p => p.code) || [];
@ -31,3 +33,8 @@ export const getPermissionsComponentChoices = (
lastSource: lastSourcesOfPermissionIds.includes(perm.code), lastSource: lastSourcesOfPermissionIds.includes(perm.code),
})); }));
}; };
export const hasPermissionSelected = (
permissions: string[],
permissionCode: PermissionEnum,
) => permissions.filter(userPerm => userPerm === permissionCode).length === 1;

View file

@ -44,13 +44,17 @@ export const AppChannelProvider: React.FC = ({ children }) => {
const [isPickerActive, setPickerActive] = React.useState(false); const [isPickerActive, setPickerActive] = React.useState(false);
React.useEffect(() => { React.useEffect(() => {
if ( const channels = user?.accessibleChannels ?? [];
!isValidChannel(selectedChannel, channelData?.channels) && const isValid = isValidChannel(selectedChannel, channels);
channelData?.channels?.length > 0
) { if (!isValid && channels?.length > 0) {
setSelectedChannel(channelData.channels[0].id); setSelectedChannel(channels[0].id);
} }
}, [channelData]);
if (!isValid && selectedChannel !== "") {
setSelectedChannel("");
}
}, [selectedChannel, setSelectedChannel, user]);
React.useEffect(() => { React.useEffect(() => {
setChannel(selectedChannel); setChannel(selectedChannel);

View file

@ -1,3 +1,4 @@
import { useUser } from "@dashboard/auth";
import { Box, Text } from "@saleor/macaw-ui/next"; import { Box, Text } from "@saleor/macaw-ui/next";
import React, { PropsWithChildren } from "react"; import React, { PropsWithChildren } from "react";
@ -20,8 +21,9 @@ export const Root: React.FC<PropsWithChildren<TopNavProps>> = ({
isAlignToRight = true, isAlignToRight = true,
children, children,
}) => { }) => {
const { availableChannels, channel, isPickerActive, setChannel } = const { channel, isPickerActive, setChannel } = useAppChannel(false);
useAppChannel(false); const user = useUser();
const channels = user?.user?.accessibleChannels ?? [];
return ( return (
<TopNavWrapper withoutBorder={withoutBorder}> <TopNavWrapper withoutBorder={withoutBorder}>
@ -37,9 +39,9 @@ export const Root: React.FC<PropsWithChildren<TopNavProps>> = ({
height="100%" height="100%"
__flex={isAlignToRight ? "initial" : 1} __flex={isAlignToRight ? "initial" : 1}
> >
{isPickerActive && ( {isPickerActive && channels.length > 0 && (
<AppChannelSelect <AppChannelSelect
channels={availableChannels} channels={channels}
selectedChannelId={channel?.id} selectedChannelId={channel?.id}
onChannelSelect={setChannel} onChannelSelect={setChannel}
/> />

View file

@ -0,0 +1,135 @@
import {
act,
render,
screen,
waitForElementToBeRemoved,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React from "react";
import { ChannelPermission } from "./ChannelPermission";
import { allChannels } from "./fixtures";
jest.mock("react-intl", () => ({
useIntl: jest.fn(() => ({
formatMessage: jest.fn(x => x.defaultMessage),
})),
defineMessages: jest.fn(x => x),
FormattedMessage: ({ defaultMessage }: { defaultMessage: string }) => (
<>{defaultMessage}</>
),
}));
describe("ChannelPermission", () => {
it("should render by default header and checkbox", () => {
// Arrange & Act
render(
<ChannelPermission
selectedChannels={[]}
allChannels={allChannels}
disabled={false}
disabledSelectAllChannels={false}
onChannelChange={jest.fn}
onHasAllChannelsChange={jest.fn}
hasAllChannels={true}
/>,
);
// Assert
expect(screen.getByText(/channels permissions/i)).toBeInTheDocument();
expect(screen.getByRole("checkbox")).toBeInTheDocument();
expect(screen.getByRole("checkbox")).toBeChecked();
expect(
screen.getByText(/allow access to orders of all channels/i),
).toBeInTheDocument();
});
it("should render channels select when access to all channels checkbox unchecked", () => {
// Arrange & Act
render(
<ChannelPermission
selectedChannels={[]}
allChannels={allChannels}
disabled={false}
disabledSelectAllChannels={false}
onChannelChange={jest.fn}
onHasAllChannelsChange={jest.fn}
hasAllChannels={false}
/>,
);
// Assert
expect(screen.getByRole("checkbox")).not.toBeChecked();
expect(screen.getByRole("listbox")).toBeInTheDocument();
});
it("should render restricted checkbox disabled", () => {
// Arrange & Act
const mockonHasAllChannelsChange = jest.fn();
render(
<ChannelPermission
selectedChannels={[]}
allChannels={allChannels}
disabled={true}
disabledSelectAllChannels={false}
onChannelChange={jest.fn}
onHasAllChannelsChange={mockonHasAllChannelsChange}
hasAllChannels={true}
/>,
);
// Act
userEvent.click(screen.getByRole("checkbox"));
// Assert
expect(mockonHasAllChannelsChange).not.toHaveBeenCalled();
expect(screen.getByRole("checkbox")).toBeDisabled();
});
it("should render selected channels when has restricted channels selected", () => {
// Arrange & Act
const selectedChannels = [allChannels[1]];
render(
<ChannelPermission
allChannels={allChannels}
selectedChannels={selectedChannels.map(chan => chan.id)}
disabled={false}
disabledSelectAllChannels={false}
onChannelChange={jest.fn}
onHasAllChannelsChange={jest.fn}
hasAllChannels={false}
/>,
);
// Assert
expect(screen.getByText(selectedChannels[0].name)).toBeInTheDocument();
});
it("should allow to remove selected channels", async () => {
// Arrange & Act
const selectedChannels = [allChannels[1]];
render(
<ChannelPermission
allChannels={allChannels}
selectedChannels={selectedChannels.map(x => x.id)}
disabled={false}
disabledSelectAllChannels={false}
onChannelChange={jest.fn}
onHasAllChannelsChange={jest.fn}
hasAllChannels={false}
/>,
);
// Assert
expect(screen.getByText(selectedChannels[0].name)).toBeInTheDocument();
// Act
act(() => {
userEvent.click(screen.getByText(/✕/i));
});
// Assert
waitForElementToBeRemoved(screen.getByText(selectedChannels[0].name));
});
});

View file

@ -0,0 +1,83 @@
import { ChannelFragment } from "@dashboard/graphql";
import { FormChange } from "@dashboard/hooks/useForm";
import { mapNodeToChoice } from "@dashboard/utils/maps";
import { Box, Checkbox, Multiselect, Text } from "@saleor/macaw-ui/next";
import React from "react";
import { useIntl } from "react-intl";
import { messages } from "./messages";
interface ChannelPermissionProps {
selectedChannels: string[];
allChannels: ChannelFragment[];
description?: string;
hasAllChannels: boolean;
disabled: boolean;
disabledSelectAllChannels: boolean;
onChannelChange: FormChange;
onHasAllChannelsChange: () => void;
}
export const ChannelPermission = ({
description,
disabled,
onHasAllChannelsChange,
onChannelChange,
allChannels,
selectedChannels,
hasAllChannels,
disabledSelectAllChannels,
}: ChannelPermissionProps) => {
const intl = useIntl();
const channelsChoices = mapNodeToChoice(allChannels);
return (
<Box height="100%">
<Text as="p" size="large" marginBottom={5}>
{intl.formatMessage(messages.title)}
</Text>
<Box height="100%">
{description && (
<Text as="p" variant="body" size="small" marginBottom={5}>
{description}
</Text>
)}
<Box __width="fit-content">
<Checkbox
disabled={disabled || disabledSelectAllChannels}
checked={hasAllChannels}
onCheckedChange={onHasAllChannelsChange}
tabIndex={-1}
>
<Text variant="body">
{intl.formatMessage(messages.allowAllChannels)}
</Text>
</Checkbox>
</Box>
{!hasAllChannels && (
<Box marginTop={5}>
<Multiselect
size="small"
data-test-id="channels"
disabled={disabled}
options={channelsChoices}
label={intl.formatMessage(messages.selectChannels)}
value={channelsChoices.filter(channel =>
selectedChannels.includes(channel.value),
)}
placeholder={intl.formatMessage(messages.searchChannels)}
onChange={values => {
onChannelChange({
target: { name: "channels", value: values.map(v => v.value) },
});
}}
/>
</Box>
)}
</Box>
</Box>
);
};

View file

@ -0,0 +1,72 @@
import { AllocationStrategyEnum, ChannelFragment } from "@dashboard/graphql";
export const allChannels: ChannelFragment[] = [
{
__typename: "Channel",
id: "Q2hhbm5lbDoy",
isActive: true,
name: "Channel-1",
slug: "channel-1",
currencyCode: "PLN",
defaultCountry: {
__typename: "CountryDisplay",
code: "PL",
country: "Poland",
},
stockSettings: {
__typename: "StockSettings",
allocationStrategy: AllocationStrategyEnum.PRIORITIZE_HIGH_STOCK,
},
},
{
__typename: "Channel",
id: "Q2hhbm5lbDoz",
isActive: true,
name: "Channel-2",
slug: "channel-2",
currencyCode: "PLN",
defaultCountry: {
__typename: "CountryDisplay",
code: "PL",
country: "Poland",
},
stockSettings: {
__typename: "StockSettings",
allocationStrategy: AllocationStrategyEnum.PRIORITIZE_HIGH_STOCK,
},
},
{
__typename: "Channel",
id: "Q2hhbm5lbDou",
isActive: true,
name: "Channel-3",
slug: "channel-3",
currencyCode: "PLN",
defaultCountry: {
__typename: "CountryDisplay",
code: "PL",
country: "Poland",
},
stockSettings: {
__typename: "StockSettings",
allocationStrategy: AllocationStrategyEnum.PRIORITIZE_HIGH_STOCK,
},
},
{
__typename: "Channel",
id: "Q2hhbm5lbDox",
isActive: true,
name: "Channel-4",
slug: "default-channel",
currencyCode: "USD",
defaultCountry: {
__typename: "CountryDisplay",
code: "US",
country: "United States of America",
},
stockSettings: {
__typename: "StockSettings",
allocationStrategy: AllocationStrategyEnum.PRIORITIZE_HIGH_STOCK,
},
},
];

View file

@ -0,0 +1 @@
export * from "./ChannelPermission";

View file

@ -0,0 +1,20 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
title: {
defaultMessage: "Channels permissions",
id: "vz3yxp",
},
allowAllChannels: {
defaultMessage: "Allow access to orders of all channels",
id: "9FGTOt",
},
selectChannels: {
defaultMessage: "Select visible order channels",
id: "vprU7C",
},
searchChannels: {
defaultMessage: "Search channels",
id: "0HBlkO",
},
});

View file

@ -27,8 +27,7 @@ export const shopInfo = gql`
name name
trackInventoryByDefault trackInventoryByDefault
permissions { permissions {
code ...Permission
name
} }
version version
} }

View file

@ -24,6 +24,8 @@ const user = {
lastName: "Newton", lastName: "Newton",
note: null, note: null,
userPermissions: staffMember.userPermissions, userPermissions: staffMember.userPermissions,
restrictedAccessToChannels: false,
accessibleChannels: [],
}; };
const versions = { const versions = {

View file

@ -1,5 +1,7 @@
// @ts-strict-ignore // @ts-strict-ignore
import { import {
AllocationStrategyEnum,
ChannelFragment,
PermissionEnum, PermissionEnum,
ShopInfoQuery, ShopInfoQuery,
ShopLimitFragment, ShopLimitFragment,
@ -355,6 +357,43 @@ export const sortPageProps: SortPage<string> = {
}, },
}; };
export const channels: ChannelFragment[] = [
{
id: "Q2hhbm5lbDoyMjQ0",
isActive: true,
name: "Channel-PLN",
slug: "channel-pln",
currencyCode: "PLN",
defaultCountry: {
code: "US",
country: "United States of America",
__typename: "CountryDisplay",
},
stockSettings: {
allocationStrategy: AllocationStrategyEnum.PRIORITIZE_HIGH_STOCK,
__typename: "StockSettings",
},
__typename: "Channel",
},
{
id: "Q2hhbm5lbDoyMjQz",
isActive: true,
name: "Channel-USD",
slug: "default-channel",
currencyCode: "USD",
defaultCountry: {
code: "US",
country: "United States of America",
__typename: "CountryDisplay",
},
stockSettings: {
allocationStrategy: AllocationStrategyEnum.PRIORITIZE_HIGH_STOCK,
__typename: "StockSettings",
},
__typename: "Channel",
},
];
export const permissions: ShopInfoQuery["shop"]["permissions"] = [ export const permissions: ShopInfoQuery["shop"]["permissions"] = [
{ {
code: PermissionEnum.MANAGE_DISCOUNTS, code: PermissionEnum.MANAGE_DISCOUNTS,

View file

@ -7,6 +7,15 @@ export const userUserPermissionFragment = gql`
} }
`; `;
export const userUserPermissionWithSourcePermissionGroupsFragment = gql`
fragment UserUserPermissionWithSourcePermissionGroups on UserPermission {
...UserPermission
sourcePermissionGroups(userId: $userId) {
id
}
}
`;
export const fragmentUser = gql` export const fragmentUser = gql`
fragment User on User { fragment User on User {
id id
@ -17,9 +26,13 @@ export const fragmentUser = gql`
userPermissions { userPermissions {
...UserPermission ...UserPermission
} }
avatar { avatar(size: 128) {
url url
} }
accessibleChannels {
...Channel
}
restrictedAccessToChannels
} }
`; `;

View file

@ -32,6 +32,10 @@ export const permissionGroupMember = gql`
export const permissionGroupDetailsFragment = gql` export const permissionGroupDetailsFragment = gql`
fragment PermissionGroupDetails on Group { fragment PermissionGroupDetails on Group {
...PermissionGroup ...PermissionGroup
restrictedAccessToChannels
accessibleChannels {
...Channel
}
permissions { permissions {
...Permission ...Permission
} }

View file

@ -171,6 +171,30 @@ export const UserPermissionFragmentDoc = gql`
name name
} }
`; `;
export const UserUserPermissionWithSourcePermissionGroupsFragmentDoc = gql`
fragment UserUserPermissionWithSourcePermissionGroups on UserPermission {
...UserPermission
sourcePermissionGroups(userId: $userId) {
id
}
}
${UserPermissionFragmentDoc}`;
export const ChannelFragmentDoc = gql`
fragment Channel on Channel {
id
isActive
name
slug
currencyCode
defaultCountry {
code
country
}
stockSettings {
allocationStrategy
}
}
`;
export const UserFragmentDoc = gql` export const UserFragmentDoc = gql`
fragment User on User { fragment User on User {
id id
@ -181,11 +205,16 @@ export const UserFragmentDoc = gql`
userPermissions { userPermissions {
...UserPermission ...UserPermission
} }
avatar { avatar(size: 128) {
url url
} }
accessibleChannels {
...Channel
} }
${UserPermissionFragmentDoc}`; restrictedAccessToChannels
}
${UserPermissionFragmentDoc}
${ChannelFragmentDoc}`;
export const CategoryFragmentDoc = gql` export const CategoryFragmentDoc = gql`
fragment Category on Category { fragment Category on Category {
id id
@ -223,22 +252,6 @@ export const ChannelErrorFragmentDoc = gql`
message message
} }
`; `;
export const ChannelFragmentDoc = gql`
fragment Channel on Channel {
id
isActive
name
slug
currencyCode
defaultCountry {
code
country
}
stockSettings {
allocationStrategy
}
}
`;
export const WarehouseFragmentDoc = gql` export const WarehouseFragmentDoc = gql`
fragment Warehouse on Warehouse { fragment Warehouse on Warehouse {
id id
@ -2117,6 +2130,10 @@ export const PermissionGroupMemberFragmentDoc = gql`
export const PermissionGroupDetailsFragmentDoc = gql` export const PermissionGroupDetailsFragmentDoc = gql`
fragment PermissionGroupDetails on Group { fragment PermissionGroupDetails on Group {
...PermissionGroup ...PermissionGroup
restrictedAccessToChannels
accessibleChannels {
...Channel
}
permissions { permissions {
...Permission ...Permission
} }
@ -2125,6 +2142,7 @@ export const PermissionGroupDetailsFragmentDoc = gql`
} }
} }
${PermissionGroupFragmentDoc} ${PermissionGroupFragmentDoc}
${ChannelFragmentDoc}
${PermissionFragmentDoc} ${PermissionFragmentDoc}
${PermissionGroupMemberFragmentDoc}`; ${PermissionGroupMemberFragmentDoc}`;
export const PluginConfigurationBaseFragmentDoc = gql` export const PluginConfigurationBaseFragmentDoc = gql`
@ -6138,14 +6156,14 @@ export const ShopInfoDocument = gql`
name name
trackInventoryByDefault trackInventoryByDefault
permissions { permissions {
code ...Permission
name
} }
version version
} }
} }
${CountryWithCodeFragmentDoc} ${CountryWithCodeFragmentDoc}
${LanguageFragmentDoc}`; ${LanguageFragmentDoc}
${PermissionFragmentDoc}`;
/** /**
* __useShopInfoQuery__ * __useShopInfoQuery__
@ -8618,20 +8636,20 @@ export type CustomerGiftCardListQueryHookResult = ReturnType<typeof useCustomerG
export type CustomerGiftCardListLazyQueryHookResult = ReturnType<typeof useCustomerGiftCardListLazyQuery>; export type CustomerGiftCardListLazyQueryHookResult = ReturnType<typeof useCustomerGiftCardListLazyQuery>;
export type CustomerGiftCardListQueryResult = Apollo.QueryResult<Types.CustomerGiftCardListQuery, Types.CustomerGiftCardListQueryVariables>; export type CustomerGiftCardListQueryResult = Apollo.QueryResult<Types.CustomerGiftCardListQuery, Types.CustomerGiftCardListQueryVariables>;
export const HomeDocument = gql` export const HomeDocument = gql`
query Home($channel: String!, $datePeriod: DateRangeInput!, $PERMISSION_MANAGE_PRODUCTS: Boolean!, $PERMISSION_MANAGE_ORDERS: Boolean!) { query Home($channel: String!, $datePeriod: DateRangeInput!, $hasPermissionToManageProducts: Boolean!, $hasPermissionToManageOrders: Boolean!) {
salesToday: ordersTotal(period: TODAY, channel: $channel) @include(if: $PERMISSION_MANAGE_ORDERS) { salesToday: ordersTotal(period: TODAY, channel: $channel) @include(if: $hasPermissionToManageOrders) {
gross { gross {
amount amount
currency currency
} }
} }
ordersToday: orders(filter: {created: $datePeriod}, channel: $channel) @include(if: $PERMISSION_MANAGE_ORDERS) { ordersToday: orders(filter: {created: $datePeriod}, channel: $channel) @include(if: $hasPermissionToManageOrders) {
totalCount totalCount
} }
ordersToFulfill: orders(filter: {status: READY_TO_FULFILL}, channel: $channel) @include(if: $PERMISSION_MANAGE_ORDERS) { ordersToFulfill: orders(filter: {status: READY_TO_FULFILL}, channel: $channel) @include(if: $hasPermissionToManageOrders) {
totalCount totalCount
} }
ordersToCapture: orders(filter: {status: READY_TO_CAPTURE}, channel: $channel) @include(if: $PERMISSION_MANAGE_ORDERS) { ordersToCapture: orders(filter: {status: READY_TO_CAPTURE}, channel: $channel) @include(if: $hasPermissionToManageOrders) {
totalCount totalCount
} }
productsOutOfStock: products( productsOutOfStock: products(
@ -8640,7 +8658,7 @@ export const HomeDocument = gql`
) { ) {
totalCount totalCount
} }
productTopToday: reportProductSales(period: TODAY, first: 5, channel: $channel) @include(if: $PERMISSION_MANAGE_PRODUCTS) { productTopToday: reportProductSales(period: TODAY, first: 5, channel: $channel) @include(if: $hasPermissionToManageProducts) {
edges { edges {
node { node {
id id
@ -8667,7 +8685,7 @@ export const HomeDocument = gql`
} }
} }
} }
activities: homepageEvents(last: 10) @include(if: $PERMISSION_MANAGE_ORDERS) { activities: homepageEvents(last: 10) @include(if: $hasPermissionToManageOrders) {
edges { edges {
node { node {
amount amount
@ -8705,8 +8723,8 @@ export const HomeDocument = gql`
* variables: { * variables: {
* channel: // value for 'channel' * channel: // value for 'channel'
* datePeriod: // value for 'datePeriod' * datePeriod: // value for 'datePeriod'
* PERMISSION_MANAGE_PRODUCTS: // value for 'PERMISSION_MANAGE_PRODUCTS' * hasPermissionToManageProducts: // value for 'hasPermissionToManageProducts'
* PERMISSION_MANAGE_ORDERS: // value for 'PERMISSION_MANAGE_ORDERS' * hasPermissionToManageOrders: // value for 'hasPermissionToManageOrders'
* }, * },
* }); * });
*/ */

View file

@ -8368,7 +8368,7 @@ export type AvailableExternalAuthenticationsQuery = { __typename: 'Query', shop:
export type UserDetailsQueryVariables = Exact<{ [key: string]: never; }>; export type UserDetailsQueryVariables = Exact<{ [key: string]: never; }>;
export type UserDetailsQuery = { __typename: 'Query', me: { __typename: 'User', id: string, email: string, firstName: string, lastName: string, isStaff: boolean, userPermissions: Array<{ __typename: 'UserPermission', code: PermissionEnum, name: string }> | null, avatar: { __typename: 'Image', url: string } | null } | null }; export type UserDetailsQuery = { __typename: 'Query', me: { __typename: 'User', id: string, email: string, firstName: string, lastName: string, isStaff: boolean, restrictedAccessToChannels: boolean, userPermissions: Array<{ __typename: 'UserPermission', code: PermissionEnum, name: string }> | null, avatar: { __typename: 'Image', url: string } | null, accessibleChannels: Array<{ __typename: 'Channel', id: string, isActive: boolean, name: string, slug: string, currencyCode: string, defaultCountry: { __typename: 'CountryDisplay', code: string, country: string }, stockSettings: { __typename: 'StockSettings', allocationStrategy: AllocationStrategyEnum } }> | null } | null };
export type CategoryDeleteMutationVariables = Exact<{ export type CategoryDeleteMutationVariables = Exact<{
id: Scalars['ID']; id: Scalars['ID'];
@ -9083,7 +9083,9 @@ export type AvailableAttributeFragment = { __typename: 'Attribute', id: string,
export type UserPermissionFragment = { __typename: 'UserPermission', code: PermissionEnum, name: string }; export type UserPermissionFragment = { __typename: 'UserPermission', code: PermissionEnum, name: string };
export type UserFragment = { __typename: 'User', id: string, email: string, firstName: string, lastName: string, isStaff: boolean, userPermissions: Array<{ __typename: 'UserPermission', code: PermissionEnum, name: string }> | null, avatar: { __typename: 'Image', url: string } | null }; export type UserUserPermissionWithSourcePermissionGroupsFragment = { __typename: 'UserPermission', code: PermissionEnum, name: string, sourcePermissionGroups: Array<{ __typename: 'Group', id: string }> | null };
export type UserFragment = { __typename: 'User', id: string, email: string, firstName: string, lastName: string, isStaff: boolean, restrictedAccessToChannels: boolean, userPermissions: Array<{ __typename: 'UserPermission', code: PermissionEnum, name: string }> | null, avatar: { __typename: 'Image', url: string } | null, accessibleChannels: Array<{ __typename: 'Channel', id: string, isActive: boolean, name: string, slug: string, currencyCode: string, defaultCountry: { __typename: 'CountryDisplay', code: string, country: string }, stockSettings: { __typename: 'StockSettings', allocationStrategy: AllocationStrategyEnum } }> | null };
export type UserBaseFragment = { __typename: 'User', id: string, firstName: string, lastName: string }; export type UserBaseFragment = { __typename: 'User', id: string, firstName: string, lastName: string };
@ -9405,7 +9407,7 @@ export type PermissionFragment = { __typename: 'Permission', code: PermissionEnu
export type PermissionGroupMemberFragment = { __typename: 'User', id: string, email: string, firstName: string, isActive: boolean, lastName: string, avatar: { __typename: 'Image', url: string } | null }; export type PermissionGroupMemberFragment = { __typename: 'User', id: string, email: string, firstName: string, isActive: boolean, lastName: string, avatar: { __typename: 'Image', url: string } | null };
export type PermissionGroupDetailsFragment = { __typename: 'Group', id: string, name: string, userCanManage: boolean, permissions: Array<{ __typename: 'Permission', code: PermissionEnum, name: string }> | null, users: Array<{ __typename: 'User', id: string, firstName: string, lastName: string, email: string, isActive: boolean, avatar: { __typename: 'Image', url: string } | null }> | null }; export type PermissionGroupDetailsFragment = { __typename: 'Group', restrictedAccessToChannels: boolean, id: string, name: string, userCanManage: boolean, accessibleChannels: Array<{ __typename: 'Channel', id: string, isActive: boolean, name: string, slug: string, currencyCode: string, defaultCountry: { __typename: 'CountryDisplay', code: string, country: string }, stockSettings: { __typename: 'StockSettings', allocationStrategy: AllocationStrategyEnum } }> | null, permissions: Array<{ __typename: 'Permission', code: PermissionEnum, name: string }> | null, users: Array<{ __typename: 'User', id: string, firstName: string, lastName: string, email: string, isActive: boolean, avatar: { __typename: 'Image', url: string } | null }> | null };
export type ConfigurationItemFragment = { __typename: 'ConfigurationItem', name: string, value: string | null, type: ConfigurationTypeFieldEnum | null, helpText: string | null, label: string | null }; export type ConfigurationItemFragment = { __typename: 'ConfigurationItem', name: string, value: string | null, type: ConfigurationTypeFieldEnum | null, helpText: string | null, label: string | null };
@ -9691,8 +9693,8 @@ export type CustomerGiftCardListQuery = { __typename: 'Query', giftCards: { __ty
export type HomeQueryVariables = Exact<{ export type HomeQueryVariables = Exact<{
channel: Scalars['String']; channel: Scalars['String'];
datePeriod: DateRangeInput; datePeriod: DateRangeInput;
PERMISSION_MANAGE_PRODUCTS: Scalars['Boolean']; hasPermissionToManageProducts: Scalars['Boolean'];
PERMISSION_MANAGE_ORDERS: Scalars['Boolean']; hasPermissionToManageOrders: Scalars['Boolean'];
}>; }>;
@ -10312,7 +10314,7 @@ export type PermissionGroupCreateMutationVariables = Exact<{
}>; }>;
export type PermissionGroupCreateMutation = { __typename: 'Mutation', permissionGroupCreate: { __typename: 'PermissionGroupCreate', errors: Array<{ __typename: 'PermissionGroupError', code: PermissionGroupErrorCode, field: string | null, message: string | null }>, group: { __typename: 'Group', id: string, name: string, userCanManage: boolean, permissions: Array<{ __typename: 'Permission', code: PermissionEnum, name: string }> | null, users: Array<{ __typename: 'User', id: string, firstName: string, lastName: string, email: string, isActive: boolean, avatar: { __typename: 'Image', url: string } | null }> | null } | null } | null }; export type PermissionGroupCreateMutation = { __typename: 'Mutation', permissionGroupCreate: { __typename: 'PermissionGroupCreate', errors: Array<{ __typename: 'PermissionGroupError', code: PermissionGroupErrorCode, field: string | null, message: string | null }>, group: { __typename: 'Group', restrictedAccessToChannels: boolean, id: string, name: string, userCanManage: boolean, accessibleChannels: Array<{ __typename: 'Channel', id: string, isActive: boolean, name: string, slug: string, currencyCode: string, defaultCountry: { __typename: 'CountryDisplay', code: string, country: string }, stockSettings: { __typename: 'StockSettings', allocationStrategy: AllocationStrategyEnum } }> | null, permissions: Array<{ __typename: 'Permission', code: PermissionEnum, name: string }> | null, users: Array<{ __typename: 'User', id: string, firstName: string, lastName: string, email: string, isActive: boolean, avatar: { __typename: 'Image', url: string } | null }> | null } | null } | null };
export type PermissionGroupUpdateMutationVariables = Exact<{ export type PermissionGroupUpdateMutationVariables = Exact<{
id: Scalars['ID']; id: Scalars['ID'];
@ -10320,7 +10322,7 @@ export type PermissionGroupUpdateMutationVariables = Exact<{
}>; }>;
export type PermissionGroupUpdateMutation = { __typename: 'Mutation', permissionGroupUpdate: { __typename: 'PermissionGroupUpdate', errors: Array<{ __typename: 'PermissionGroupError', code: PermissionGroupErrorCode, field: string | null, message: string | null }>, group: { __typename: 'Group', id: string, name: string, userCanManage: boolean, permissions: Array<{ __typename: 'Permission', code: PermissionEnum, name: string }> | null, users: Array<{ __typename: 'User', id: string, firstName: string, lastName: string, email: string, isActive: boolean, avatar: { __typename: 'Image', url: string } | null }> | null } | null } | null }; export type PermissionGroupUpdateMutation = { __typename: 'Mutation', permissionGroupUpdate: { __typename: 'PermissionGroupUpdate', errors: Array<{ __typename: 'PermissionGroupError', code: PermissionGroupErrorCode, field: string | null, message: string | null }>, group: { __typename: 'Group', restrictedAccessToChannels: boolean, id: string, name: string, userCanManage: boolean, accessibleChannels: Array<{ __typename: 'Channel', id: string, isActive: boolean, name: string, slug: string, currencyCode: string, defaultCountry: { __typename: 'CountryDisplay', code: string, country: string }, stockSettings: { __typename: 'StockSettings', allocationStrategy: AllocationStrategyEnum } }> | null, permissions: Array<{ __typename: 'Permission', code: PermissionEnum, name: string }> | null, users: Array<{ __typename: 'User', id: string, firstName: string, lastName: string, email: string, isActive: boolean, avatar: { __typename: 'Image', url: string } | null }> | null } | null } | null };
export type PermissionGroupListQueryVariables = Exact<{ export type PermissionGroupListQueryVariables = Exact<{
after?: InputMaybe<Scalars['String']>; after?: InputMaybe<Scalars['String']>;
@ -10340,7 +10342,7 @@ export type PermissionGroupDetailsQueryVariables = Exact<{
}>; }>;
export type PermissionGroupDetailsQuery = { __typename: 'Query', permissionGroup: { __typename: 'Group', id: string, name: string, userCanManage: boolean, permissions: Array<{ __typename: 'Permission', code: PermissionEnum, name: string }> | null, users: Array<{ __typename: 'User', id: string, firstName: string, lastName: string, email: string, isActive: boolean, avatar: { __typename: 'Image', url: string } | null }> | null } | null, user: { __typename: 'User', editableGroups: Array<{ __typename: 'Group', id: string }> | null, userPermissions: Array<{ __typename: 'UserPermission', code: PermissionEnum, sourcePermissionGroups: Array<{ __typename: 'Group', id: string }> | null }> | null } | null }; export type PermissionGroupDetailsQuery = { __typename: 'Query', permissionGroup: { __typename: 'Group', restrictedAccessToChannels: boolean, id: string, name: string, userCanManage: boolean, accessibleChannels: Array<{ __typename: 'Channel', id: string, isActive: boolean, name: string, slug: string, currencyCode: string, defaultCountry: { __typename: 'CountryDisplay', code: string, country: string }, stockSettings: { __typename: 'StockSettings', allocationStrategy: AllocationStrategyEnum } }> | null, permissions: Array<{ __typename: 'Permission', code: PermissionEnum, name: string }> | null, users: Array<{ __typename: 'User', id: string, firstName: string, lastName: string, email: string, isActive: boolean, avatar: { __typename: 'Image', url: string } | null }> | null } | null, user: { __typename: 'User', editableGroups: Array<{ __typename: 'Group', id: string }> | null, userPermissions: Array<{ __typename: 'UserPermission', code: PermissionEnum, sourcePermissionGroups: Array<{ __typename: 'Group', id: string }> | null }> | null } | null };
export type PluginUpdateMutationVariables = Exact<{ export type PluginUpdateMutationVariables = Exact<{
channelId?: InputMaybe<Scalars['ID']>; channelId?: InputMaybe<Scalars['ID']>;

View file

@ -4,30 +4,30 @@ export const home = gql`
query Home( query Home(
$channel: String! $channel: String!
$datePeriod: DateRangeInput! $datePeriod: DateRangeInput!
$PERMISSION_MANAGE_PRODUCTS: Boolean! $hasPermissionToManageProducts: Boolean!
$PERMISSION_MANAGE_ORDERS: Boolean! $hasPermissionToManageOrders: Boolean!
) { ) {
salesToday: ordersTotal(period: TODAY, channel: $channel) salesToday: ordersTotal(period: TODAY, channel: $channel)
@include(if: $PERMISSION_MANAGE_ORDERS) { @include(if: $hasPermissionToManageOrders) {
gross { gross {
amount amount
currency currency
} }
} }
ordersToday: orders(filter: { created: $datePeriod }, channel: $channel) ordersToday: orders(filter: { created: $datePeriod }, channel: $channel)
@include(if: $PERMISSION_MANAGE_ORDERS) { @include(if: $hasPermissionToManageOrders) {
totalCount totalCount
} }
ordersToFulfill: orders( ordersToFulfill: orders(
filter: { status: READY_TO_FULFILL } filter: { status: READY_TO_FULFILL }
channel: $channel channel: $channel
) @include(if: $PERMISSION_MANAGE_ORDERS) { ) @include(if: $hasPermissionToManageOrders) {
totalCount totalCount
} }
ordersToCapture: orders( ordersToCapture: orders(
filter: { status: READY_TO_CAPTURE } filter: { status: READY_TO_CAPTURE }
channel: $channel channel: $channel
) @include(if: $PERMISSION_MANAGE_ORDERS) { ) @include(if: $hasPermissionToManageOrders) {
totalCount totalCount
} }
productsOutOfStock: products( productsOutOfStock: products(
@ -40,7 +40,7 @@ export const home = gql`
period: TODAY period: TODAY
first: 5 first: 5
channel: $channel channel: $channel
) @include(if: $PERMISSION_MANAGE_PRODUCTS) { ) @include(if: $hasPermissionToManageProducts) {
edges { edges {
node { node {
id id
@ -68,7 +68,7 @@ export const home = gql`
} }
} }
activities: homepageEvents(last: 10) activities: homepageEvents(last: 10)
@include(if: $PERMISSION_MANAGE_ORDERS) { @include(if: $hasPermissionToManageOrders) {
edges { edges {
node { node {
amount amount

View file

@ -2,8 +2,10 @@
import { useUser } from "@dashboard/auth"; import { useUser } from "@dashboard/auth";
import { channelsListUrl } from "@dashboard/channels/urls"; import { channelsListUrl } from "@dashboard/channels/urls";
import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext"; import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext";
import { hasPermissions } from "@dashboard/components/RequirePermissions";
import { import {
OrderStatusFilter, OrderStatusFilter,
PermissionEnum,
StockAvailability, StockAvailability,
useHomeQuery, useHomeQuery,
} from "@dashboard/graphql"; } from "@dashboard/graphql";
@ -21,10 +23,21 @@ const HomeSection = () => {
const noChannel = !channel && typeof channel !== "undefined"; const noChannel = !channel && typeof channel !== "undefined";
const userPermissions = user?.userPermissions || [];
const { data } = useHomeQuery({ const { data } = useHomeQuery({
displayLoader: true, displayLoader: true,
skip: noChannel, skip: noChannel,
variables: { channel: channel?.slug, datePeriod: getDatePeriod(1) }, variables: {
channel: channel?.slug,
datePeriod: getDatePeriod(1),
hasPermissionToManageOrders: hasPermissions(userPermissions, [
PermissionEnum.MANAGE_ORDERS,
]),
hasPermissionToManageProducts: hasPermissions(userPermissions, [
PermissionEnum.MANAGE_PRODUCTS,
]),
},
}); });
return ( return (

View file

@ -1,3 +1,4 @@
import { useUserAccessibleChannels } from "@dashboard/auth/hooks/useUserAccessibleChannels";
import { TopNav } from "@dashboard/components/AppLayout"; import { TopNav } from "@dashboard/components/AppLayout";
import { LimitsInfo } from "@dashboard/components/AppLayout/LimitsInfo"; import { LimitsInfo } from "@dashboard/components/AppLayout/LimitsInfo";
import { FilterPresetsSelect } from "@dashboard/components/FilterPresetsSelect"; import { FilterPresetsSelect } from "@dashboard/components/FilterPresetsSelect";
@ -5,7 +6,7 @@ import { RefreshLimitsQuery } from "@dashboard/graphql";
import { sectionNames } from "@dashboard/intl"; import { sectionNames } from "@dashboard/intl";
import { FilterPresetsProps } from "@dashboard/types"; import { FilterPresetsProps } from "@dashboard/types";
import { hasLimits, isLimitReached } from "@dashboard/utils/limits"; import { hasLimits, isLimitReached } from "@dashboard/utils/limits";
import { Box, Button, ChevronRightIcon } from "@saleor/macaw-ui/next"; import { Box, Button, ChevronRightIcon, Tooltip } from "@saleor/macaw-ui/next";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
@ -33,6 +34,8 @@ export const OrderDraftListHeader = ({
onAdd, onAdd,
}: OrderDraftListHeaderProps) => { }: OrderDraftListHeaderProps) => {
const intl = useIntl(); const intl = useIntl();
const userAccessibleChannels = useUserAccessibleChannels();
const hasAccessibleChannels = userAccessibleChannels.length > 0;
const limitsReached = isLimitReached(limits, "orders"); const limitsReached = isLimitReached(limits, "orders");
return ( return (
@ -71,9 +74,11 @@ export const OrderDraftListHeader = ({
/> />
</Box> </Box>
<Box display="flex" alignItems="center" gap={2}> <Box display="flex" alignItems="center" gap={2}>
<Tooltip>
<Tooltip.Trigger>
<Button <Button
variant="primary" variant="primary"
disabled={disabled || limitsReached} disabled={disabled || limitsReached || !hasAccessibleChannels}
onClick={onAdd} onClick={onAdd}
data-test-id="create-draft-order-button" data-test-id="create-draft-order-button"
> >
@ -83,6 +88,16 @@ export const OrderDraftListHeader = ({
description="button" description="button"
/> />
</Button> </Button>
</Tooltip.Trigger>
<Tooltip.Content>
{!hasAccessibleChannels && (
<FormattedMessage
defaultMessage="You don't have access to any channels"
id="grkY2V"
/>
)}
</Tooltip.Content>
</Tooltip>
{hasLimits(limits, "orders") && ( {hasLimits(limits, "orders") && (
<LimitsInfo <LimitsInfo

View file

@ -4,6 +4,7 @@ import {
mapToMenuItems, mapToMenuItems,
useExtensions, useExtensions,
} from "@dashboard/apps/hooks/useExtensions"; } from "@dashboard/apps/hooks/useExtensions";
import { useUserAccessibleChannels } from "@dashboard/auth/hooks/useUserAccessibleChannels";
import { LimitsInfo } from "@dashboard/components/AppLayout/LimitsInfo"; import { LimitsInfo } from "@dashboard/components/AppLayout/LimitsInfo";
import { ListFilters } from "@dashboard/components/AppLayout/ListFilters"; import { ListFilters } from "@dashboard/components/AppLayout/ListFilters";
import { TopNav } from "@dashboard/components/AppLayout/TopNav"; import { TopNav } from "@dashboard/components/AppLayout/TopNav";
@ -29,7 +30,7 @@ import {
} from "@dashboard/types"; } from "@dashboard/types";
import { hasLimits, isLimitReached } from "@dashboard/utils/limits"; import { hasLimits, isLimitReached } from "@dashboard/utils/limits";
import { Card } from "@material-ui/core"; import { Card } from "@material-ui/core";
import { Box, Button, ChevronRightIcon } from "@saleor/macaw-ui/next"; import { Box, Button, ChevronRightIcon, Tooltip } from "@saleor/macaw-ui/next";
import React, { useState } from "react"; import React, { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
@ -75,6 +76,8 @@ const OrderListPage: React.FC<OrderListPageProps> = ({
...listProps ...listProps
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const userAccessibleChannels = useUserAccessibleChannels();
const hasAccessibleChannels = userAccessibleChannels.length > 0;
const filterStructure = createFilterStructure(intl, filterOpts); const filterStructure = createFilterStructure(intl, filterOpts);
const limitsReached = isLimitReached(limits, "orders"); const limitsReached = isLimitReached(limits, "orders");
const [isFilterPresetOpen, setFilterPresetOpen] = useState(false); const [isFilterPresetOpen, setFilterPresetOpen] = useState(false);
@ -162,12 +165,15 @@ const OrderListPage: React.FC<OrderListPageProps> = ({
]} ]}
/> />
)} )}
<Tooltip>
<Tooltip.Trigger>
{extensionCreateButtonItems.length > 0 ? ( {extensionCreateButtonItems.length > 0 ? (
<ButtonWithDropdown <ButtonWithDropdown
onClick={onAdd} onClick={onAdd}
testId={"create-order-button"} testId={"create-order-button"}
options={extensionCreateButtonItems} options={extensionCreateButtonItems}
disabled={limitsReached} disabled={limitsReached || !hasAccessibleChannels}
> >
<FormattedMessage <FormattedMessage
id="LshEVn" id="LshEVn"
@ -179,7 +185,7 @@ const OrderListPage: React.FC<OrderListPageProps> = ({
<Button <Button
data-test-id="create-order-button" data-test-id="create-order-button"
onClick={onAdd} onClick={onAdd}
disabled={limitsReached} disabled={limitsReached || !hasAccessibleChannels}
> >
<FormattedMessage <FormattedMessage
id="LshEVn" id="LshEVn"
@ -188,6 +194,17 @@ const OrderListPage: React.FC<OrderListPageProps> = ({
/> />
</Button> </Button>
)} )}
</Tooltip.Trigger>
<Tooltip.Content>
{!hasAccessibleChannels && (
<FormattedMessage
defaultMessage="You don't have access to any channels"
id="grkY2V"
/>
)}
</Tooltip.Content>
</Tooltip>
{hasLimits(limits, "orders") && ( {hasLimits(limits, "orders") && (
<LimitsInfo <LimitsInfo
text={intl.formatMessage( text={intl.formatMessage(

View file

@ -1,4 +1,5 @@
// @ts-strict-ignore // @ts-strict-ignore
import { useUser } from "@dashboard/auth";
import ChannelPickerDialog from "@dashboard/channels/components/ChannelPickerDialog"; import ChannelPickerDialog from "@dashboard/channels/components/ChannelPickerDialog";
import ActionDialog from "@dashboard/components/ActionDialog"; import ActionDialog from "@dashboard/components/ActionDialog";
import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext"; import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext";
@ -100,7 +101,10 @@ export const OrderDraftList: React.FC<OrderDraftListProps> = ({ params }) => {
}, },
}); });
const { channel, availableChannels } = useAppChannel(false); const { channel } = useAppChannel(false);
const user = useUser();
const channels = user?.user?.accessibleChannels ?? [];
const limitOpts = useShopLimitsQuery({ const limitOpts = useShopLimitsQuery({
variables: { variables: {
orders: true, orders: true,
@ -274,7 +278,7 @@ export const OrderDraftList: React.FC<OrderDraftListProps> = ({ params }) => {
tabName={presets[presetIdToDelete - 1]?.name ?? "..."} tabName={presets[presetIdToDelete - 1]?.name ?? "..."}
/> />
<ChannelPickerDialog <ChannelPickerDialog
channelsChoices={mapNodeToChoice(availableChannels)} channelsChoices={mapNodeToChoice(channels)}
confirmButtonState="success" confirmButtonState="success"
defaultChoice={channel?.id} defaultChoice={channel?.id}
open={params.action === "create-order"} open={params.action === "create-order"}

View file

@ -1,4 +1,5 @@
// @ts-strict-ignore // @ts-strict-ignore
import { useUser } from "@dashboard/auth";
import ChannelPickerDialog from "@dashboard/channels/components/ChannelPickerDialog"; import ChannelPickerDialog from "@dashboard/channels/components/ChannelPickerDialog";
import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext"; import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext";
import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog"; import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog";
@ -73,6 +74,9 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
usePaginationReset(orderListUrl, params, settings.rowNumber); usePaginationReset(orderListUrl, params, settings.rowNumber);
const intl = useIntl(); const intl = useIntl();
const { channel, availableChannels } = useAppChannel(false);
const user = useUser();
const channels = user?.user?.accessibleChannels ?? [];
const [createOrder] = useOrderDraftCreateMutation({ const [createOrder] = useOrderDraftCreateMutation({
onCompleted: data => { onCompleted: data => {
@ -87,7 +91,6 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
}, },
}); });
const { channel, availableChannels } = useAppChannel(false);
const limitOpts = useShopLimitsQuery({ const limitOpts = useShopLimitsQuery({
variables: { variables: {
orders: true, orders: true,
@ -95,9 +98,7 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
}); });
const noChannel = !channel && typeof channel !== "undefined"; const noChannel = !channel && typeof channel !== "undefined";
const channelOpts = availableChannels const channelOpts = availableChannels ? mapNodeToChoice(channels) : null;
? mapNodeToChoice(availableChannels)
: null;
const [changeFilters, resetFilters, handleSearchChange] = useFilterHandlers({ const [changeFilters, resetFilters, handleSearchChange] = useFilterHandlers({
createUrl: orderListUrl, createUrl: orderListUrl,

View file

@ -1,18 +1,20 @@
// @ts-strict-ignore import { channels, permissions } from "@dashboard/fixtures";
import { permissions } from "@dashboard/fixtures";
import React from "react"; import React from "react";
import { errorsOfPermissionGroupCreate } from "../../fixtures"; import { errorsOfPermissionGroupCreate } from "../../fixtures";
import PermissionGroupCreatePage, { import {
PermissionGroupCreatePage,
PermissionGroupCreatePageProps, PermissionGroupCreatePageProps,
} from "./PermissionGroupCreatePage"; } from "./PermissionGroupCreatePage";
const props: PermissionGroupCreatePageProps = { const props: PermissionGroupCreatePageProps = {
disabled: false, disabled: false,
errors: [], errors: [],
onSubmit: () => undefined, onSubmit: () => new Promise(resolve => resolve(undefined)),
permissions, permissions,
saveButtonBarState: undefined, channels,
saveButtonBarState: "default",
hasRestrictedChannels: false,
}; };
export default { export default {

View file

@ -1,21 +1,23 @@
// @ts-strict-ignore
import AccountPermissions from "@dashboard/components/AccountPermissions"; import AccountPermissions from "@dashboard/components/AccountPermissions";
import { TopNav } from "@dashboard/components/AppLayout/TopNav"; import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import { Backlink } from "@dashboard/components/Backlink"; import { Backlink } from "@dashboard/components/Backlink";
import { ConfirmButtonTransitionState } from "@dashboard/components/ConfirmButton"; import { ChannelPermission } from "@dashboard/components/ChannelPermission";
import Form from "@dashboard/components/Form"; import Form from "@dashboard/components/Form";
import FormSpacer from "@dashboard/components/FormSpacer";
import { DetailPageLayout } from "@dashboard/components/Layouts"; import { DetailPageLayout } from "@dashboard/components/Layouts";
import Savebar from "@dashboard/components/Savebar"; import Savebar from "@dashboard/components/Savebar";
import { import {
ChannelFragment,
PermissionEnum, PermissionEnum,
PermissionGroupErrorFragment, PermissionGroupErrorFragment,
} from "@dashboard/graphql"; } from "@dashboard/graphql";
import { SubmitPromise } from "@dashboard/hooks/useForm"; import { FormChange, SubmitPromise } from "@dashboard/hooks/useForm";
import useNavigator from "@dashboard/hooks/useNavigator"; import useNavigator from "@dashboard/hooks/useNavigator";
import { sectionNames } from "@dashboard/intl"; import { buttonMessages, sectionNames } from "@dashboard/intl";
import { permissionGroupListUrl } from "@dashboard/permissionGroups/urls"; import { permissionGroupListUrl } from "@dashboard/permissionGroups/urls";
import { getFormErrors } from "@dashboard/utils/errors"; import { getFormErrors } from "@dashboard/utils/errors";
import getPermissionGroupErrorMessage from "@dashboard/utils/errors/permissionGroups"; import getPermissionGroupErrorMessage from "@dashboard/utils/errors/permissionGroups";
import { Box } from "@saleor/macaw-ui/next";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
@ -25,30 +27,40 @@ import PermissionGroupInfo from "../PermissionGroupInfo";
export interface PermissionGroupCreateFormData { export interface PermissionGroupCreateFormData {
name: string; name: string;
hasFullAccess: boolean; hasFullAccess: boolean;
hasAllChannels: boolean;
isActive: boolean; isActive: boolean;
permissions: PermissionEnum[]; permissions: PermissionEnum[];
channels: string[];
} }
const initialForm: PermissionGroupCreateFormData = { const initialForm: PermissionGroupCreateFormData = {
hasFullAccess: false, hasFullAccess: false,
hasAllChannels: true,
isActive: false, isActive: false,
name: "", name: "",
permissions: [], permissions: [],
channels: [],
}; };
export interface PermissionGroupCreatePageProps { export interface PermissionGroupCreatePageProps {
disabled: boolean; disabled: boolean;
errors: PermissionGroupErrorFragment[]; errors: PermissionGroupErrorFragment[];
permissions: PermissionData[]; permissions: PermissionData[];
saveButtonBarState: ConfirmButtonTransitionState; channels: ChannelFragment[];
hasRestrictedChannels: boolean;
saveButtonBarState: "loading" | "success" | "error" | "default";
onSubmit: (data: PermissionGroupCreateFormData) => SubmitPromise; onSubmit: (data: PermissionGroupCreateFormData) => SubmitPromise;
} }
const PermissionGroupCreatePage: React.FC<PermissionGroupCreatePageProps> = ({ export const PermissionGroupCreatePage: React.FC<
PermissionGroupCreatePageProps
> = ({
disabled, disabled,
permissions, permissions,
channels,
onSubmit, onSubmit,
saveButtonBarState, saveButtonBarState,
hasRestrictedChannels,
errors, errors,
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
@ -63,11 +75,33 @@ const PermissionGroupCreatePage: React.FC<PermissionGroupCreatePageProps> = ({
return ( return (
<Form <Form
confirmLeave confirmLeave
initial={initialForm} initial={{
...initialForm,
hasAllChannels: !hasRestrictedChannels,
}}
onSubmit={onSubmit} onSubmit={onSubmit}
disabled={disabled} disabled={disabled}
> >
{({ data, change, submit, isSaveDisabled }) => ( {({ data, change, submit, isSaveDisabled }) => {
const handleChannelChange: FormChange = event => {
change({
target: {
name: "channels",
value: event.target.value,
},
});
};
const handleHasAllChannelsChange = () => {
change({
target: {
name: "hasAllChannels",
value: !data.hasAllChannels,
},
});
};
return (
<DetailPageLayout> <DetailPageLayout>
<TopNav title="New Permission Group" /> <TopNav title="New Permission Group" />
<DetailPageLayout.Content> <DetailPageLayout.Content>
@ -80,6 +114,20 @@ const PermissionGroupCreatePage: React.FC<PermissionGroupCreatePageProps> = ({
onChange={change} onChange={change}
disabled={disabled} disabled={disabled}
/> />
<FormSpacer />
<Box paddingX={6}>
<ChannelPermission
allChannels={channels}
selectedChannels={data.channels}
onChannelChange={handleChannelChange}
onHasAllChannelsChange={handleHasAllChannelsChange}
hasAllChannels={data.hasAllChannels}
disabled={false}
disabledSelectAllChannels={hasRestrictedChannels}
/>
</Box>
</DetailPageLayout.Content> </DetailPageLayout.Content>
<DetailPageLayout.RightSidebar> <DetailPageLayout.RightSidebar>
<AccountPermissions <AccountPermissions
@ -89,11 +137,7 @@ const PermissionGroupCreatePage: React.FC<PermissionGroupCreatePageProps> = ({
disabled={disabled} disabled={disabled}
permissions={permissions} permissions={permissions}
onChange={change} onChange={change}
fullAccessLabel={intl.formatMessage({ fullAccessLabel={intl.formatMessage(buttonMessages.selectAll)}
id: "mAabef",
defaultMessage: "Group has full access to the store",
description: "checkbox label",
})}
description={intl.formatMessage({ description={intl.formatMessage({
id: "CYZse9", id: "CYZse9",
defaultMessage: defaultMessage:
@ -106,12 +150,11 @@ const PermissionGroupCreatePage: React.FC<PermissionGroupCreatePageProps> = ({
onCancel={() => navigate(permissionGroupListUrl())} onCancel={() => navigate(permissionGroupListUrl())}
onSubmit={submit} onSubmit={submit}
state={saveButtonBarState} state={saveButtonBarState}
disabled={isSaveDisabled} disabled={!!isSaveDisabled}
/> />
</DetailPageLayout> </DetailPageLayout>
)} );
}}
</Form> </Form>
); );
}; };
PermissionGroupCreatePage.displayName = "PermissionGroupCreatePage";
export default PermissionGroupCreatePage;

View file

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

View file

@ -1,30 +1,42 @@
// @ts-strict-ignore import { channels, permissions } from "@dashboard/fixtures";
import { permissions } from "@dashboard/fixtures"; import { MembersListUrlSortField } from "@dashboard/permissionGroups/urls";
import React from "react"; import React from "react";
import { emptyPermissionGroup, permissionGroup, users } from "../../fixtures"; import {
import PermissionGroupDetailsPage, { emptyPermissionGroup,
PermissionGroupDetailsPageProps, permissionGroup,
permissionGroupWithChannels,
users,
} from "../../fixtures";
import {
PermissionGroupDetailsPage,
PermissonGroupDetailsPageProps,
} from "./PermissionGroupDetailsPage"; } from "./PermissionGroupDetailsPage";
export * from "./PermissionGroupDetailsPage";
const props: PermissionGroupDetailsPageProps = { const props: PermissonGroupDetailsPageProps = {
disabled: false, disabled: false,
isUserAbleToEditChannels: true,
errors: [], errors: [],
isChecked: () => false, isChecked: () => false,
members: users, members: users,
onAssign: () => undefined, onAssign: () => undefined,
onSort: () => undefined, onSort: () => undefined,
onSubmit: () => undefined, onSubmit: () => new Promise(resolve => resolve(undefined)),
onUnassign: () => undefined, onUnassign: () => undefined,
permissionGroup, permissionGroup,
permissions, permissions,
permissionsExceeded: false, permissionsExceeded: false,
saveButtonBarState: undefined, saveButtonBarState: "default",
selected: 0, selected: 0,
sort: null, sort: {
asc: true,
sort: MembersListUrlSortField.name,
},
toggle: () => undefined, toggle: () => undefined,
toggleAll: () => undefined, toggleAll: () => undefined,
toolbar: null, toolbar: null,
channels,
}; };
export default { export default {
@ -45,7 +57,23 @@ export const Loading = () => (
<PermissionGroupDetailsPage <PermissionGroupDetailsPage
{...props} {...props}
disabled={true} disabled={true}
permissionGroup={undefined} permissionGroup={permissionGroup}
permissions={undefined} permissions={permissions}
/>
);
export const WithRestrictedChannels = () => (
<PermissionGroupDetailsPage
{...props}
permissionGroup={permissionGroupWithChannels}
/>
);
export const WithRestrictedChannelsAndWithoutAccessToEdit = () => (
<PermissionGroupDetailsPage
{...props}
disabled={true}
isUserAbleToEditChannels={false}
permissionGroup={permissionGroupWithChannels}
/> />
); );

View file

@ -1,42 +1,50 @@
// @ts-strict-ignore import { useUser } from "@dashboard/auth";
import AccountPermissions from "@dashboard/components/AccountPermissions"; import AccountPermissions from "@dashboard/components/AccountPermissions";
import { TopNav } from "@dashboard/components/AppLayout/TopNav"; import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import { ConfirmButtonTransitionState } from "@dashboard/components/ConfirmButton"; import { ChannelPermission } from "@dashboard/components/ChannelPermission";
import Form from "@dashboard/components/Form"; import Form from "@dashboard/components/Form";
import FormSpacer from "@dashboard/components/FormSpacer"; import FormSpacer from "@dashboard/components/FormSpacer";
import { DetailPageLayout } from "@dashboard/components/Layouts"; import { DetailPageLayout } from "@dashboard/components/Layouts";
import Savebar from "@dashboard/components/Savebar"; import Savebar from "@dashboard/components/Savebar";
import { import {
ChannelFragment,
PermissionEnum, PermissionEnum,
PermissionGroupDetailsFragment, PermissionGroupDetailsFragment,
PermissionGroupErrorFragment, PermissionGroupErrorFragment,
UserPermissionFragment, UserPermissionFragment,
} from "@dashboard/graphql"; } from "@dashboard/graphql";
import { SubmitPromise } from "@dashboard/hooks/useForm"; import { FormChange, SubmitPromise } from "@dashboard/hooks/useForm";
import useNavigator from "@dashboard/hooks/useNavigator"; import useNavigator from "@dashboard/hooks/useNavigator";
import { buttonMessages } from "@dashboard/intl";
import { import {
MembersListUrlSortField, MembersListUrlSortField,
permissionGroupListUrl, permissionGroupListUrl,
} from "@dashboard/permissionGroups/urls"; } from "@dashboard/permissionGroups/urls";
import {
extractPermissionCodes,
isGroupFullAccess,
} from "@dashboard/permissionGroups/utils";
import { ListActions, SortPage } from "@dashboard/types"; import { ListActions, SortPage } from "@dashboard/types";
import { getFormErrors } from "@dashboard/utils/errors"; import { getFormErrors } from "@dashboard/utils/errors";
import getPermissionGroupErrorMessage from "@dashboard/utils/errors/permissionGroups"; import getPermissionGroupErrorMessage from "@dashboard/utils/errors/permissionGroups";
import { Box } from "@saleor/macaw-ui/next";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import {
checkIfUserHasRestictedAccessToChannels,
extractPermissionCodes,
getInitialChannels,
getUserAccessibleChannelsOptions,
isGroupFullAccess,
} from "../../utils";
import PermissionGroupInfo from "../PermissionGroupInfo"; import PermissionGroupInfo from "../PermissionGroupInfo";
import PermissionGroupMemberList from "../PermissionGroupMemberList"; import PermissionGroupMemberList from "../PermissionGroupMemberList";
export interface PermissionGroupDetailsPageFormData { export interface PermissionGroupDetailsPageFormData {
name: string; name: string;
hasFullAccess: boolean; hasFullAccess: boolean;
hasAllChannels: boolean;
isActive: boolean; isActive: boolean;
permissions: PermissionEnum[]; permissions: PermissionEnum[];
users: PermissionGroupDetailsFragment["users"]; users: PermissionGroupDetailsFragment["users"];
channels: string[];
} }
export interface PermissionData export interface PermissionData
@ -45,22 +53,26 @@ export interface PermissionData
disabled?: boolean; disabled?: boolean;
} }
export interface PermissionGroupDetailsPageProps export interface PermissonGroupDetailsPageProps
extends ListActions, extends ListActions,
SortPage<MembersListUrlSortField> { SortPage<MembersListUrlSortField> {
channels: ChannelFragment[];
disabled: boolean; disabled: boolean;
isUserAbleToEditChannels: boolean;
errors: PermissionGroupErrorFragment[]; errors: PermissionGroupErrorFragment[];
members: PermissionGroupDetailsFragment["users"]; members: PermissionGroupDetailsFragment["users"];
permissionGroup: PermissionGroupDetailsFragment; permissionGroup: PermissionGroupDetailsFragment | null | undefined;
permissions: PermissionData[]; permissions: PermissionData[];
permissionsExceeded: boolean; permissionsExceeded: boolean;
saveButtonBarState: ConfirmButtonTransitionState; saveButtonBarState: "loading" | "success" | "error" | "default";
onAssign: () => void; onAssign: () => void;
onUnassign: (ids: string[]) => void; onUnassign: (ids: string[]) => void;
onSubmit: (data: PermissionGroupDetailsPageFormData) => SubmitPromise; onSubmit: (data: PermissionGroupDetailsPageFormData) => SubmitPromise;
} }
const PermissionGroupDetailsPage: React.FC<PermissionGroupDetailsPageProps> = ({ export const PermissionGroupDetailsPage: React.FC<
PermissonGroupDetailsPageProps
> = ({
disabled, disabled,
errors, errors,
members, members,
@ -69,13 +81,23 @@ const PermissionGroupDetailsPage: React.FC<PermissionGroupDetailsPageProps> = ({
permissions, permissions,
permissionsExceeded, permissionsExceeded,
saveButtonBarState, saveButtonBarState,
channels,
isUserAbleToEditChannels,
...listProps ...listProps
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const navigate = useNavigator(); const navigate = useNavigator();
const user = useUser();
const channelsOptions = getUserAccessibleChannelsOptions(channels, user.user);
const hasUserRestrictedChannels = checkIfUserHasRestictedAccessToChannels(
user.user,
);
const initialForm: PermissionGroupDetailsPageFormData = { const initialForm: PermissionGroupDetailsPageFormData = {
hasFullAccess: isGroupFullAccess(permissionGroup, permissions), hasFullAccess: isGroupFullAccess(permissionGroup, permissions),
hasAllChannels: !permissionGroup?.restrictedAccessToChannels ?? false,
channels: getInitialChannels(permissionGroup, channels?.length ?? 0),
isActive: false, isActive: false,
name: permissionGroup?.name || "", name: permissionGroup?.name || "",
permissions: extractPermissionCodes(permissionGroup), permissions: extractPermissionCodes(permissionGroup),
@ -90,7 +112,26 @@ const PermissionGroupDetailsPage: React.FC<PermissionGroupDetailsPageProps> = ({
return ( return (
<Form confirmLeave initial={initialForm} onSubmit={onSubmit}> <Form confirmLeave initial={initialForm} onSubmit={onSubmit}>
{({ data, change, submit }) => ( {({ data, change, submit }) => {
const handleChannelChange: FormChange = event => {
change({
target: {
name: "channels",
value: event.target.value,
},
});
};
const handleHasAllChannelsChange = () => {
change({
target: {
name: "hasAllChannels",
value: !data.hasAllChannels,
},
});
};
return (
<DetailPageLayout> <DetailPageLayout>
<TopNav <TopNav
href={permissionGroupListUrl()} href={permissionGroupListUrl()}
@ -103,6 +144,25 @@ const PermissionGroupDetailsPage: React.FC<PermissionGroupDetailsPageProps> = ({
errors={errors} errors={errors}
onChange={change} onChange={change}
/> />
<FormSpacer />
<Box paddingX={6}>
<ChannelPermission
allChannels={
// I pass all channels because Multiselect components based on ids,
// and need data that will take information about channel
!isUserAbleToEditChannels ? channels : channelsOptions
}
hasAllChannels={data.hasAllChannels}
selectedChannels={data.channels}
onHasAllChannelsChange={handleHasAllChannelsChange}
onChannelChange={handleChannelChange}
disabled={!isUserAbleToEditChannels || disabled}
disabledSelectAllChannels={hasUserRestrictedChannels}
/>
</Box>
<FormSpacer /> <FormSpacer />
<PermissionGroupMemberList <PermissionGroupMemberList
disabled={disabled} disabled={disabled}
@ -118,11 +178,7 @@ const PermissionGroupDetailsPage: React.FC<PermissionGroupDetailsPageProps> = ({
permissions={permissions} permissions={permissions}
onChange={change} onChange={change}
errorMessage={permissionsError} errorMessage={permissionsError}
fullAccessLabel={intl.formatMessage({ fullAccessLabel={intl.formatMessage(buttonMessages.selectAll)}
id: "mAabef",
defaultMessage: "Group has full access to the store",
description: "checkbox label",
})}
description={intl.formatMessage({ description={intl.formatMessage({
id: "CYZse9", id: "CYZse9",
defaultMessage: defaultMessage:
@ -140,9 +196,8 @@ const PermissionGroupDetailsPage: React.FC<PermissionGroupDetailsPageProps> = ({
/> />
</div> </div>
</DetailPageLayout> </DetailPageLayout>
)} );
}}
</Form> </Form>
); );
}; };
PermissionGroupDetailsPage.displayName = "PermissionGroupDetailsPage";
export default PermissionGroupDetailsPage;

View file

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

View file

@ -1,5 +1,5 @@
// @ts-strict-ignore
import avatarImg from "@assets/images/avatars/avatar.png"; import avatarImg from "@assets/images/avatars/avatar.png";
import { channels } from "@dashboard/fixtures";
import { import {
PermissionEnum, PermissionEnum,
PermissionGroupDetailsFragment, PermissionGroupDetailsFragment,
@ -113,6 +113,7 @@ export const emptyPermissionGroup: PermissionGroupDetailsFragment = {
userCanManage: true, userCanManage: true,
users: [], users: [],
__typename: "Group", __typename: "Group",
permissions: [ permissions: [
{ {
code: PermissionEnum.MANAGE_PAGES, code: PermissionEnum.MANAGE_PAGES,
@ -120,6 +121,8 @@ export const emptyPermissionGroup: PermissionGroupDetailsFragment = {
__typename: "Permission", __typename: "Permission",
}, },
], ],
accessibleChannels: [],
restrictedAccessToChannels: false,
}; };
export const errorsOfPermissionGroupCreate: PermissionGroupErrorFragment[] = [ export const errorsOfPermissionGroupCreate: PermissionGroupErrorFragment[] = [
@ -161,6 +164,8 @@ export const permissionGroup: PermissionGroupDetailsFragment = {
avatar: null, avatar: null,
}, },
], ],
accessibleChannels: [],
restrictedAccessToChannels: false,
__typename: "Group", __typename: "Group",
permissions: [ permissions: [
{ {
@ -171,7 +176,46 @@ export const permissionGroup: PermissionGroupDetailsFragment = {
], ],
}; };
export const users: RelayToFlat<SearchStaffMembersQuery["search"]> = [ export const permissionGroupWithChannels: NonNullable<PermissionGroupDetailsFragment> =
{
id: "R3JvdXA6Mw==",
name: "Editors",
userCanManage: true,
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,
},
],
accessibleChannels: [channels[0]],
restrictedAccessToChannels: true,
__typename: "Group",
permissions: [
{
code: PermissionEnum.MANAGE_PAGES,
name: "Manage pages.",
__typename: "Permission",
},
],
};
export const users: RelayToFlat<
NonNullable<SearchStaffMembersQuery["search"]>
> = [
{ {
node: { node: {
id: "VXNlcjoyMQ==", id: "VXNlcjoyMQ==",

View file

@ -15,8 +15,8 @@ import {
PermissionGroupListUrlQueryParams, PermissionGroupListUrlQueryParams,
PermissionGroupListUrlSortField, PermissionGroupListUrlSortField,
} from "./urls"; } from "./urls";
import PermissionGroupCreate from "./views/PermissionGroupCreate"; import { PermissionGroupCreate } from "./views/PermissionGroupCreate";
import PermissionGroupDetailsComponent from "./views/PermissionGroupDetails"; import { PermissionGroupDetails as PermissionGroupDetailsComponent } from "./views/PermissionGroupDetails";
import PermissionGroupListComponent from "./views/PermissionGroupList"; import PermissionGroupListComponent from "./views/PermissionGroupList";
const permissionGroupList: React.FC<RouteComponentProps<{}>> = ({ const permissionGroupList: React.FC<RouteComponentProps<{}>> = ({

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
import { StaffMemberFragment } from "@dashboard/graphql"; import { StaffMemberFragment } from "@dashboard/graphql";
import { getUserName } from "@dashboard/misc"; import { getUserName } from "@dashboard/misc";
@ -7,12 +6,13 @@ import { MembersListUrlSortField } from "./urls";
export const sortMembers = export const sortMembers =
(sort: string, asc: boolean) => (sort: string, asc: boolean) =>
(a: StaffMemberFragment, b: StaffMemberFragment) => { (a: StaffMemberFragment, b: StaffMemberFragment) => {
let valueA; let valueA: string = "";
let valueB; let valueB: string = "";
switch (sort) { switch (sort) {
case MembersListUrlSortField.name: case MembersListUrlSortField.name:
valueA = getUserName(a); valueA = getUserName(a) ?? "";
valueB = getUserName(b); valueB = getUserName(b) ?? "";
break; break;
case MembersListUrlSortField.email: case MembersListUrlSortField.email:
valueA = a.email; valueA = a.email;
@ -21,6 +21,6 @@ export const sortMembers =
} }
return asc return asc
? ("" + valueA).localeCompare(valueB) ? valueA.localeCompare(valueB)
: ("" + valueA).localeCompare(valueB) * -1; : valueA.localeCompare(valueB) * -1;
}; };

View file

@ -0,0 +1,662 @@
import { UserContext } from "@dashboard/auth/types";
import {
ChannelFragment,
PermissionEnum,
PermissionFragment,
PermissionGroupDetailsFragment,
UserFragment,
} from "@dashboard/graphql";
import { PermissionGroupDetailsPageFormData } from "./components/PermissionGroupDetailsPage";
import { permissionGroup, permissionGroupWithChannels } from "./fixtures";
import {
arePermissionsExceeded,
channelsDiff,
checkIfUserHasRestictedAccessToChannels,
extractPermissionCodes,
getInitialChannels,
getUserAccessibleChannelsOptions,
isGroupFullAccess,
mapAccessibleChannelsToChoice,
permissionsDiff,
usersDiff,
} from "./utils";
describe("Permission group utils", () => {
describe("channelDiff", () => {
it("should return empty added and removed channels when user is not eligible to edit channels", () => {
// Arrange
const isUserEligibleToEditPermissionGroup = false;
const allAviableChannels: ChannelFragment[] = [];
const formData = {
channels: [],
} as unknown as PermissionGroupDetailsPageFormData;
const permissionGroup = {
restrictedAccessToChannels: false,
accessibleChannels: [
{
id: "1",
name: "channel-1",
},
{
id: "2",
name: "channel-2",
},
{
id: "3",
name: "channel-3",
},
],
} as PermissionGroupDetailsFragment;
// Act
const { addChannels, removeChannels } = channelsDiff(
permissionGroup,
formData,
allAviableChannels,
isUserEligibleToEditPermissionGroup,
);
// Assert
expect(addChannels).toEqual([]);
expect(removeChannels).toEqual([]);
});
it("should return added channel and no removed channels when user had no restricted channels", () => {
// Arrange
const isUserEligibleToEditPermissionGroup = true;
const allAviableChannels: ChannelFragment[] = [];
const formData = {
channels: ["1"],
} as PermissionGroupDetailsPageFormData;
const permissionGroup = {
restrictedAccessToChannels: false,
accessibleChannels: [
{
id: "1",
name: "channel-1",
},
{
id: "2",
name: "channel-2",
},
{
id: "3",
name: "channel-3",
},
],
} as PermissionGroupDetailsFragment;
// Act
const { addChannels, removeChannels } = channelsDiff(
permissionGroup,
formData,
allAviableChannels,
isUserEligibleToEditPermissionGroup,
);
// Assert
expect(addChannels).toEqual(["1"]);
expect(removeChannels).toEqual([]);
});
it("should return all added and removed channels", () => {
// Arrange
const isUserEligibleToEditPermissionGroup = true;
const allAviableChannels: ChannelFragment[] = [];
const formData = {
channels: ["2", "3", "55"],
} as PermissionGroupDetailsPageFormData;
const permissionGroup = {
restrictedAccessToChannels: true,
accessibleChannels: [
{
id: "1",
name: "channel-1",
},
{
id: "2",
name: "channel-2",
},
{
id: "3",
name: "channel-3",
},
],
} as PermissionGroupDetailsFragment;
// Act
const { addChannels, removeChannels } = channelsDiff(
permissionGroup,
formData,
allAviableChannels,
isUserEligibleToEditPermissionGroup,
);
// Assert
expect(addChannels).toEqual(["55"]);
expect(removeChannels).toEqual(["1"]);
});
it("should only removed channels", () => {
// Arrange
const isUserEligibleToEditPermissionGroup = true;
const allAviableChannels: ChannelFragment[] = [];
const formData = {
channels: ["2"],
} as PermissionGroupDetailsPageFormData;
const permissionGroup = {
restrictedAccessToChannels: true,
accessibleChannels: [
{
id: "1",
name: "channel-1",
},
{
id: "2",
name: "channel-2",
},
],
} as PermissionGroupDetailsFragment;
// Act
const { addChannels, removeChannels } = channelsDiff(
permissionGroup,
formData,
allAviableChannels,
isUserEligibleToEditPermissionGroup,
);
// Assert
expect(addChannels).toEqual([]);
expect(removeChannels).toEqual(["1"]);
});
it("should return all channels when no restricted channels and allow access all", () => {
// Arrnage
const isUserEligibleToEditPermissionGroup = true;
const formData = {
channels: ["2"],
hasAllChannels: true,
} as PermissionGroupDetailsPageFormData;
const permissionGroup = {
restrictedAccessToChannels: false,
accessibleChannels: [
{
id: "1",
name: "channel-1",
},
{
id: "2",
name: "channel-2",
},
],
} as PermissionGroupDetailsFragment;
const allChannels = [
{ id: "12", name: "channel-12" },
{ id: "22", name: "channel-22" },
] as ChannelFragment[];
// Act
const { addChannels, removeChannels } = channelsDiff(
permissionGroup,
formData,
allChannels,
isUserEligibleToEditPermissionGroup,
);
// Assert
expect(addChannels).toEqual(["12", "22"]);
expect(removeChannels).toEqual([]);
});
});
describe("isGroupFullAccess", () => {
it("should return true when have all permissions available in shop assigned", () => {
// Arrange
const permissionGroup = {
permissions: [
{ code: PermissionEnum.HANDLE_TAXES, name: "Handle taxes" },
{ code: PermissionEnum.HANDLE_CHECKOUTS, name: "Handle checkouts" },
{ code: PermissionEnum.HANDLE_PAYMENTS, name: "Handle payments" },
{ code: PermissionEnum.MANAGE_APPS, name: "Handle apps" },
],
} as PermissionGroupDetailsFragment;
const shopPermissions = [
{ code: PermissionEnum.HANDLE_TAXES, name: "Handle taxes" },
{ code: PermissionEnum.HANDLE_CHECKOUTS, name: "Handle checkouts" },
{ code: PermissionEnum.HANDLE_PAYMENTS, name: "Handle payments" },
{ code: PermissionEnum.MANAGE_APPS, name: "Handle apps" },
] as Array<Omit<PermissionFragment, "__typename">>;
// Act & Assert
expect(isGroupFullAccess(permissionGroup, shopPermissions)).toBe(true);
});
it("should return false when permission length is different", () => {
// Arrange
const permissionGroup = {
permissions: [
{ code: PermissionEnum.HANDLE_CHECKOUTS, name: "Handle checkouts" },
{ code: PermissionEnum.HANDLE_PAYMENTS, name: "Handle payments" },
{ code: PermissionEnum.MANAGE_APPS, name: "Handle apps" },
],
} as PermissionGroupDetailsFragment;
const shopPermissions = [
{ code: PermissionEnum.HANDLE_TAXES, name: "Handle taxes" },
{ code: PermissionEnum.HANDLE_CHECKOUTS, name: "Handle checkouts" },
{ code: PermissionEnum.HANDLE_PAYMENTS, name: "Handle payments" },
{ code: PermissionEnum.MANAGE_APPS, name: "Handle apps" },
] as Array<Omit<PermissionFragment, "__typename">>;
// Act & Assert
expect(isGroupFullAccess(permissionGroup, shopPermissions)).toBe(false);
});
it("should return false when permission does not have all shop permissions", () => {
// Arrange
const permissionGroup = {
permissions: [
{ code: PermissionEnum.HANDLE_TAXES, name: "Handle taxes" },
{ code: PermissionEnum.HANDLE_CHECKOUTS, name: "Handle checkouts" },
{ code: PermissionEnum.HANDLE_PAYMENTS, name: "Handle payments" },
{ code: PermissionEnum.MANAGE_APPS, name: "Handle apps" },
],
} as PermissionGroupDetailsFragment;
const shopPermissions = [
{ code: PermissionEnum.MANAGE_ORDERS, name: "Handle order" },
{ code: PermissionEnum.HANDLE_TAXES, name: "Handle taxes" },
{ code: PermissionEnum.HANDLE_CHECKOUTS, name: "Handle checkouts" },
{ code: PermissionEnum.HANDLE_PAYMENTS, name: "Handle payments" },
{ code: PermissionEnum.MANAGE_APPS, name: "Handle apps" },
] as Array<Omit<PermissionFragment, "__typename">>;
// Act & Assert
expect(isGroupFullAccess(permissionGroup, shopPermissions)).toBe(false);
});
});
describe("extractPermissionCodes", () => {
it("should return list of permission codes", () => {
// Arrange
const permissions = {
permissions: [
{ code: PermissionEnum.HANDLE_TAXES, name: "Handle taxes" },
{ code: PermissionEnum.HANDLE_CHECKOUTS, name: "Handle checkouts" },
{ code: PermissionEnum.HANDLE_PAYMENTS, name: "Handle payments" },
{ code: PermissionEnum.MANAGE_APPS, name: "Handle apps" },
],
} as PermissionGroupDetailsFragment;
// Act & Assert
expect(extractPermissionCodes(permissions)).toEqual([
PermissionEnum.HANDLE_TAXES,
PermissionEnum.HANDLE_CHECKOUTS,
PermissionEnum.HANDLE_PAYMENTS,
PermissionEnum.MANAGE_APPS,
]);
});
});
describe("permissionsDiff", () => {
it("should return added permissions and no removed permissions when user had no permissions", () => {
// Arrange
const formData = {
permissions: [PermissionEnum.HANDLE_TAXES],
} as PermissionGroupDetailsPageFormData;
const permissionGroup = {
permissions: [],
} as unknown as PermissionGroupDetailsFragment;
// Act
const { addPermissions, removePermissions } = permissionsDiff(
permissionGroup,
formData,
);
// Assert
expect(addPermissions).toEqual([PermissionEnum.HANDLE_TAXES]);
expect(removePermissions).toEqual([]);
});
it("should return all added and removed permissions", () => {
// Arrange
const formData = {
permissions: [
PermissionEnum.HANDLE_TAXES,
PermissionEnum.HANDLE_CHECKOUTS,
],
} as PermissionGroupDetailsPageFormData;
const permissionGroup = {
permissions: [
{ code: PermissionEnum.HANDLE_TAXES, name: "Handle taxes" },
{ code: PermissionEnum.HANDLE_PAYMENTS, name: "Handle payments" },
],
} as PermissionGroupDetailsFragment;
// Act
const { addPermissions, removePermissions } = permissionsDiff(
permissionGroup,
formData,
);
// Assert
expect(addPermissions).toEqual([PermissionEnum.HANDLE_CHECKOUTS]);
expect(removePermissions).toEqual([PermissionEnum.HANDLE_PAYMENTS]);
});
});
describe("usersDiff", () => {
it("should return added users and no removed users when user had no users", () => {
// Arrange
const formData = {
users: [
{ id: "1", email: "test1@test.com" },
{ id: "2", email: "test2@test.com" },
],
} as PermissionGroupDetailsPageFormData;
const permissionGroup = {
users: [],
} as unknown as PermissionGroupDetailsFragment;
// Act
const { addUsers, removeUsers } = usersDiff(permissionGroup, formData);
// Assert
expect(addUsers).toEqual(["1", "2"]);
expect(removeUsers).toEqual([]);
});
it("should return all added and removed users", () => {
// Arrange
const formData = {
users: [
{ id: "2", email: "test2@test.com" },
{ id: "3", email: "test3@test.com" },
],
} as PermissionGroupDetailsPageFormData;
const permissionGroup = {
users: [{ id: "1", email: "test1@test.com" }],
} as PermissionGroupDetailsFragment;
// Act
const { addUsers, removeUsers } = usersDiff(permissionGroup, formData);
// Assert
expect(addUsers).toEqual(["2", "3"]);
expect(removeUsers).toEqual(["1"]);
});
});
describe("arePermissionsExceeded", () => {
it("should return false when number of permissions is not exceeded", () => {
// Arrange
const permissions = {
permissions: [
{ code: PermissionEnum.HANDLE_TAXES, name: "Handle taxes" },
{ code: PermissionEnum.HANDLE_CHECKOUTS, name: "Handle checkouts" },
{ code: PermissionEnum.HANDLE_PAYMENTS, name: "Handle payments" },
{ code: PermissionEnum.MANAGE_APPS, name: "Handle apps" },
],
} as PermissionGroupDetailsFragment;
const user = {
userPermissions: [
{ code: PermissionEnum.HANDLE_TAXES, name: "Handle taxe" },
{ code: PermissionEnum.HANDLE_CHECKOUTS, name: "Handle checkouts" },
{ code: PermissionEnum.HANDLE_PAYMENTS, name: "Handle payments" },
{ code: PermissionEnum.MANAGE_APPS, name: "Handle apps" },
],
} as UserFragment;
// Act
const permissionsExceeded = arePermissionsExceeded(permissions, user);
// Assert
expect(permissionsExceeded).toBe(false);
});
it("should return true when number of permissions is exceeded", () => {
// Arrange
const permissions = {
permissions: [
{ code: PermissionEnum.HANDLE_TAXES, name: "Handle taxe" },
{ code: PermissionEnum.HANDLE_CHECKOUTS, name: "Handle checkouts" },
{ code: PermissionEnum.HANDLE_PAYMENTS, name: "Handle payments" },
{ code: PermissionEnum.MANAGE_APPS, name: "Handle apps" },
],
} as PermissionGroupDetailsFragment;
const user = {
userPermissions: [
{ code: PermissionEnum.HANDLE_CHECKOUTS, name: "Handle checkouts" },
{ code: PermissionEnum.HANDLE_PAYMENTS, name: "Handle payments" },
{ code: PermissionEnum.MANAGE_APPS, name: "Handle apps" },
],
} as UserFragment;
// Act
const permissionsExceeded = arePermissionsExceeded(permissions, user);
// Assert
expect(permissionsExceeded).toBe(true);
});
});
describe("getPermissionGroupAccessibleChannels", () => {
it("should return all accessible channels ", () => {
// Arrange
const permissionGroup = {
accessibleChannels: [
{
id: "1",
name: "Channel 1",
slug: "channel-1",
currencyCode: "USD",
},
{
id: "2",
name: "Channel 2",
slug: "channel-2",
currencyCode: "USD",
},
],
} as PermissionGroupDetailsFragment;
// Act
const accessibleChannels = mapAccessibleChannelsToChoice(permissionGroup);
// Assert
expect(accessibleChannels).toEqual([
{ label: "Channel 1", value: "1", disabled: false },
{ label: "Channel 2", value: "2", disabled: false },
]);
});
});
describe("getUserAccessibleChannelsOptions", () => {
it("should return empty array when no users", () => {
// Arrange
const availableChannels = [
{
id: "1",
name: "Channel 1",
slug: "channel-1",
currencyCode: "USD",
},
{
id: "2",
name: "Channel 2",
slug: "channel-2",
currencyCode: "USD",
},
] as ChannelFragment[];
// Act
const filteredChannels =
getUserAccessibleChannelsOptions(availableChannels);
// Assert
expect(filteredChannels).toEqual([]);
});
it("should return available channels when user has no restricted channels", () => {
// Arrange
const availableChannels = [
{
id: "1",
name: "Channel 1",
slug: "channel-1",
currencyCode: "USD",
},
{
id: "2",
name: "Channel 2",
slug: "channel-2",
currencyCode: "USD",
},
] as ChannelFragment[];
const user = {
restrictedAccessToChannels: false,
} as UserContext["user"];
// Act
const filteredChannels = getUserAccessibleChannelsOptions(
availableChannels,
user,
);
// Assert
expect(filteredChannels).toEqual(availableChannels);
});
it("should return user accessible channels when user has accessibleChannels and has restricted access to channels", () => {
// Arrange
const availableChannels = [
{
id: "1",
name: "Channel 1",
slug: "channel-1",
currencyCode: "USD",
},
{
id: "2",
name: "Channel 2",
slug: "channel-2",
currencyCode: "USD",
},
] as ChannelFragment[];
const user = {
accessibleChannels: [
{
id: "1",
name: "UserChannel 1",
slug: "Userchannel-1",
},
],
restrictedAccessToChannels: true,
} as UserContext["user"];
// Act
const filteredChannels = getUserAccessibleChannelsOptions(
availableChannels,
user,
);
// Assert
expect(filteredChannels).toEqual([
{
id: "1",
name: "UserChannel 1",
slug: "Userchannel-1",
},
]);
});
});
describe("checkIfUserHasRestictedChannels", () => {
it("should return true when user has restricted channels", () => {
// Arrange
const user = {
restrictedAccessToChannels: true,
} as UserContext["user"];
// Act
const hasRestrictedChannels =
checkIfUserHasRestictedAccessToChannels(user);
// Assert
expect(hasRestrictedChannels).toBe(true);
});
it("should return false when user has no restricted channels", () => {
// Arrange
const user = {
restrictedAccessToChannels: false,
} as UserContext["user"];
// Act
const hasRestrictedChannels =
checkIfUserHasRestictedAccessToChannels(user);
// Assert
expect(hasRestrictedChannels).toBe(false);
});
it("should return false when user no user", () => {
// Arrange and Act
const hasRestrictedChannels =
checkIfUserHasRestictedAccessToChannels(undefined);
// Assert
expect(hasRestrictedChannels).toBe(false);
});
});
describe("getInitialChannels", () => {
it("should return empty array when no restricted channels and accessible channels length is equal all channels length", () => {
// Arrange
const allChannelsLength = 0;
// Act
const initialChannels = getInitialChannels(
permissionGroup,
allChannelsLength,
);
// Assert
expect(initialChannels).toEqual([]);
});
it("should return all accessible channels otherwise", () => {
// Arrange
const allChannelsLength = 10;
// Act
const initialChannels = getInitialChannels(
permissionGroupWithChannels,
allChannelsLength,
);
// Assert
expect(initialChannels).toEqual([
permissionGroupWithChannels.accessibleChannels?.[0].id,
]);
});
});
});

View file

@ -1,5 +1,7 @@
// @ts-strict-ignore import { UserContext } from "@dashboard/auth/types";
import { MultiAutocompleteChoiceType } from "@dashboard/components/MultiAutocompleteSelectField";
import { import {
ChannelFragment,
PermissionFragment, PermissionFragment,
PermissionGroupDetailsFragment, PermissionGroupDetailsFragment,
UserFragment, UserFragment,
@ -7,14 +9,16 @@ import {
import difference from "lodash/difference"; import difference from "lodash/difference";
import { PermissionGroupDetailsPageFormData } from "./components/PermissionGroupDetailsPage"; import { PermissionGroupDetailsPageFormData } from "./components/PermissionGroupDetailsPage";
/** /**
* Will return true if group has all permissions available in shop assigned. * Will return true if group has all permissions available in shop assigned.
*/ */
export const isGroupFullAccess = ( export const isGroupFullAccess = (
permissionGroup: PermissionGroupDetailsFragment, permissionGroup: PermissionGroupDetailsFragment | null | undefined,
shopPermissions: Array<Omit<PermissionFragment, "__typename">>, shopPermissions: Array<Omit<PermissionFragment, "__typename">>,
) => { ) => {
if (!permissionGroup) {
return false;
}
const assignedCodes = extractPermissionCodes(permissionGroup); const assignedCodes = extractPermissionCodes(permissionGroup);
if (assignedCodes.length !== shopPermissions?.length) { if (assignedCodes.length !== shopPermissions?.length) {
@ -33,19 +37,31 @@ export const isGroupFullAccess = (
* Return list of codes which are assigned to the permission group. * Return list of codes which are assigned to the permission group.
*/ */
export const extractPermissionCodes = ( export const extractPermissionCodes = (
permissionGroup: PermissionGroupDetailsFragment, permissionGroup: PermissionGroupDetailsFragment | null | undefined,
) => ) => {
permissionGroup?.permissions if (!permissionGroup) {
return [];
}
return permissionGroup?.permissions
? permissionGroup.permissions.map(perm => perm.code) ? permissionGroup.permissions.map(perm => perm.code)
: []; : [];
};
/** /**
* Return lists of permissions which have to be added and removed from group. * Return lists of permissions which have to be added and removed from group.
*/ */
export const permissionsDiff = ( export const permissionsDiff = (
permissionGroup: PermissionGroupDetailsFragment, permissionGroup: PermissionGroupDetailsFragment | null | undefined,
formData: PermissionGroupDetailsPageFormData, formData: PermissionGroupDetailsPageFormData,
) => { ) => {
if (!permissionGroup) {
return {
addPermissions: [],
removePermissions: [],
};
}
const newPermissions = formData.permissions; const newPermissions = formData.permissions;
const oldPermissions = extractPermissionCodes(permissionGroup); const oldPermissions = extractPermissionCodes(permissionGroup);
@ -59,11 +75,18 @@ export const permissionsDiff = (
* Return lists of users which have to be added and removed from group. * Return lists of users which have to be added and removed from group.
*/ */
export const usersDiff = ( export const usersDiff = (
permissionGroup: PermissionGroupDetailsFragment, permissionGroup: PermissionGroupDetailsFragment | null | undefined,
formData: PermissionGroupDetailsPageFormData, formData: PermissionGroupDetailsPageFormData,
) => { ) => {
const newUsers = formData.users.map(u => u.id); if (!permissionGroup) {
const oldUsers = permissionGroup?.users.map(u => u.id); return {
addUsers: [],
removeUsers: [],
};
}
const newUsers = formData?.users?.map(u => u.id) ?? [];
const oldUsers = permissionGroup?.users?.map(u => u.id) ?? [];
return { return {
addUsers: difference(newUsers, oldUsers), addUsers: difference(newUsers, oldUsers),
@ -71,14 +94,130 @@ export const usersDiff = (
}; };
}; };
/**
* Return lists of channels which have to be added and removed from group.
*/
export const channelsDiff = (
permissionGroup: PermissionGroupDetailsFragment | null | undefined,
formData: PermissionGroupDetailsPageFormData,
allChannels: ChannelFragment[],
isGroupEditable: boolean,
) => {
if (!permissionGroup || !isGroupEditable) {
return {
addChannels: [],
removeChannels: [],
};
}
const newChannels = formData.hasAllChannels
? allChannels.map(c => c.id)
: formData.channels;
const oldChannels = permissionGroup?.accessibleChannels?.map(c => c.id) ?? [];
const hasRestrictedChannels =
permissionGroup?.restrictedAccessToChannels ?? false;
if (!hasRestrictedChannels) {
return {
addChannels: newChannels,
removeChannels: [],
};
}
return {
addChannels: difference(newChannels, oldChannels),
removeChannels: difference(oldChannels, newChannels),
};
};
/** /**
* Permissions are exceeded when group has permission which is not handled by user * Permissions are exceeded when group has permission which is not handled by user
*/ */
export const arePermissionsExceeded = ( export const arePermissionsExceeded = (
permissionGroup: PermissionGroupDetailsFragment, permissionGroup: PermissionGroupDetailsFragment | null | undefined,
user: UserFragment, user: UserFragment | null | undefined,
) => { ) => {
if (!permissionGroup || !user) {
return false;
}
const groupPermissions = extractPermissionCodes(permissionGroup); const groupPermissions = extractPermissionCodes(permissionGroup);
const userPermissions = user.userPermissions.map(p => p.code); const userPermissions = user?.userPermissions?.map(p => p.code) ?? [];
return difference(groupPermissions, userPermissions).length > 0; return difference(groupPermissions, userPermissions).length > 0;
}; };
/**
* Return lists of permission group accessible channels.
*/
export const mapAccessibleChannelsToChoice = (
permissionGroup: PermissionGroupDetailsFragment,
isUserAbleToEdit?: boolean,
): MultiAutocompleteChoiceType[] =>
permissionGroup?.accessibleChannels?.map(
channel =>
({
label: channel.name,
value: channel.id,
disabled: isUserAbleToEdit !== undefined ? !isUserAbleToEdit : false,
} as unknown as MultiAutocompleteChoiceType),
) ?? [];
export const checkIfUserBelongToPermissionGroup = (
permissionGroup: PermissionGroupDetailsFragment | null | undefined,
userId: string,
) => {
return permissionGroup?.users?.some(u => u.id === userId) ?? false;
};
/**
* Get channels options for select field.
*/
export const getUserAccessibleChannelsOptions = (
availableChannels: ChannelFragment[],
user?: UserContext["user"],
): ChannelFragment[] => {
if (!user) {
return [];
}
if (!user.restrictedAccessToChannels) {
return availableChannels;
}
if (user.accessibleChannels !== null) {
return user.accessibleChannels;
}
return [];
};
/**
* Check if user has restricted access to channels.
*/
export const checkIfUserHasRestictedAccessToChannels = (
user?: UserContext["user"],
) => {
if (user) {
return user.restrictedAccessToChannels;
}
return false;
};
export const getInitialChannels = (
permissionGroup: PermissionGroupDetailsFragment | null | undefined,
allChannelsLength: number,
) => {
if (!permissionGroup) {
return [];
}
if (
!permissionGroup?.restrictedAccessToChannels &&
permissionGroup?.accessibleChannels?.length === allChannelsLength
) {
return [];
}
return permissionGroup?.accessibleChannels?.map(channel => channel.id) ?? [];
};

View file

@ -1,5 +1,5 @@
// @ts-strict-ignore
import { useUser } from "@dashboard/auth"; import { useUser } from "@dashboard/auth";
import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext";
import { WindowTitle } from "@dashboard/components/WindowTitle"; import { WindowTitle } from "@dashboard/components/WindowTitle";
import { usePermissionGroupCreateMutation } from "@dashboard/graphql"; import { usePermissionGroupCreateMutation } from "@dashboard/graphql";
import useNavigator from "@dashboard/hooks/useNavigator"; import useNavigator from "@dashboard/hooks/useNavigator";
@ -7,20 +7,33 @@ import useNotifier from "@dashboard/hooks/useNotifier";
import useShop from "@dashboard/hooks/useShop"; import useShop from "@dashboard/hooks/useShop";
import { extractMutationErrors } from "@dashboard/misc"; import { extractMutationErrors } from "@dashboard/misc";
import { PermissionData } from "@dashboard/permissionGroups/components/PermissionGroupDetailsPage"; import { PermissionData } from "@dashboard/permissionGroups/components/PermissionGroupDetailsPage";
import React from "react"; import React, { useMemo } from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import PermissionGroupCreatePage, { import {
PermissionGroupCreateFormData, PermissionGroupCreateFormData,
PermissionGroupCreatePage,
} from "../../components/PermissionGroupCreatePage"; } from "../../components/PermissionGroupCreatePage";
import { permissionGroupDetailsUrl } from "../../urls"; import { permissionGroupDetailsUrl } from "../../urls";
import {
checkIfUserHasRestictedAccessToChannels,
getUserAccessibleChannelsOptions,
} from "../../utils";
const PermissionGroupCreateView: React.FC = () => { export const PermissionGroupCreate: React.FC = () => {
const navigate = useNavigator(); const navigate = useNavigator();
const notify = useNotifier(); const notify = useNotifier();
const intl = useIntl(); const intl = useIntl();
const shop = useShop(); const shop = useShop();
const user = useUser(); const user = useUser();
const { availableChannels } = useAppChannel(false);
const hasUserRestrictedAccessToChannels =
checkIfUserHasRestictedAccessToChannels(user.user);
const userAccessibleChannelsOptions = useMemo(
() => getUserAccessibleChannelsOptions(availableChannels, user.user),
[availableChannels, user.user],
);
const [createPermissionGroup, createPermissionGroupResult] = const [createPermissionGroup, createPermissionGroupResult] =
usePermissionGroupCreateMutation({ usePermissionGroupCreateMutation({
@ -33,32 +46,43 @@ const PermissionGroupCreateView: React.FC = () => {
defaultMessage: "Permission group created", defaultMessage: "Permission group created",
}), }),
}); });
if (data?.permissionGroupCreate?.group?.id) {
navigate( navigate(
permissionGroupDetailsUrl(data.permissionGroupCreate.group.id), permissionGroupDetailsUrl(data.permissionGroupCreate.group.id),
); );
} }
}
}, },
}); });
const errors = const errors =
createPermissionGroupResult?.data?.permissionGroupCreate?.errors || []; createPermissionGroupResult?.data?.permissionGroupCreate?.errors || [];
const onSubmit = (formData: PermissionGroupCreateFormData) => const onSubmit = (formData: PermissionGroupCreateFormData) => {
extractMutationErrors( const channelChoices = userAccessibleChannelsOptions.map(
channel => channel.id,
);
return extractMutationErrors(
createPermissionGroup({ createPermissionGroup({
variables: { variables: {
input: { input: {
addPermissions: formData.hasFullAccess addPermissions: formData.permissions,
? shop.permissions.map(perm => perm.code)
: formData.permissions,
addUsers: [], addUsers: [],
name: formData.name, name: formData.name,
addChannels: formData.hasAllChannels
? channelChoices
: formData.channels,
restrictedAccessToChannels:
hasUserRestrictedAccessToChannels || !formData.hasAllChannels,
}, },
}, },
}), }),
); );
};
const userPermissions = user?.user.userPermissions.map(p => p.code) || []; const userPermissions = user?.user?.userPermissions?.map(p => p.code) || [];
const permissions: PermissionData[] = const permissions: PermissionData[] =
shop?.permissions.map( shop?.permissions.map(
@ -80,15 +104,14 @@ const PermissionGroupCreateView: React.FC = () => {
})} })}
/> />
<PermissionGroupCreatePage <PermissionGroupCreatePage
errors={errors} errors={errors as any}
disabled={createPermissionGroupResult.loading} disabled={createPermissionGroupResult.loading}
permissions={permissions} permissions={permissions}
channels={userAccessibleChannelsOptions}
hasRestrictedChannels={hasUserRestrictedAccessToChannels}
saveButtonBarState={createPermissionGroupResult.status} saveButtonBarState={createPermissionGroupResult.status}
onSubmit={onSubmit} onSubmit={onSubmit}
/> />
</> </>
); );
}; };
PermissionGroupCreateView.displayName = "PermissionGroupCreateView";
export default PermissionGroupCreateView;

View file

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

View file

@ -1,8 +1,9 @@
// @ts-strict-ignore
import { useUser } from "@dashboard/auth"; import { useUser } from "@dashboard/auth";
import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext";
import { Button } from "@dashboard/components/Button"; import { Button } from "@dashboard/components/Button";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@dashboard/config"; import { DEFAULT_INITIAL_SEARCH_DATA } from "@dashboard/config";
import { import {
PermissionGroupDetailsQuery,
usePermissionGroupDetailsQuery, usePermissionGroupDetailsQuery,
usePermissionGroupUpdateMutation, usePermissionGroupUpdateMutation,
} from "@dashboard/graphql"; } from "@dashboard/graphql";
@ -14,11 +15,6 @@ import useStateFromProps from "@dashboard/hooks/useStateFromProps";
import { commonMessages } from "@dashboard/intl"; import { commonMessages } from "@dashboard/intl";
import { extractMutationErrors } from "@dashboard/misc"; import { extractMutationErrors } from "@dashboard/misc";
import MembersErrorDialog from "@dashboard/permissionGroups/components/MembersErrorDialog"; import MembersErrorDialog from "@dashboard/permissionGroups/components/MembersErrorDialog";
import {
arePermissionsExceeded,
permissionsDiff,
usersDiff,
} from "@dashboard/permissionGroups/utils";
import useStaffMemberSearch from "@dashboard/searches/useStaffMemberSearch"; import useStaffMemberSearch from "@dashboard/searches/useStaffMemberSearch";
import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers";
import createSortHandler from "@dashboard/utils/handlers/sortHandler"; import createSortHandler from "@dashboard/utils/handlers/sortHandler";
@ -28,7 +24,8 @@ import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import AssignMembersDialog from "../../components/AssignMembersDialog"; import AssignMembersDialog from "../../components/AssignMembersDialog";
import PermissionGroupDetailsPage, { import {
PermissionGroupDetailsPage,
PermissionGroupDetailsPageFormData, PermissionGroupDetailsPageFormData,
} from "../../components/PermissionGroupDetailsPage"; } from "../../components/PermissionGroupDetailsPage";
import UnassignMembersDialog from "../../components/UnassignMembersDialog"; import UnassignMembersDialog from "../../components/UnassignMembersDialog";
@ -37,12 +34,23 @@ import {
PermissionGroupDetailsUrlDialog, PermissionGroupDetailsUrlDialog,
PermissionGroupDetailsUrlQueryParams, PermissionGroupDetailsUrlQueryParams,
} from "../../urls"; } from "../../urls";
import {
arePermissionsExceeded,
channelsDiff,
checkIfUserBelongToPermissionGroup,
permissionsDiff,
usersDiff,
} from "../../utils";
interface PermissionGroupDetailsProps { interface PermissionGroupDetailsProps {
id: string; id: string;
params: PermissionGroupDetailsUrlQueryParams; params: PermissionGroupDetailsUrlQueryParams;
} }
type Members = NonNullable<
NonNullable<PermissionGroupDetailsQuery["permissionGroup"]>["users"]
>;
export const PermissionGroupDetails: React.FC<PermissionGroupDetailsProps> = ({ export const PermissionGroupDetails: React.FC<PermissionGroupDetailsProps> = ({
id, id,
params, params,
@ -55,11 +63,13 @@ export const PermissionGroupDetails: React.FC<PermissionGroupDetailsProps> = ({
const { data, loading, refetch } = usePermissionGroupDetailsQuery({ const { data, loading, refetch } = usePermissionGroupDetailsQuery({
displayLoader: true, displayLoader: true,
variables: { id, userId: user?.user.id }, variables: { id, userId: user?.user?.id ?? "" },
}); });
const [membersList, setMembersList] = useStateFromProps( const { availableChannels } = useAppChannel(false);
data?.permissionGroup.users,
const [membersList, setMembersList] = useStateFromProps<Members>(
data?.permissionGroup?.users ?? [],
); );
const { const {
@ -76,16 +86,30 @@ export const PermissionGroupDetails: React.FC<PermissionGroupDetailsProps> = ({
const [permissionGroupUpdate, permissionGroupUpdateResult] = const [permissionGroupUpdate, permissionGroupUpdateResult] =
usePermissionGroupUpdateMutation({ usePermissionGroupUpdateMutation({
onCompleted: data => { onCompleted: updatedData => {
if (data.permissionGroupUpdate.errors.length === 0) { if (updatedData?.permissionGroupUpdate?.errors?.length === 0) {
notify({ notify({
status: "success", status: "success",
text: intl.formatMessage(commonMessages.savedChanges), text: intl.formatMessage(commonMessages.savedChanges),
}); });
// When user belong to editedd permission group refetch user details
// as they are root of user accessible channels
if (
checkIfUserBelongToPermissionGroup(
data?.permissionGroup,
user?.user?.id ?? "",
)
) {
user.refetchUser();
}
refetch(); refetch();
closeModal(); closeModal();
} else if ( } else if (
data.permissionGroupUpdate.errors.some(e => e.field === "removeUsers") updatedData?.permissionGroupUpdate?.errors.some(
e => e.field === "removeUsers",
)
) { ) {
openModal("unassignError"); openModal("unassignError");
} }
@ -104,22 +128,25 @@ export const PermissionGroupDetails: React.FC<PermissionGroupDetailsProps> = ({
); );
const unassignMembers = () => { const unassignMembers = () => {
setMembersList(membersList?.filter(m => !listElements.includes(m.id))); setMembersList(
membersList?.filter(m => !listElements.includes(m.id)) ?? [],
);
closeModal(); closeModal();
}; };
const isGroupEditable = const isGroupEditable =
(data?.user.editableGroups || []).filter(g => g.id === id).length > 0; (data?.user?.editableGroups || []).filter(g => g.id === id).length > 0 &&
data?.permissionGroup?.userCanManage;
const lastSourcesOfPermission = (data?.user.userPermissions || []) const lastSourcesOfPermission = (data?.user?.userPermissions || [])
.filter( .filter(
perm => perm =>
perm.sourcePermissionGroups.length === 1 && perm.sourcePermissionGroups?.length === 1 &&
perm.sourcePermissionGroups[0].id === id, perm.sourcePermissionGroups?.[0].id === id,
) )
.map(perm => perm.code); .map(perm => perm.code);
const userPermissions = user?.user.userPermissions.map(p => p.code) || []; const userPermissions = user?.user?.userPermissions?.map(p => p.code) || [];
const permissions = (shop?.permissions || []).map(perm => ({ const permissions = (shop?.permissions || []).map(perm => ({
...perm, ...perm,
@ -129,9 +156,11 @@ export const PermissionGroupDetails: React.FC<PermissionGroupDetailsProps> = ({
const permissionsExceeded = arePermissionsExceeded( const permissionsExceeded = arePermissionsExceeded(
data?.permissionGroup, data?.permissionGroup,
user.user, user?.user,
); );
const disabled = loading || !isGroupEditable || permissionsExceeded;
const isLoading = loading || permissionGroupUpdateResult.loading;
const disabled = isLoading || !isGroupEditable || permissionsExceeded;
const handleSubmit = async (formData: PermissionGroupDetailsPageFormData) => const handleSubmit = async (formData: PermissionGroupDetailsPageFormData) =>
extractMutationErrors( extractMutationErrors(
@ -142,6 +171,13 @@ export const PermissionGroupDetails: React.FC<PermissionGroupDetailsProps> = ({
name: formData.name, name: formData.name,
...permissionsDiff(data?.permissionGroup, formData), ...permissionsDiff(data?.permissionGroup, formData),
...usersDiff(data?.permissionGroup, formData), ...usersDiff(data?.permissionGroup, formData),
...channelsDiff(
data?.permissionGroup,
formData,
availableChannels,
!!isGroupEditable,
),
restrictedAccessToChannels: !formData.hasAllChannels,
}, },
}, },
}), }),
@ -152,11 +188,13 @@ export const PermissionGroupDetails: React.FC<PermissionGroupDetailsProps> = ({
<PermissionGroupDetailsPage <PermissionGroupDetailsPage
permissionGroup={data?.permissionGroup} permissionGroup={data?.permissionGroup}
permissionsExceeded={permissionsExceeded} permissionsExceeded={permissionsExceeded}
members={membersList || []} isUserAbleToEditChannels={!!isGroupEditable}
channels={availableChannels}
members={membersList}
onAssign={() => openModal("assign")} onAssign={() => openModal("assign")}
onUnassign={ids => openModal("unassign", { ids })} onUnassign={ids => openModal("unassign", { ids })}
errors={ errors={
permissionGroupUpdateResult?.data?.permissionGroupUpdate.errors || [] permissionGroupUpdateResult?.data?.permissionGroupUpdate?.errors ?? []
} }
onSubmit={handleSubmit} onSubmit={handleSubmit}
permissions={permissions} permissions={permissions}
@ -183,20 +221,17 @@ export const PermissionGroupDetails: React.FC<PermissionGroupDetailsProps> = ({
/> />
<AssignMembersDialog <AssignMembersDialog
loading={searchResult.loading} loading={searchResult.loading}
staffMembers={mapEdgesToItems(searchResult?.data?.search)} staffMembers={mapEdgesToItems(searchResult?.data?.search) ?? []}
onSearchChange={search} onSearchChange={search}
onFetchMore={loadMore} onFetchMore={loadMore}
disabled={disabled} disabled={disabled}
hasMore={searchResult?.data?.search.pageInfo.hasNextPage} hasMore={searchResult?.data?.search?.pageInfo?.hasNextPage ?? false}
initialSearch="" initialSearch=""
confirmButtonState={permissionGroupUpdateResult.status} confirmButtonState={permissionGroupUpdateResult.status}
open={params.action === "assign"} open={params.action === "assign"}
onClose={closeModal} onClose={closeModal}
onSubmit={formData => { onSubmit={formData => {
setMembersList([ setMembersList([...(membersList ?? []), ...formData] as Members);
...membersList,
...formData.filter(member => !membersList.includes(member)),
]);
closeModal(); closeModal();
}} }}
/> />
@ -216,5 +251,3 @@ export const PermissionGroupDetails: React.FC<PermissionGroupDetailsProps> = ({
</> </>
); );
}; };
export default PermissionGroupDetails;

View file

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

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
import { import {
PermissionGroupErrorFragment, PermissionGroupErrorFragment,
usePermissionGroupDeleteMutation, usePermissionGroupDeleteMutation,
@ -60,7 +59,7 @@ export const PermissionGroupList: React.FC<PermissionGroupListProps> = ({
}); });
const paginationValues = usePaginator({ const paginationValues = usePaginator({
pageInfo: data?.permissionGroups.pageInfo, pageInfo: data?.permissionGroups?.pageInfo,
paginationState, paginationState,
queryString: params, queryString: params,
}); });
@ -76,13 +75,13 @@ export const PermissionGroupList: React.FC<PermissionGroupListProps> = ({
PermissionGroupListUrlQueryParams PermissionGroupListUrlQueryParams
>(navigate, permissionGroupListUrl, params); >(navigate, permissionGroupListUrl, params);
const permissionGroups = mapEdgesToItems(data?.permissionGroups); const permissionGroups = mapEdgesToItems(data?.permissionGroups) ?? [];
const [deleteError, setDeleteError] = const [deleteError, setDeleteError] =
React.useState<PermissionGroupErrorFragment>(); React.useState<PermissionGroupErrorFragment>();
const [permissionGroupDelete] = usePermissionGroupDeleteMutation({ const [permissionGroupDelete] = usePermissionGroupDeleteMutation({
onCompleted: data => { onCompleted: data => {
if (data.permissionGroupDelete.errors.length === 0) { if (data?.permissionGroupDelete?.errors?.length === 0) {
notify({ notify({
status: "success", status: "success",
text: intl.formatMessage({ text: intl.formatMessage({
@ -94,7 +93,7 @@ export const PermissionGroupList: React.FC<PermissionGroupListProps> = ({
setDeleteError(undefined); setDeleteError(undefined);
closeModal(); closeModal();
} else { } else {
setDeleteError(data.permissionGroupDelete.errors[0]); setDeleteError(data?.permissionGroupDelete?.errors?.[0]);
} }
}, },
}); });
@ -114,7 +113,7 @@ export const PermissionGroupList: React.FC<PermissionGroupListProps> = ({
onConfirm={() => onConfirm={() =>
permissionGroupDelete({ permissionGroupDelete({
variables: { variables: {
id: params.id, id: params?.id ?? "",
}, },
}) })
} }

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
import { PermissionGroupSortField } from "@dashboard/graphql"; import { PermissionGroupSortField } from "@dashboard/graphql";
import { PermissionGroupListUrlSortField } from "@dashboard/permissionGroups/urls"; import { PermissionGroupListUrlSortField } from "@dashboard/permissionGroups/urls";
import { createGetSortQueryVariables } from "@dashboard/utils/sort"; import { createGetSortQueryVariables } from "@dashboard/utils/sort";
@ -10,7 +9,7 @@ export function getSortQueryField(
case PermissionGroupListUrlSortField.name: case PermissionGroupListUrlSortField.name:
return PermissionGroupSortField.NAME; return PermissionGroupSortField.NAME;
default: default:
return undefined; return "" as PermissionGroupSortField;
} }
} }

View file

@ -81,7 +81,7 @@ export interface SortPage<TSortKey extends string> {
export interface ListActionsWithoutToolbar { export interface ListActionsWithoutToolbar {
toggle: (id: string) => void; toggle: (id: string) => void;
toggleAll: (items: React.ReactNodeArray, selected: number) => void; toggleAll: (items: Node[], selected: number) => void;
isChecked: (id: string) => boolean; isChecked: (id: string) => boolean;
selected: number; selected: number;
} }

View file

@ -30,7 +30,7 @@ const messages = defineMessages({
}); });
function getPermissionGroupErrorMessage( function getPermissionGroupErrorMessage(
err: PermissionGroupErrorFragment, err: PermissionGroupErrorFragment | undefined,
intl: IntlShape, intl: IntlShape,
): string | undefined { ): string | undefined {
if (err) { if (err) {