From a63af3ab73ac63268f7f95a5394cf67b1f26f579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chy=C5=82a?= Date: Fri, 4 Aug 2023 12:42:04 +0200 Subject: [PATCH] Assign channel to permission group (#3515) --- .changeset/twenty-ways-reflect.md | 5 + .storybook/helpers/MockedUserProvider.tsx | 3 + introspection.json | 26 +- locale/defaultMessages.json | 19 +- src/auth/hooks/useAuthProvider.ts | 1 + src/auth/hooks/useUserAccessibleChannels.ts | 11 + src/auth/index.tsx | 1 + src/auth/types.ts | 4 +- .../AccountPermissions/AccountPermissions.tsx | 250 +++---- .../components/Header/Header.tsx | 32 + .../components/Header/index.ts | 1 + .../PermissionExeeded/PermissionExeeded.tsx | 54 ++ .../components/PermissionExeeded/index.ts | 1 + .../PermissionList/PermissionList.tsx | 79 +++ .../components/PermissionList/index.ts | 1 + src/components/AccountPermissions/messages.ts | 25 + src/components/AccountPermissions/utils.ts | 25 +- .../AppLayout/AppChannelContext.tsx | 16 +- src/components/AppLayout/TopNav/Root.tsx | 10 +- .../ChannelPermission.test.tsx | 135 ++++ .../ChannelPermission/ChannelPermission.tsx | 83 +++ src/components/ChannelPermission/fixtures.ts | 72 ++ src/components/ChannelPermission/index.ts | 1 + src/components/ChannelPermission/messages.ts | 20 + src/components/Shop/queries.ts | 3 +- .../ConfigurationPage.stories.tsx | 2 + src/fixtures.ts | 39 ++ src/fragments/auth.ts | 15 +- src/fragments/permissionGroups.ts | 4 + src/graphql/hooks.generated.ts | 78 ++- src/graphql/types.generated.ts | 18 +- src/home/queries.ts | 16 +- src/home/views/index.tsx | 15 +- .../OrderDraftListHeader.tsx | 41 +- .../OrderListPage/OrderListPage.tsx | 71 +- .../views/OrderDraftList/OrderDraftList.tsx | 8 +- src/orders/views/OrderList/OrderList.tsx | 9 +- .../PermissionGroupCreatePage.stories.tsx | 12 +- .../PermissionGroupCreatePage.tsx | 145 ++-- .../PermissionGroupCreatePage/index.ts | 1 - .../PermissionGroupDetailsPage.stories.tsx | 50 +- .../PermissionGroupDetailsPage.tsx | 181 +++-- .../PermissionGroupDetailsPage/index.ts | 1 - src/permissionGroups/fixtures.ts | 48 +- src/permissionGroups/index.tsx | 4 +- src/permissionGroups/sort.ts | 14 +- src/permissionGroups/utils.test.ts | 662 ++++++++++++++++++ src/permissionGroups/utils.ts | 165 ++++- .../PermissionGroupCreate.tsx | 57 +- .../views/PermissionGroupCreate/index.ts | 1 - .../PermissionGroupDetails.tsx | 95 ++- .../views/PermissionGroupDetails/index.ts | 1 - .../PermissionGroupList.tsx | 11 +- .../views/PermissionGroupList/sort.ts | 3 +- src/types.ts | 2 +- src/utils/errors/permissionGroups.ts | 2 +- 56 files changed, 2121 insertions(+), 528 deletions(-) create mode 100644 .changeset/twenty-ways-reflect.md create mode 100644 src/auth/hooks/useUserAccessibleChannels.ts create mode 100644 src/components/AccountPermissions/components/Header/Header.tsx create mode 100644 src/components/AccountPermissions/components/Header/index.ts create mode 100644 src/components/AccountPermissions/components/PermissionExeeded/PermissionExeeded.tsx create mode 100644 src/components/AccountPermissions/components/PermissionExeeded/index.ts create mode 100644 src/components/AccountPermissions/components/PermissionList/PermissionList.tsx create mode 100644 src/components/AccountPermissions/components/PermissionList/index.ts create mode 100644 src/components/AccountPermissions/messages.ts create mode 100644 src/components/ChannelPermission/ChannelPermission.test.tsx create mode 100644 src/components/ChannelPermission/ChannelPermission.tsx create mode 100644 src/components/ChannelPermission/fixtures.ts create mode 100644 src/components/ChannelPermission/index.ts create mode 100644 src/components/ChannelPermission/messages.ts create mode 100644 src/permissionGroups/utils.test.ts diff --git a/.changeset/twenty-ways-reflect.md b/.changeset/twenty-ways-reflect.md new file mode 100644 index 000000000..3ef81fcda --- /dev/null +++ b/.changeset/twenty-ways-reflect.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": minor +--- + +Assign channel permission to permission group diff --git a/.storybook/helpers/MockedUserProvider.tsx b/.storybook/helpers/MockedUserProvider.tsx index 47f1ad61a..f201b88e3 100644 --- a/.storybook/helpers/MockedUserProvider.tsx +++ b/.storybook/helpers/MockedUserProvider.tsx @@ -15,6 +15,7 @@ export const MockedUserProvider: React.FC<{ requestLoginByExternalPlugin: undefined, authenticating: false, authenticated: false, + refetchUser: undefined, user: { id: "0", email: "email@email.me", @@ -24,6 +25,8 @@ export const MockedUserProvider: React.FC<{ userPermissions: customPermissions ?? adminUserPermissions, avatar: null, __typename: "User", + accessibleChannels: [], + restrictedAccessToChannels: false, }, errors: [], }} diff --git a/introspection.json b/introspection.json index 4856ca403..b259c4938 100644 --- a/introspection.json +++ b/introspection.json @@ -130685,10 +130685,7 @@ "name": "deprecated", "description": "Marks an element of a GraphQL schema as no longer supported.", "isRepeatable": false, - "locations": [ - "ENUM_VALUE", - "FIELD_DEFINITION" - ], + "locations": ["ENUM_VALUE", "FIELD_DEFINITION"], "args": [ { "name": "reason", @@ -130738,11 +130735,7 @@ "name": "include", "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", "isRepeatable": false, - "locations": [ - "FIELD", - "FRAGMENT_SPREAD", - "INLINE_FRAGMENT" - ], + "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], "args": [ { "name": "if", @@ -130766,11 +130759,7 @@ "name": "skip", "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", "isRepeatable": false, - "locations": [ - "FIELD", - "FRAGMENT_SPREAD", - "INLINE_FRAGMENT" - ], + "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], "args": [ { "name": "if", @@ -130794,12 +130783,7 @@ "name": "webhookEventsInfo", "description": "Webhook events triggered by a specific location.", "isRepeatable": false, - "locations": [ - "FIELD", - "FIELD_DEFINITION", - "INPUT_OBJECT", - "OBJECT" - ], + "locations": ["FIELD", "FIELD_DEFINITION", "INPUT_OBJECT", "OBJECT"], "args": [ { "name": "asyncEvents", @@ -130853,4 +130837,4 @@ } ] } -} \ No newline at end of file +} diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 0dd73b262..d250bd41d 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -349,6 +349,9 @@ "context": "min price in channel", "string": "Min. value" }, + "0HBlkO": { + "string": "Search channels" + }, "0KmZCN": { "context": "button", "string": "Open playground" @@ -1509,6 +1512,9 @@ "context": "modal button images upload", "string": "Upload Images" }, + "9FGTOt": { + "string": "Allow access to orders of all channels" + }, "9IWg/f": { "context": "button", "string": "SETUP END DATE" @@ -6248,6 +6254,9 @@ "context": "page header", "string": "Create Page" }, + "grkY2V": { + "string": "You don't have access to any channels" + }, "gvOzOl": { "string": "Page Title" }, @@ -6979,10 +6988,6 @@ "context": "add authorization key error", "string": "Authorization key with this type already exists" }, - "mAabef": { - "context": "checkbox label", - "string": "Group has full access to the store" - }, "mCP0UD": { "context": "order draft creation date", "string": "Date" @@ -8323,10 +8328,16 @@ "context": "button", "string": "Create category" }, + "vprU7C": { + "string": "Select visible order channels" + }, "vwMO04": { "context": "draft order", "string": "Created" }, + "vz3yxp": { + "string": "Channels permissions" + }, "vzce9B": { "context": "customer gift cards card subtitle", "string": "Only five newest gift cards are shown here" diff --git a/src/auth/hooks/useAuthProvider.ts b/src/auth/hooks/useAuthProvider.ts index 83755242f..33c601320 100644 --- a/src/auth/hooks/useAuthProvider.ts +++ b/src/auth/hooks/useAuthProvider.ts @@ -245,6 +245,7 @@ export function useAuthProvider({ authenticating: authenticating && !errors.length, authenticated: authenticated && !!user?.isStaff && !errors.length, user: userDetails.data?.me, + refetchUser: userDetails.refetch, errors, }; } diff --git a/src/auth/hooks/useUserAccessibleChannels.ts b/src/auth/hooks/useUserAccessibleChannels.ts new file mode 100644 index 000000000..671e60097 --- /dev/null +++ b/src/auth/hooks/useUserAccessibleChannels.ts @@ -0,0 +1,11 @@ +import { useUser } from ".."; + +export const useUserAccessibleChannels = () => { + const user = useUser(); + + if (!user?.user?.accessibleChannels) { + return []; + } + + return user.user.accessibleChannels; +}; diff --git a/src/auth/index.tsx b/src/auth/index.tsx index 26eb821ef..ee817cca7 100644 --- a/src/auth/index.tsx +++ b/src/auth/index.tsx @@ -31,6 +31,7 @@ export const UserContext = React.createContext({ authenticating: false, authenticated: false, errors: [], + refetchUser: undefined, }); const AuthRouter: React.FC = () => ( diff --git a/src/auth/types.ts b/src/auth/types.ts index 866c32f0d..3aa49c5b5 100644 --- a/src/auth/types.ts +++ b/src/auth/types.ts @@ -1,4 +1,5 @@ -import { UserFragment } from "@dashboard/graphql"; +import { ApolloQueryResult } from "@apollo/client"; +import { UserDetailsQuery, UserFragment } from "@dashboard/graphql"; import { GetExternalAccessTokenData, GetExternalAuthUrlData, @@ -44,4 +45,5 @@ export interface UserContext { authenticating: boolean; authenticated: boolean; errors: UserContextError[]; + refetchUser: () => Promise>; } diff --git a/src/components/AccountPermissions/AccountPermissions.tsx b/src/components/AccountPermissions/AccountPermissions.tsx index e11eaf2a3..17a6626e0 100644 --- a/src/components/AccountPermissions/AccountPermissions.tsx +++ b/src/components/AccountPermissions/AccountPermissions.tsx @@ -1,42 +1,13 @@ -// @ts-strict-ignore import { useUser } from "@dashboard/auth"; -import CardTitle from "@dashboard/components/CardTitle"; -import Skeleton from "@dashboard/components/Skeleton"; -import { PermissionData } from "@dashboard/permissionGroups/components/PermissionGroupDetailsPage/PermissionGroupDetailsPage"; -import { - Card, - CardContent, - Checkbox, - List, - ListItem, - ListItemIcon, - ListItemText, - Typography, -} from "@material-ui/core"; -import { makeStyles } from "@saleor/macaw-ui"; -import React from "react"; +import { PermissionData } from "@dashboard/permissionGroups/components/PermissionGroupDetailsPage"; +import { Box, Text } from "@saleor/macaw-ui/next"; +import React, { ChangeEvent } from "react"; import { useIntl } from "react-intl"; -const byAlphabeticalOrder = - (field: string) => - (a: T, b: T) => - a[field].localeCompare(b[field]); - -const useStyles = makeStyles( - theme => ({ - checkboxContainer: { - marginTop: theme.spacing(), - }, - hr: { - backgroundColor: theme.palette.divider, - border: "none", - height: 1, - marginBottom: 0, - marginTop: 0, - }, - }), - { name: "AccountPermissions" }, -); +import { Header } from "./components/Header"; +import { PermissionsExceeded } from "./components/PermissionExeeded"; +import { PermissionList } from "./components/PermissionList"; +import { messages } from "./messages"; interface AccountPermissionsProps { permissions: PermissionData[]; @@ -47,7 +18,7 @@ interface AccountPermissionsProps { }; disabled: boolean; description: string; - errorMessage: string; + errorMessage: string | undefined; fullAccessLabel: string; onChange: (event: React.ChangeEvent, cb?: () => void) => void; } @@ -63,11 +34,10 @@ const AccountPermissions: React.FC = props => { errorMessage, } = props; - const permissions = Object.values(props?.permissions ?? {}).sort( - byAlphabeticalOrder("name"), + const permissions = Object.values(props?.permissions ?? {}).sort((a, b) => + a.name.localeCompare(b.name), ); - const classes = useStyles(props); const intl = useIntl(); const { user } = useUser(); @@ -75,161 +45,99 @@ const AccountPermissions: React.FC = props => { onChange({ target: { 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); + onChange({ target: { name: "hasFullAccess", value: !data.hasFullAccess, }, - } as any); + } as ChangeEvent); }; - 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); + onChange({ target: { name: "permissions", - value: !value - ? data.permissions.concat([key]) - : data.permissions.filter(perm => perm !== key), + value: updatedPersmissions, }, - } as any); + } as ChangeEvent); }; return ( - - + + + {intl.formatMessage(messages.title)} + + {permissionsExceeded && ( - <> - - - {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", - })} - - -
- - - {intl.formatMessage({ - id: "6cS4Rd", - defaultMessage: "Available permissions", - description: "card section description", - })} - - - {user.userPermissions.map(perm => ( - - - - ))} - - - + )} + {!permissionsExceeded && ( <> - - {description} - - - - - - - - {!data.hasFullAccess && ( - <> -
- - {permissions === undefined ? ( - - ) : ( - permissions.map(perm => ( - userPerm === perm.code, - ).length === 1, - )} - > - - userPerm === perm.code, - ).length === 1 - } - tabIndex={-1} - disableRipple - name={perm.code} - inputProps={{ "aria-labelledby": perm.code }} - /> - - - - )) - )} - - - )} +
+ + + + + {!!errorMessage && ( <> -
- - - {errorMessage} - - + + + {errorMessage} + )} )} - + ); }; diff --git a/src/components/AccountPermissions/components/Header/Header.tsx b/src/components/AccountPermissions/components/Header/Header.tsx new file mode 100644 index 000000000..65a62aa2a --- /dev/null +++ b/src/components/AccountPermissions/components/Header/Header.tsx @@ -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) => ( + + {description} + + + {fullAccessLabel} + + + +); diff --git a/src/components/AccountPermissions/components/Header/index.ts b/src/components/AccountPermissions/components/Header/index.ts new file mode 100644 index 000000000..9e08a64db --- /dev/null +++ b/src/components/AccountPermissions/components/Header/index.ts @@ -0,0 +1 @@ +export * from "./Header"; diff --git a/src/components/AccountPermissions/components/PermissionExeeded/PermissionExeeded.tsx b/src/components/AccountPermissions/components/PermissionExeeded/PermissionExeeded.tsx new file mode 100644 index 000000000..0e9cb859d --- /dev/null +++ b/src/components/AccountPermissions/components/PermissionExeeded/PermissionExeeded.tsx @@ -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 ( + <> + + {intl.formatMessage(messages.exeededPermission)} + + + + + + {intl.formatMessage(messages.availablePermissions)} + + + + {userPermissions.map(perm => ( + + {`- ${perm.name}`} + + ))} + + + ); +}; diff --git a/src/components/AccountPermissions/components/PermissionExeeded/index.ts b/src/components/AccountPermissions/components/PermissionExeeded/index.ts new file mode 100644 index 000000000..1a5edde94 --- /dev/null +++ b/src/components/AccountPermissions/components/PermissionExeeded/index.ts @@ -0,0 +1 @@ +export * from "./PermissionExeeded"; diff --git a/src/components/AccountPermissions/components/PermissionList/PermissionList.tsx b/src/components/AccountPermissions/components/PermissionList/PermissionList.tsx new file mode 100644 index 000000000..207c60753 --- /dev/null +++ b/src/components/AccountPermissions/components/PermissionList/PermissionList.tsx @@ -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 ( + + + + ); + } + + return ( + + {permissions.map(perm => ( + + onPermissionChange( + perm.code, + hasPermissionSelected(selectedPermissions, perm.code), + ) + } + > + + + + + + ))} + + ); +}; diff --git a/src/components/AccountPermissions/components/PermissionList/index.ts b/src/components/AccountPermissions/components/PermissionList/index.ts new file mode 100644 index 000000000..91cadcbe1 --- /dev/null +++ b/src/components/AccountPermissions/components/PermissionList/index.ts @@ -0,0 +1 @@ +export * from "./PermissionList"; diff --git a/src/components/AccountPermissions/messages.ts b/src/components/AccountPermissions/messages.ts new file mode 100644 index 000000000..4df3a5c2f --- /dev/null +++ b/src/components/AccountPermissions/messages.ts @@ -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", + }, +}); diff --git a/src/components/AccountPermissions/utils.ts b/src/components/AccountPermissions/utils.ts index cbcaea78d..72c303324 100644 --- a/src/components/AccountPermissions/utils.ts +++ b/src/components/AccountPermissions/utils.ts @@ -1,25 +1,27 @@ -// @ts-strict-ignore import { - PermissionGroupDetailsQuery, - ShopInfoQuery, - UserDetailsQuery, + PermissionEnum, + PermissionFragment, + UserPermissionFragment, + UserUserPermissionWithSourcePermissionGroupsFragment, } from "@dashboard/graphql"; export const getLastSourcesOfPermission = ( groupId: string, - userPermissions: PermissionGroupDetailsQuery["user"]["userPermissions"], + userPermissions: Array< + NonNullable + >, ) => userPermissions .filter( perm => - perm.sourcePermissionGroups.length === 1 && - perm.sourcePermissionGroups[0].id === groupId, + perm.sourcePermissionGroups?.length === 1 && + perm.sourcePermissionGroups[0]?.id === groupId, ) .map(perm => perm.code); export const getPermissionsComponentChoices = ( - userPermissions: UserDetailsQuery["me"]["userPermissions"], - shopPermissions: ShopInfoQuery["shop"]["permissions"], + userPermissions: UserPermissionFragment[], + shopPermissions: PermissionFragment[], lastSourcesOfPermissionIds: string[], ) => { const userCodes = userPermissions.map(p => p.code) || []; @@ -31,3 +33,8 @@ export const getPermissionsComponentChoices = ( lastSource: lastSourcesOfPermissionIds.includes(perm.code), })); }; + +export const hasPermissionSelected = ( + permissions: string[], + permissionCode: PermissionEnum, +) => permissions.filter(userPerm => userPerm === permissionCode).length === 1; diff --git a/src/components/AppLayout/AppChannelContext.tsx b/src/components/AppLayout/AppChannelContext.tsx index 41991db98..2acb110db 100644 --- a/src/components/AppLayout/AppChannelContext.tsx +++ b/src/components/AppLayout/AppChannelContext.tsx @@ -44,13 +44,17 @@ export const AppChannelProvider: React.FC = ({ children }) => { const [isPickerActive, setPickerActive] = React.useState(false); React.useEffect(() => { - if ( - !isValidChannel(selectedChannel, channelData?.channels) && - channelData?.channels?.length > 0 - ) { - setSelectedChannel(channelData.channels[0].id); + const channels = user?.accessibleChannels ?? []; + const isValid = isValidChannel(selectedChannel, channels); + + if (!isValid && channels?.length > 0) { + setSelectedChannel(channels[0].id); } - }, [channelData]); + + if (!isValid && selectedChannel !== "") { + setSelectedChannel(""); + } + }, [selectedChannel, setSelectedChannel, user]); React.useEffect(() => { setChannel(selectedChannel); diff --git a/src/components/AppLayout/TopNav/Root.tsx b/src/components/AppLayout/TopNav/Root.tsx index 306d90d23..9344d59e5 100644 --- a/src/components/AppLayout/TopNav/Root.tsx +++ b/src/components/AppLayout/TopNav/Root.tsx @@ -1,3 +1,4 @@ +import { useUser } from "@dashboard/auth"; import { Box, Text } from "@saleor/macaw-ui/next"; import React, { PropsWithChildren } from "react"; @@ -20,8 +21,9 @@ export const Root: React.FC> = ({ isAlignToRight = true, children, }) => { - const { availableChannels, channel, isPickerActive, setChannel } = - useAppChannel(false); + const { channel, isPickerActive, setChannel } = useAppChannel(false); + const user = useUser(); + const channels = user?.user?.accessibleChannels ?? []; return ( @@ -37,9 +39,9 @@ export const Root: React.FC> = ({ height="100%" __flex={isAlignToRight ? "initial" : 1} > - {isPickerActive && ( + {isPickerActive && channels.length > 0 && ( diff --git a/src/components/ChannelPermission/ChannelPermission.test.tsx b/src/components/ChannelPermission/ChannelPermission.test.tsx new file mode 100644 index 000000000..cbcc6305a --- /dev/null +++ b/src/components/ChannelPermission/ChannelPermission.test.tsx @@ -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( + , + ); + + // 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( + , + ); + + // 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( + , + ); + + // 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( + 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( + 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)); + }); +}); diff --git a/src/components/ChannelPermission/ChannelPermission.tsx b/src/components/ChannelPermission/ChannelPermission.tsx new file mode 100644 index 000000000..5b9d73fdb --- /dev/null +++ b/src/components/ChannelPermission/ChannelPermission.tsx @@ -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 ( + + + {intl.formatMessage(messages.title)} + + + + {description && ( + + {description} + + )} + + + + + {intl.formatMessage(messages.allowAllChannels)} + + + + + {!hasAllChannels && ( + + + selectedChannels.includes(channel.value), + )} + placeholder={intl.formatMessage(messages.searchChannels)} + onChange={values => { + onChannelChange({ + target: { name: "channels", value: values.map(v => v.value) }, + }); + }} + /> + + )} + + + ); +}; diff --git a/src/components/ChannelPermission/fixtures.ts b/src/components/ChannelPermission/fixtures.ts new file mode 100644 index 000000000..d66a967d7 --- /dev/null +++ b/src/components/ChannelPermission/fixtures.ts @@ -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, + }, + }, +]; diff --git a/src/components/ChannelPermission/index.ts b/src/components/ChannelPermission/index.ts new file mode 100644 index 000000000..db56dc14f --- /dev/null +++ b/src/components/ChannelPermission/index.ts @@ -0,0 +1 @@ +export * from "./ChannelPermission"; diff --git a/src/components/ChannelPermission/messages.ts b/src/components/ChannelPermission/messages.ts new file mode 100644 index 000000000..14abfbf84 --- /dev/null +++ b/src/components/ChannelPermission/messages.ts @@ -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", + }, +}); diff --git a/src/components/Shop/queries.ts b/src/components/Shop/queries.ts index dee9de278..a159e87c2 100644 --- a/src/components/Shop/queries.ts +++ b/src/components/Shop/queries.ts @@ -27,8 +27,7 @@ export const shopInfo = gql` name trackInventoryByDefault permissions { - code - name + ...Permission } version } diff --git a/src/configuration/ConfigurationPage.stories.tsx b/src/configuration/ConfigurationPage.stories.tsx index f35ef6f46..d89fc156e 100644 --- a/src/configuration/ConfigurationPage.stories.tsx +++ b/src/configuration/ConfigurationPage.stories.tsx @@ -24,6 +24,8 @@ const user = { lastName: "Newton", note: null, userPermissions: staffMember.userPermissions, + restrictedAccessToChannels: false, + accessibleChannels: [], }; const versions = { diff --git a/src/fixtures.ts b/src/fixtures.ts index 370a919e2..d443e3a1d 100644 --- a/src/fixtures.ts +++ b/src/fixtures.ts @@ -1,5 +1,7 @@ // @ts-strict-ignore import { + AllocationStrategyEnum, + ChannelFragment, PermissionEnum, ShopInfoQuery, ShopLimitFragment, @@ -355,6 +357,43 @@ export const sortPageProps: SortPage = { }, }; +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"] = [ { code: PermissionEnum.MANAGE_DISCOUNTS, diff --git a/src/fragments/auth.ts b/src/fragments/auth.ts index 98b0dc105..0b0f7c522 100644 --- a/src/fragments/auth.ts +++ b/src/fragments/auth.ts @@ -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` fragment User on User { id @@ -17,9 +26,13 @@ export const fragmentUser = gql` userPermissions { ...UserPermission } - avatar { + avatar(size: 128) { url } + accessibleChannels { + ...Channel + } + restrictedAccessToChannels } `; diff --git a/src/fragments/permissionGroups.ts b/src/fragments/permissionGroups.ts index 932ce2678..a7dd57a3f 100644 --- a/src/fragments/permissionGroups.ts +++ b/src/fragments/permissionGroups.ts @@ -32,6 +32,10 @@ export const permissionGroupMember = gql` export const permissionGroupDetailsFragment = gql` fragment PermissionGroupDetails on Group { ...PermissionGroup + restrictedAccessToChannels + accessibleChannels { + ...Channel + } permissions { ...Permission } diff --git a/src/graphql/hooks.generated.ts b/src/graphql/hooks.generated.ts index e36bebbb6..1a89e4f66 100644 --- a/src/graphql/hooks.generated.ts +++ b/src/graphql/hooks.generated.ts @@ -171,6 +171,30 @@ export const UserPermissionFragmentDoc = gql` 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` fragment User on User { id @@ -181,11 +205,16 @@ export const UserFragmentDoc = gql` userPermissions { ...UserPermission } - avatar { + avatar(size: 128) { url } + accessibleChannels { + ...Channel + } + restrictedAccessToChannels } - ${UserPermissionFragmentDoc}`; + ${UserPermissionFragmentDoc} +${ChannelFragmentDoc}`; export const CategoryFragmentDoc = gql` fragment Category on Category { id @@ -223,22 +252,6 @@ export const ChannelErrorFragmentDoc = gql` message } `; -export const ChannelFragmentDoc = gql` - fragment Channel on Channel { - id - isActive - name - slug - currencyCode - defaultCountry { - code - country - } - stockSettings { - allocationStrategy - } -} - `; export const WarehouseFragmentDoc = gql` fragment Warehouse on Warehouse { id @@ -2117,6 +2130,10 @@ export const PermissionGroupMemberFragmentDoc = gql` export const PermissionGroupDetailsFragmentDoc = gql` fragment PermissionGroupDetails on Group { ...PermissionGroup + restrictedAccessToChannels + accessibleChannels { + ...Channel + } permissions { ...Permission } @@ -2125,6 +2142,7 @@ export const PermissionGroupDetailsFragmentDoc = gql` } } ${PermissionGroupFragmentDoc} +${ChannelFragmentDoc} ${PermissionFragmentDoc} ${PermissionGroupMemberFragmentDoc}`; export const PluginConfigurationBaseFragmentDoc = gql` @@ -6138,14 +6156,14 @@ export const ShopInfoDocument = gql` name trackInventoryByDefault permissions { - code - name + ...Permission } version } } ${CountryWithCodeFragmentDoc} -${LanguageFragmentDoc}`; +${LanguageFragmentDoc} +${PermissionFragmentDoc}`; /** * __useShopInfoQuery__ @@ -8618,20 +8636,20 @@ export type CustomerGiftCardListQueryHookResult = ReturnType; export type CustomerGiftCardListQueryResult = Apollo.QueryResult; export const HomeDocument = gql` - query Home($channel: String!, $datePeriod: DateRangeInput!, $PERMISSION_MANAGE_PRODUCTS: Boolean!, $PERMISSION_MANAGE_ORDERS: Boolean!) { - salesToday: ordersTotal(period: TODAY, channel: $channel) @include(if: $PERMISSION_MANAGE_ORDERS) { + query Home($channel: String!, $datePeriod: DateRangeInput!, $hasPermissionToManageProducts: Boolean!, $hasPermissionToManageOrders: Boolean!) { + salesToday: ordersTotal(period: TODAY, channel: $channel) @include(if: $hasPermissionToManageOrders) { gross { amount currency } } - ordersToday: orders(filter: {created: $datePeriod}, channel: $channel) @include(if: $PERMISSION_MANAGE_ORDERS) { + ordersToday: orders(filter: {created: $datePeriod}, channel: $channel) @include(if: $hasPermissionToManageOrders) { 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 } - 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 } productsOutOfStock: products( @@ -8640,7 +8658,7 @@ export const HomeDocument = gql` ) { 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 { node { 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 { node { amount @@ -8705,8 +8723,8 @@ export const HomeDocument = gql` * variables: { * channel: // value for 'channel' * datePeriod: // value for 'datePeriod' - * PERMISSION_MANAGE_PRODUCTS: // value for 'PERMISSION_MANAGE_PRODUCTS' - * PERMISSION_MANAGE_ORDERS: // value for 'PERMISSION_MANAGE_ORDERS' + * hasPermissionToManageProducts: // value for 'hasPermissionToManageProducts' + * hasPermissionToManageOrders: // value for 'hasPermissionToManageOrders' * }, * }); */ diff --git a/src/graphql/types.generated.ts b/src/graphql/types.generated.ts index c1cc4de3c..5f1e511c0 100644 --- a/src/graphql/types.generated.ts +++ b/src/graphql/types.generated.ts @@ -8368,7 +8368,7 @@ export type AvailableExternalAuthenticationsQuery = { __typename: 'Query', shop: 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<{ 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 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 }; @@ -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 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 }; @@ -9691,8 +9693,8 @@ export type CustomerGiftCardListQuery = { __typename: 'Query', giftCards: { __ty export type HomeQueryVariables = Exact<{ channel: Scalars['String']; datePeriod: DateRangeInput; - PERMISSION_MANAGE_PRODUCTS: Scalars['Boolean']; - PERMISSION_MANAGE_ORDERS: Scalars['Boolean']; + hasPermissionToManageProducts: 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<{ 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<{ after?: InputMaybe; @@ -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<{ channelId?: InputMaybe; diff --git a/src/home/queries.ts b/src/home/queries.ts index 489310656..de097bfa3 100644 --- a/src/home/queries.ts +++ b/src/home/queries.ts @@ -4,30 +4,30 @@ export const home = gql` query Home( $channel: String! $datePeriod: DateRangeInput! - $PERMISSION_MANAGE_PRODUCTS: Boolean! - $PERMISSION_MANAGE_ORDERS: Boolean! + $hasPermissionToManageProducts: Boolean! + $hasPermissionToManageOrders: Boolean! ) { salesToday: ordersTotal(period: TODAY, channel: $channel) - @include(if: $PERMISSION_MANAGE_ORDERS) { + @include(if: $hasPermissionToManageOrders) { gross { amount currency } } ordersToday: orders(filter: { created: $datePeriod }, channel: $channel) - @include(if: $PERMISSION_MANAGE_ORDERS) { + @include(if: $hasPermissionToManageOrders) { totalCount } ordersToFulfill: orders( filter: { status: READY_TO_FULFILL } channel: $channel - ) @include(if: $PERMISSION_MANAGE_ORDERS) { + ) @include(if: $hasPermissionToManageOrders) { totalCount } ordersToCapture: orders( filter: { status: READY_TO_CAPTURE } channel: $channel - ) @include(if: $PERMISSION_MANAGE_ORDERS) { + ) @include(if: $hasPermissionToManageOrders) { totalCount } productsOutOfStock: products( @@ -40,7 +40,7 @@ export const home = gql` period: TODAY first: 5 channel: $channel - ) @include(if: $PERMISSION_MANAGE_PRODUCTS) { + ) @include(if: $hasPermissionToManageProducts) { edges { node { id @@ -68,7 +68,7 @@ export const home = gql` } } activities: homepageEvents(last: 10) - @include(if: $PERMISSION_MANAGE_ORDERS) { + @include(if: $hasPermissionToManageOrders) { edges { node { amount diff --git a/src/home/views/index.tsx b/src/home/views/index.tsx index a6ae32418..30e815a8b 100644 --- a/src/home/views/index.tsx +++ b/src/home/views/index.tsx @@ -2,8 +2,10 @@ import { useUser } from "@dashboard/auth"; import { channelsListUrl } from "@dashboard/channels/urls"; import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext"; +import { hasPermissions } from "@dashboard/components/RequirePermissions"; import { OrderStatusFilter, + PermissionEnum, StockAvailability, useHomeQuery, } from "@dashboard/graphql"; @@ -21,10 +23,21 @@ const HomeSection = () => { const noChannel = !channel && typeof channel !== "undefined"; + const userPermissions = user?.userPermissions || []; + const { data } = useHomeQuery({ displayLoader: true, 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 ( diff --git a/src/orders/components/OrderDraftListHeader/OrderDraftListHeader.tsx b/src/orders/components/OrderDraftListHeader/OrderDraftListHeader.tsx index b748b68c0..016ed414e 100644 --- a/src/orders/components/OrderDraftListHeader/OrderDraftListHeader.tsx +++ b/src/orders/components/OrderDraftListHeader/OrderDraftListHeader.tsx @@ -1,3 +1,4 @@ +import { useUserAccessibleChannels } from "@dashboard/auth/hooks/useUserAccessibleChannels"; import { TopNav } from "@dashboard/components/AppLayout"; import { LimitsInfo } from "@dashboard/components/AppLayout/LimitsInfo"; import { FilterPresetsSelect } from "@dashboard/components/FilterPresetsSelect"; @@ -5,7 +6,7 @@ import { RefreshLimitsQuery } from "@dashboard/graphql"; import { sectionNames } from "@dashboard/intl"; import { FilterPresetsProps } from "@dashboard/types"; 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 { FormattedMessage, useIntl } from "react-intl"; @@ -33,6 +34,8 @@ export const OrderDraftListHeader = ({ onAdd, }: OrderDraftListHeaderProps) => { const intl = useIntl(); + const userAccessibleChannels = useUserAccessibleChannels(); + const hasAccessibleChannels = userAccessibleChannels.length > 0; const limitsReached = isLimitReached(limits, "orders"); return ( @@ -71,18 +74,30 @@ export const OrderDraftListHeader = ({ /> - + + + + + + {!hasAccessibleChannels && ( + + )} + + {hasLimits(limits, "orders") && ( = ({ ...listProps }) => { const intl = useIntl(); + const userAccessibleChannels = useUserAccessibleChannels(); + const hasAccessibleChannels = userAccessibleChannels.length > 0; const filterStructure = createFilterStructure(intl, filterOpts); const limitsReached = isLimitReached(limits, "orders"); const [isFilterPresetOpen, setFilterPresetOpen] = useState(false); @@ -162,32 +165,46 @@ const OrderListPage: React.FC = ({ ]} /> )} - {extensionCreateButtonItems.length > 0 ? ( - - - - ) : ( - - )} + + + + {extensionCreateButtonItems.length > 0 ? ( + + + + ) : ( + + )} + + + {!hasAccessibleChannels && ( + + )} + + + {hasLimits(limits, "orders") && ( = ({ params }) => { }, }); - const { channel, availableChannels } = useAppChannel(false); + const { channel } = useAppChannel(false); + const user = useUser(); + const channels = user?.user?.accessibleChannels ?? []; + const limitOpts = useShopLimitsQuery({ variables: { orders: true, @@ -274,7 +278,7 @@ export const OrderDraftList: React.FC = ({ params }) => { tabName={presets[presetIdToDelete - 1]?.name ?? "..."} /> = ({ params }) => { usePaginationReset(orderListUrl, params, settings.rowNumber); const intl = useIntl(); + const { channel, availableChannels } = useAppChannel(false); + const user = useUser(); + const channels = user?.user?.accessibleChannels ?? []; const [createOrder] = useOrderDraftCreateMutation({ onCompleted: data => { @@ -87,7 +91,6 @@ export const OrderList: React.FC = ({ params }) => { }, }); - const { channel, availableChannels } = useAppChannel(false); const limitOpts = useShopLimitsQuery({ variables: { orders: true, @@ -95,9 +98,7 @@ export const OrderList: React.FC = ({ params }) => { }); const noChannel = !channel && typeof channel !== "undefined"; - const channelOpts = availableChannels - ? mapNodeToChoice(availableChannels) - : null; + const channelOpts = availableChannels ? mapNodeToChoice(channels) : null; const [changeFilters, resetFilters, handleSearchChange] = useFilterHandlers({ createUrl: orderListUrl, diff --git a/src/permissionGroups/components/PermissionGroupCreatePage/PermissionGroupCreatePage.stories.tsx b/src/permissionGroups/components/PermissionGroupCreatePage/PermissionGroupCreatePage.stories.tsx index dc2d696fb..3cb3bf346 100644 --- a/src/permissionGroups/components/PermissionGroupCreatePage/PermissionGroupCreatePage.stories.tsx +++ b/src/permissionGroups/components/PermissionGroupCreatePage/PermissionGroupCreatePage.stories.tsx @@ -1,18 +1,20 @@ -// @ts-strict-ignore -import { permissions } from "@dashboard/fixtures"; +import { channels, permissions } from "@dashboard/fixtures"; import React from "react"; import { errorsOfPermissionGroupCreate } from "../../fixtures"; -import PermissionGroupCreatePage, { +import { + PermissionGroupCreatePage, PermissionGroupCreatePageProps, } from "./PermissionGroupCreatePage"; const props: PermissionGroupCreatePageProps = { disabled: false, errors: [], - onSubmit: () => undefined, + onSubmit: () => new Promise(resolve => resolve(undefined)), permissions, - saveButtonBarState: undefined, + channels, + saveButtonBarState: "default", + hasRestrictedChannels: false, }; export default { diff --git a/src/permissionGroups/components/PermissionGroupCreatePage/PermissionGroupCreatePage.tsx b/src/permissionGroups/components/PermissionGroupCreatePage/PermissionGroupCreatePage.tsx index e93a76eec..5e85180e3 100644 --- a/src/permissionGroups/components/PermissionGroupCreatePage/PermissionGroupCreatePage.tsx +++ b/src/permissionGroups/components/PermissionGroupCreatePage/PermissionGroupCreatePage.tsx @@ -1,21 +1,23 @@ -// @ts-strict-ignore import AccountPermissions from "@dashboard/components/AccountPermissions"; import { TopNav } from "@dashboard/components/AppLayout/TopNav"; 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 FormSpacer from "@dashboard/components/FormSpacer"; import { DetailPageLayout } from "@dashboard/components/Layouts"; import Savebar from "@dashboard/components/Savebar"; import { + ChannelFragment, PermissionEnum, PermissionGroupErrorFragment, } from "@dashboard/graphql"; -import { SubmitPromise } from "@dashboard/hooks/useForm"; +import { FormChange, SubmitPromise } from "@dashboard/hooks/useForm"; import useNavigator from "@dashboard/hooks/useNavigator"; -import { sectionNames } from "@dashboard/intl"; +import { buttonMessages, sectionNames } from "@dashboard/intl"; import { permissionGroupListUrl } from "@dashboard/permissionGroups/urls"; import { getFormErrors } from "@dashboard/utils/errors"; import getPermissionGroupErrorMessage from "@dashboard/utils/errors/permissionGroups"; +import { Box } from "@saleor/macaw-ui/next"; import React from "react"; import { useIntl } from "react-intl"; @@ -25,30 +27,40 @@ import PermissionGroupInfo from "../PermissionGroupInfo"; export interface PermissionGroupCreateFormData { name: string; hasFullAccess: boolean; + hasAllChannels: boolean; isActive: boolean; permissions: PermissionEnum[]; + channels: string[]; } const initialForm: PermissionGroupCreateFormData = { hasFullAccess: false, + hasAllChannels: true, isActive: false, name: "", permissions: [], + channels: [], }; export interface PermissionGroupCreatePageProps { disabled: boolean; errors: PermissionGroupErrorFragment[]; permissions: PermissionData[]; - saveButtonBarState: ConfirmButtonTransitionState; + channels: ChannelFragment[]; + hasRestrictedChannels: boolean; + saveButtonBarState: "loading" | "success" | "error" | "default"; onSubmit: (data: PermissionGroupCreateFormData) => SubmitPromise; } -const PermissionGroupCreatePage: React.FC = ({ +export const PermissionGroupCreatePage: React.FC< + PermissionGroupCreatePageProps +> = ({ disabled, permissions, + channels, onSubmit, saveButtonBarState, + hasRestrictedChannels, errors, }) => { const intl = useIntl(); @@ -63,55 +75,86 @@ const PermissionGroupCreatePage: React.FC = ({ return (
- {({ data, change, submit, isSaveDisabled }) => ( - - - - - {intl.formatMessage(sectionNames.permissionGroups)} - - { + const handleChannelChange: FormChange = event => { + change({ + target: { + name: "channels", + value: event.target.value, + }, + }); + }; + + const handleHasAllChannelsChange = () => { + change({ + target: { + name: "hasAllChannels", + value: !data.hasAllChannels, + }, + }); + }; + + return ( + + + + + {intl.formatMessage(sectionNames.permissionGroups)} + + + + + + + + + + + + + navigate(permissionGroupListUrl())} + onSubmit={submit} + state={saveButtonBarState} + disabled={!!isSaveDisabled} /> - - - - - navigate(permissionGroupListUrl())} - onSubmit={submit} - state={saveButtonBarState} - disabled={isSaveDisabled} - /> - - )} + + ); + }}
); }; -PermissionGroupCreatePage.displayName = "PermissionGroupCreatePage"; -export default PermissionGroupCreatePage; diff --git a/src/permissionGroups/components/PermissionGroupCreatePage/index.ts b/src/permissionGroups/components/PermissionGroupCreatePage/index.ts index 0a89cd1b7..d16fb769d 100644 --- a/src/permissionGroups/components/PermissionGroupCreatePage/index.ts +++ b/src/permissionGroups/components/PermissionGroupCreatePage/index.ts @@ -1,2 +1 @@ -export { default } from "./PermissionGroupCreatePage"; export * from "./PermissionGroupCreatePage"; diff --git a/src/permissionGroups/components/PermissionGroupDetailsPage/PermissionGroupDetailsPage.stories.tsx b/src/permissionGroups/components/PermissionGroupDetailsPage/PermissionGroupDetailsPage.stories.tsx index a80511b55..6d2826e94 100644 --- a/src/permissionGroups/components/PermissionGroupDetailsPage/PermissionGroupDetailsPage.stories.tsx +++ b/src/permissionGroups/components/PermissionGroupDetailsPage/PermissionGroupDetailsPage.stories.tsx @@ -1,30 +1,42 @@ -// @ts-strict-ignore -import { permissions } from "@dashboard/fixtures"; +import { channels, permissions } from "@dashboard/fixtures"; +import { MembersListUrlSortField } from "@dashboard/permissionGroups/urls"; import React from "react"; -import { emptyPermissionGroup, permissionGroup, users } from "../../fixtures"; -import PermissionGroupDetailsPage, { - PermissionGroupDetailsPageProps, +import { + emptyPermissionGroup, + permissionGroup, + permissionGroupWithChannels, + users, +} from "../../fixtures"; +import { + PermissionGroupDetailsPage, + PermissonGroupDetailsPageProps, } from "./PermissionGroupDetailsPage"; +export * from "./PermissionGroupDetailsPage"; -const props: PermissionGroupDetailsPageProps = { +const props: PermissonGroupDetailsPageProps = { disabled: false, + isUserAbleToEditChannels: true, errors: [], isChecked: () => false, members: users, onAssign: () => undefined, onSort: () => undefined, - onSubmit: () => undefined, + onSubmit: () => new Promise(resolve => resolve(undefined)), onUnassign: () => undefined, permissionGroup, permissions, permissionsExceeded: false, - saveButtonBarState: undefined, + saveButtonBarState: "default", selected: 0, - sort: null, + sort: { + asc: true, + sort: MembersListUrlSortField.name, + }, toggle: () => undefined, toggleAll: () => undefined, toolbar: null, + channels, }; export default { @@ -45,7 +57,23 @@ export const Loading = () => ( +); + +export const WithRestrictedChannels = () => ( + +); + +export const WithRestrictedChannelsAndWithoutAccessToEdit = () => ( + ); diff --git a/src/permissionGroups/components/PermissionGroupDetailsPage/PermissionGroupDetailsPage.tsx b/src/permissionGroups/components/PermissionGroupDetailsPage/PermissionGroupDetailsPage.tsx index 85a6eab07..e24ecd690 100644 --- a/src/permissionGroups/components/PermissionGroupDetailsPage/PermissionGroupDetailsPage.tsx +++ b/src/permissionGroups/components/PermissionGroupDetailsPage/PermissionGroupDetailsPage.tsx @@ -1,42 +1,50 @@ -// @ts-strict-ignore +import { useUser } from "@dashboard/auth"; import AccountPermissions from "@dashboard/components/AccountPermissions"; 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 FormSpacer from "@dashboard/components/FormSpacer"; import { DetailPageLayout } from "@dashboard/components/Layouts"; import Savebar from "@dashboard/components/Savebar"; import { + ChannelFragment, PermissionEnum, PermissionGroupDetailsFragment, PermissionGroupErrorFragment, UserPermissionFragment, } from "@dashboard/graphql"; -import { SubmitPromise } from "@dashboard/hooks/useForm"; +import { FormChange, SubmitPromise } from "@dashboard/hooks/useForm"; import useNavigator from "@dashboard/hooks/useNavigator"; +import { buttonMessages } from "@dashboard/intl"; import { MembersListUrlSortField, permissionGroupListUrl, } from "@dashboard/permissionGroups/urls"; -import { - extractPermissionCodes, - isGroupFullAccess, -} from "@dashboard/permissionGroups/utils"; import { ListActions, SortPage } from "@dashboard/types"; import { getFormErrors } from "@dashboard/utils/errors"; import getPermissionGroupErrorMessage from "@dashboard/utils/errors/permissionGroups"; +import { Box } from "@saleor/macaw-ui/next"; import React from "react"; import { useIntl } from "react-intl"; +import { + checkIfUserHasRestictedAccessToChannels, + extractPermissionCodes, + getInitialChannels, + getUserAccessibleChannelsOptions, + isGroupFullAccess, +} from "../../utils"; import PermissionGroupInfo from "../PermissionGroupInfo"; import PermissionGroupMemberList from "../PermissionGroupMemberList"; export interface PermissionGroupDetailsPageFormData { name: string; hasFullAccess: boolean; + hasAllChannels: boolean; isActive: boolean; permissions: PermissionEnum[]; users: PermissionGroupDetailsFragment["users"]; + channels: string[]; } export interface PermissionData @@ -45,22 +53,26 @@ export interface PermissionData disabled?: boolean; } -export interface PermissionGroupDetailsPageProps +export interface PermissonGroupDetailsPageProps extends ListActions, SortPage { + channels: ChannelFragment[]; disabled: boolean; + isUserAbleToEditChannels: boolean; errors: PermissionGroupErrorFragment[]; members: PermissionGroupDetailsFragment["users"]; - permissionGroup: PermissionGroupDetailsFragment; + permissionGroup: PermissionGroupDetailsFragment | null | undefined; permissions: PermissionData[]; permissionsExceeded: boolean; - saveButtonBarState: ConfirmButtonTransitionState; + saveButtonBarState: "loading" | "success" | "error" | "default"; onAssign: () => void; onUnassign: (ids: string[]) => void; onSubmit: (data: PermissionGroupDetailsPageFormData) => SubmitPromise; } -const PermissionGroupDetailsPage: React.FC = ({ +export const PermissionGroupDetailsPage: React.FC< + PermissonGroupDetailsPageProps +> = ({ disabled, errors, members, @@ -69,13 +81,23 @@ const PermissionGroupDetailsPage: React.FC = ({ permissions, permissionsExceeded, saveButtonBarState, + channels, + isUserAbleToEditChannels, ...listProps }) => { const intl = useIntl(); const navigate = useNavigator(); + const user = useUser(); + + const channelsOptions = getUserAccessibleChannelsOptions(channels, user.user); + const hasUserRestrictedChannels = checkIfUserHasRestictedAccessToChannels( + user.user, + ); const initialForm: PermissionGroupDetailsPageFormData = { hasFullAccess: isGroupFullAccess(permissionGroup, permissions), + hasAllChannels: !permissionGroup?.restrictedAccessToChannels ?? false, + channels: getInitialChannels(permissionGroup, channels?.length ?? 0), isActive: false, name: permissionGroup?.name || "", permissions: extractPermissionCodes(permissionGroup), @@ -90,59 +112,92 @@ const PermissionGroupDetailsPage: React.FC = ({ return (
- {({ 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 ( + + - - - - - - -
- navigate(permissionGroupListUrl())} - onSubmit={submit} - state={saveButtonBarState} - disabled={disabled} - /> -
-
- )} + + + + + + + + + + + + + + + +
+ navigate(permissionGroupListUrl())} + onSubmit={submit} + state={saveButtonBarState} + disabled={disabled} + /> +
+ + ); + }}
); }; -PermissionGroupDetailsPage.displayName = "PermissionGroupDetailsPage"; -export default PermissionGroupDetailsPage; diff --git a/src/permissionGroups/components/PermissionGroupDetailsPage/index.ts b/src/permissionGroups/components/PermissionGroupDetailsPage/index.ts index 122f3ffc1..336c21b6d 100644 --- a/src/permissionGroups/components/PermissionGroupDetailsPage/index.ts +++ b/src/permissionGroups/components/PermissionGroupDetailsPage/index.ts @@ -1,2 +1 @@ -export { default } from "./PermissionGroupDetailsPage"; export * from "./PermissionGroupDetailsPage"; diff --git a/src/permissionGroups/fixtures.ts b/src/permissionGroups/fixtures.ts index 0aee4d82d..8a0eb452a 100644 --- a/src/permissionGroups/fixtures.ts +++ b/src/permissionGroups/fixtures.ts @@ -1,5 +1,5 @@ -// @ts-strict-ignore import avatarImg from "@assets/images/avatars/avatar.png"; +import { channels } from "@dashboard/fixtures"; import { PermissionEnum, PermissionGroupDetailsFragment, @@ -113,6 +113,7 @@ export const emptyPermissionGroup: PermissionGroupDetailsFragment = { userCanManage: true, users: [], __typename: "Group", + permissions: [ { code: PermissionEnum.MANAGE_PAGES, @@ -120,6 +121,8 @@ export const emptyPermissionGroup: PermissionGroupDetailsFragment = { __typename: "Permission", }, ], + accessibleChannels: [], + restrictedAccessToChannels: false, }; export const errorsOfPermissionGroupCreate: PermissionGroupErrorFragment[] = [ @@ -161,6 +164,8 @@ export const permissionGroup: PermissionGroupDetailsFragment = { avatar: null, }, ], + accessibleChannels: [], + restrictedAccessToChannels: false, __typename: "Group", permissions: [ { @@ -171,7 +176,46 @@ export const permissionGroup: PermissionGroupDetailsFragment = { ], }; -export const users: RelayToFlat = [ +export const permissionGroupWithChannels: NonNullable = + { + 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 +> = [ { node: { id: "VXNlcjoyMQ==", diff --git a/src/permissionGroups/index.tsx b/src/permissionGroups/index.tsx index 56dfcd0cd..a70460e9e 100644 --- a/src/permissionGroups/index.tsx +++ b/src/permissionGroups/index.tsx @@ -15,8 +15,8 @@ import { PermissionGroupListUrlQueryParams, PermissionGroupListUrlSortField, } from "./urls"; -import PermissionGroupCreate from "./views/PermissionGroupCreate"; -import PermissionGroupDetailsComponent from "./views/PermissionGroupDetails"; +import { PermissionGroupCreate } from "./views/PermissionGroupCreate"; +import { PermissionGroupDetails as PermissionGroupDetailsComponent } from "./views/PermissionGroupDetails"; import PermissionGroupListComponent from "./views/PermissionGroupList"; const permissionGroupList: React.FC> = ({ diff --git a/src/permissionGroups/sort.ts b/src/permissionGroups/sort.ts index dcc9f35a4..dfe6a5820 100644 --- a/src/permissionGroups/sort.ts +++ b/src/permissionGroups/sort.ts @@ -1,4 +1,3 @@ -// @ts-strict-ignore import { StaffMemberFragment } from "@dashboard/graphql"; import { getUserName } from "@dashboard/misc"; @@ -7,12 +6,13 @@ import { MembersListUrlSortField } from "./urls"; export const sortMembers = (sort: string, asc: boolean) => (a: StaffMemberFragment, b: StaffMemberFragment) => { - let valueA; - let valueB; + let valueA: string = ""; + let valueB: string = ""; + switch (sort) { case MembersListUrlSortField.name: - valueA = getUserName(a); - valueB = getUserName(b); + valueA = getUserName(a) ?? ""; + valueB = getUserName(b) ?? ""; break; case MembersListUrlSortField.email: valueA = a.email; @@ -21,6 +21,6 @@ export const sortMembers = } return asc - ? ("" + valueA).localeCompare(valueB) - : ("" + valueA).localeCompare(valueB) * -1; + ? valueA.localeCompare(valueB) + : valueA.localeCompare(valueB) * -1; }; diff --git a/src/permissionGroups/utils.test.ts b/src/permissionGroups/utils.test.ts new file mode 100644 index 000000000..abdd03deb --- /dev/null +++ b/src/permissionGroups/utils.test.ts @@ -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>; + + // 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>; + + // 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>; + + // 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, + ]); + }); + }); +}); diff --git a/src/permissionGroups/utils.ts b/src/permissionGroups/utils.ts index cfa12ee0c..b2a94e7c7 100644 --- a/src/permissionGroups/utils.ts +++ b/src/permissionGroups/utils.ts @@ -1,5 +1,7 @@ -// @ts-strict-ignore +import { UserContext } from "@dashboard/auth/types"; +import { MultiAutocompleteChoiceType } from "@dashboard/components/MultiAutocompleteSelectField"; import { + ChannelFragment, PermissionFragment, PermissionGroupDetailsFragment, UserFragment, @@ -7,14 +9,16 @@ import { import difference from "lodash/difference"; import { PermissionGroupDetailsPageFormData } from "./components/PermissionGroupDetailsPage"; - /** * Will return true if group has all permissions available in shop assigned. */ export const isGroupFullAccess = ( - permissionGroup: PermissionGroupDetailsFragment, + permissionGroup: PermissionGroupDetailsFragment | null | undefined, shopPermissions: Array>, ) => { + if (!permissionGroup) { + return false; + } const assignedCodes = extractPermissionCodes(permissionGroup); if (assignedCodes.length !== shopPermissions?.length) { @@ -33,19 +37,31 @@ export const isGroupFullAccess = ( * Return list of codes which are assigned to the permission group. */ export const extractPermissionCodes = ( - permissionGroup: PermissionGroupDetailsFragment, -) => - permissionGroup?.permissions + permissionGroup: PermissionGroupDetailsFragment | null | undefined, +) => { + if (!permissionGroup) { + return []; + } + + return permissionGroup?.permissions ? permissionGroup.permissions.map(perm => perm.code) : []; +}; /** * Return lists of permissions which have to be added and removed from group. */ export const permissionsDiff = ( - permissionGroup: PermissionGroupDetailsFragment, + permissionGroup: PermissionGroupDetailsFragment | null | undefined, formData: PermissionGroupDetailsPageFormData, ) => { + if (!permissionGroup) { + return { + addPermissions: [], + removePermissions: [], + }; + } + const newPermissions = formData.permissions; const oldPermissions = extractPermissionCodes(permissionGroup); @@ -59,11 +75,18 @@ export const permissionsDiff = ( * Return lists of users which have to be added and removed from group. */ export const usersDiff = ( - permissionGroup: PermissionGroupDetailsFragment, + permissionGroup: PermissionGroupDetailsFragment | null | undefined, formData: PermissionGroupDetailsPageFormData, ) => { - const newUsers = formData.users.map(u => u.id); - const oldUsers = permissionGroup?.users.map(u => u.id); + if (!permissionGroup) { + return { + addUsers: [], + removeUsers: [], + }; + } + + const newUsers = formData?.users?.map(u => u.id) ?? []; + const oldUsers = permissionGroup?.users?.map(u => u.id) ?? []; return { 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 */ export const arePermissionsExceeded = ( - permissionGroup: PermissionGroupDetailsFragment, - user: UserFragment, + permissionGroup: PermissionGroupDetailsFragment | null | undefined, + user: UserFragment | null | undefined, ) => { + if (!permissionGroup || !user) { + return false; + } + 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 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) ?? []; +}; diff --git a/src/permissionGroups/views/PermissionGroupCreate/PermissionGroupCreate.tsx b/src/permissionGroups/views/PermissionGroupCreate/PermissionGroupCreate.tsx index ee3a918ac..2efd5f5cd 100644 --- a/src/permissionGroups/views/PermissionGroupCreate/PermissionGroupCreate.tsx +++ b/src/permissionGroups/views/PermissionGroupCreate/PermissionGroupCreate.tsx @@ -1,5 +1,5 @@ -// @ts-strict-ignore import { useUser } from "@dashboard/auth"; +import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext"; import { WindowTitle } from "@dashboard/components/WindowTitle"; import { usePermissionGroupCreateMutation } from "@dashboard/graphql"; import useNavigator from "@dashboard/hooks/useNavigator"; @@ -7,20 +7,33 @@ import useNotifier from "@dashboard/hooks/useNotifier"; import useShop from "@dashboard/hooks/useShop"; import { extractMutationErrors } from "@dashboard/misc"; import { PermissionData } from "@dashboard/permissionGroups/components/PermissionGroupDetailsPage"; -import React from "react"; +import React, { useMemo } from "react"; import { useIntl } from "react-intl"; -import PermissionGroupCreatePage, { +import { PermissionGroupCreateFormData, + PermissionGroupCreatePage, } from "../../components/PermissionGroupCreatePage"; import { permissionGroupDetailsUrl } from "../../urls"; +import { + checkIfUserHasRestictedAccessToChannels, + getUserAccessibleChannelsOptions, +} from "../../utils"; -const PermissionGroupCreateView: React.FC = () => { +export const PermissionGroupCreate: React.FC = () => { const navigate = useNavigator(); const notify = useNotifier(); const intl = useIntl(); const shop = useShop(); 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] = usePermissionGroupCreateMutation({ @@ -33,9 +46,12 @@ const PermissionGroupCreateView: React.FC = () => { defaultMessage: "Permission group created", }), }); - navigate( - permissionGroupDetailsUrl(data.permissionGroupCreate.group.id), - ); + + if (data?.permissionGroupCreate?.group?.id) { + navigate( + permissionGroupDetailsUrl(data.permissionGroupCreate.group.id), + ); + } } }, }); @@ -43,22 +59,30 @@ const PermissionGroupCreateView: React.FC = () => { const errors = createPermissionGroupResult?.data?.permissionGroupCreate?.errors || []; - const onSubmit = (formData: PermissionGroupCreateFormData) => - extractMutationErrors( + const onSubmit = (formData: PermissionGroupCreateFormData) => { + const channelChoices = userAccessibleChannelsOptions.map( + channel => channel.id, + ); + + return extractMutationErrors( createPermissionGroup({ variables: { input: { - addPermissions: formData.hasFullAccess - ? shop.permissions.map(perm => perm.code) - : formData.permissions, + addPermissions: formData.permissions, addUsers: [], 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[] = shop?.permissions.map( @@ -80,15 +104,14 @@ const PermissionGroupCreateView: React.FC = () => { })} /> ); }; -PermissionGroupCreateView.displayName = "PermissionGroupCreateView"; - -export default PermissionGroupCreateView; diff --git a/src/permissionGroups/views/PermissionGroupCreate/index.ts b/src/permissionGroups/views/PermissionGroupCreate/index.ts index 56c9e6f4f..4a5c35981 100644 --- a/src/permissionGroups/views/PermissionGroupCreate/index.ts +++ b/src/permissionGroups/views/PermissionGroupCreate/index.ts @@ -1,2 +1 @@ -export { default } from "./PermissionGroupCreate"; export * from "./PermissionGroupCreate"; diff --git a/src/permissionGroups/views/PermissionGroupDetails/PermissionGroupDetails.tsx b/src/permissionGroups/views/PermissionGroupDetails/PermissionGroupDetails.tsx index eaf0482bb..27c3ccead 100644 --- a/src/permissionGroups/views/PermissionGroupDetails/PermissionGroupDetails.tsx +++ b/src/permissionGroups/views/PermissionGroupDetails/PermissionGroupDetails.tsx @@ -1,8 +1,9 @@ -// @ts-strict-ignore import { useUser } from "@dashboard/auth"; +import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext"; import { Button } from "@dashboard/components/Button"; import { DEFAULT_INITIAL_SEARCH_DATA } from "@dashboard/config"; import { + PermissionGroupDetailsQuery, usePermissionGroupDetailsQuery, usePermissionGroupUpdateMutation, } from "@dashboard/graphql"; @@ -14,11 +15,6 @@ import useStateFromProps from "@dashboard/hooks/useStateFromProps"; import { commonMessages } from "@dashboard/intl"; import { extractMutationErrors } from "@dashboard/misc"; import MembersErrorDialog from "@dashboard/permissionGroups/components/MembersErrorDialog"; -import { - arePermissionsExceeded, - permissionsDiff, - usersDiff, -} from "@dashboard/permissionGroups/utils"; import useStaffMemberSearch from "@dashboard/searches/useStaffMemberSearch"; import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers"; import createSortHandler from "@dashboard/utils/handlers/sortHandler"; @@ -28,7 +24,8 @@ import React from "react"; import { useIntl } from "react-intl"; import AssignMembersDialog from "../../components/AssignMembersDialog"; -import PermissionGroupDetailsPage, { +import { + PermissionGroupDetailsPage, PermissionGroupDetailsPageFormData, } from "../../components/PermissionGroupDetailsPage"; import UnassignMembersDialog from "../../components/UnassignMembersDialog"; @@ -37,12 +34,23 @@ import { PermissionGroupDetailsUrlDialog, PermissionGroupDetailsUrlQueryParams, } from "../../urls"; +import { + arePermissionsExceeded, + channelsDiff, + checkIfUserBelongToPermissionGroup, + permissionsDiff, + usersDiff, +} from "../../utils"; interface PermissionGroupDetailsProps { id: string; params: PermissionGroupDetailsUrlQueryParams; } +type Members = NonNullable< + NonNullable["users"] +>; + export const PermissionGroupDetails: React.FC = ({ id, params, @@ -55,11 +63,13 @@ export const PermissionGroupDetails: React.FC = ({ const { data, loading, refetch } = usePermissionGroupDetailsQuery({ displayLoader: true, - variables: { id, userId: user?.user.id }, + variables: { id, userId: user?.user?.id ?? "" }, }); - const [membersList, setMembersList] = useStateFromProps( - data?.permissionGroup.users, + const { availableChannels } = useAppChannel(false); + + const [membersList, setMembersList] = useStateFromProps( + data?.permissionGroup?.users ?? [], ); const { @@ -76,16 +86,30 @@ export const PermissionGroupDetails: React.FC = ({ const [permissionGroupUpdate, permissionGroupUpdateResult] = usePermissionGroupUpdateMutation({ - onCompleted: data => { - if (data.permissionGroupUpdate.errors.length === 0) { + onCompleted: updatedData => { + if (updatedData?.permissionGroupUpdate?.errors?.length === 0) { notify({ status: "success", 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(); closeModal(); } else if ( - data.permissionGroupUpdate.errors.some(e => e.field === "removeUsers") + updatedData?.permissionGroupUpdate?.errors.some( + e => e.field === "removeUsers", + ) ) { openModal("unassignError"); } @@ -104,22 +128,25 @@ export const PermissionGroupDetails: React.FC = ({ ); const unassignMembers = () => { - setMembersList(membersList?.filter(m => !listElements.includes(m.id))); + setMembersList( + membersList?.filter(m => !listElements.includes(m.id)) ?? [], + ); closeModal(); }; 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( perm => - perm.sourcePermissionGroups.length === 1 && - perm.sourcePermissionGroups[0].id === id, + perm.sourcePermissionGroups?.length === 1 && + perm.sourcePermissionGroups?.[0].id === id, ) .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 => ({ ...perm, @@ -129,9 +156,11 @@ export const PermissionGroupDetails: React.FC = ({ const permissionsExceeded = arePermissionsExceeded( 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) => extractMutationErrors( @@ -142,6 +171,13 @@ export const PermissionGroupDetails: React.FC = ({ name: formData.name, ...permissionsDiff(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 = ({ openModal("assign")} onUnassign={ids => openModal("unassign", { ids })} errors={ - permissionGroupUpdateResult?.data?.permissionGroupUpdate.errors || [] + permissionGroupUpdateResult?.data?.permissionGroupUpdate?.errors ?? [] } onSubmit={handleSubmit} permissions={permissions} @@ -183,20 +221,17 @@ export const PermissionGroupDetails: React.FC = ({ /> { - setMembersList([ - ...membersList, - ...formData.filter(member => !membersList.includes(member)), - ]); + setMembersList([...(membersList ?? []), ...formData] as Members); closeModal(); }} /> @@ -216,5 +251,3 @@ export const PermissionGroupDetails: React.FC = ({ ); }; - -export default PermissionGroupDetails; diff --git a/src/permissionGroups/views/PermissionGroupDetails/index.ts b/src/permissionGroups/views/PermissionGroupDetails/index.ts index af25c63e2..63670e5f2 100644 --- a/src/permissionGroups/views/PermissionGroupDetails/index.ts +++ b/src/permissionGroups/views/PermissionGroupDetails/index.ts @@ -1,2 +1 @@ -export { default } from "./PermissionGroupDetails"; export * from "./PermissionGroupDetails"; diff --git a/src/permissionGroups/views/PermissionGroupList/PermissionGroupList.tsx b/src/permissionGroups/views/PermissionGroupList/PermissionGroupList.tsx index a218400ef..a37890035 100644 --- a/src/permissionGroups/views/PermissionGroupList/PermissionGroupList.tsx +++ b/src/permissionGroups/views/PermissionGroupList/PermissionGroupList.tsx @@ -1,4 +1,3 @@ -// @ts-strict-ignore import { PermissionGroupErrorFragment, usePermissionGroupDeleteMutation, @@ -60,7 +59,7 @@ export const PermissionGroupList: React.FC = ({ }); const paginationValues = usePaginator({ - pageInfo: data?.permissionGroups.pageInfo, + pageInfo: data?.permissionGroups?.pageInfo, paginationState, queryString: params, }); @@ -76,13 +75,13 @@ export const PermissionGroupList: React.FC = ({ PermissionGroupListUrlQueryParams >(navigate, permissionGroupListUrl, params); - const permissionGroups = mapEdgesToItems(data?.permissionGroups); + const permissionGroups = mapEdgesToItems(data?.permissionGroups) ?? []; const [deleteError, setDeleteError] = React.useState(); const [permissionGroupDelete] = usePermissionGroupDeleteMutation({ onCompleted: data => { - if (data.permissionGroupDelete.errors.length === 0) { + if (data?.permissionGroupDelete?.errors?.length === 0) { notify({ status: "success", text: intl.formatMessage({ @@ -94,7 +93,7 @@ export const PermissionGroupList: React.FC = ({ setDeleteError(undefined); closeModal(); } else { - setDeleteError(data.permissionGroupDelete.errors[0]); + setDeleteError(data?.permissionGroupDelete?.errors?.[0]); } }, }); @@ -114,7 +113,7 @@ export const PermissionGroupList: React.FC = ({ onConfirm={() => permissionGroupDelete({ variables: { - id: params.id, + id: params?.id ?? "", }, }) } diff --git a/src/permissionGroups/views/PermissionGroupList/sort.ts b/src/permissionGroups/views/PermissionGroupList/sort.ts index db801732f..581353ad4 100644 --- a/src/permissionGroups/views/PermissionGroupList/sort.ts +++ b/src/permissionGroups/views/PermissionGroupList/sort.ts @@ -1,4 +1,3 @@ -// @ts-strict-ignore import { PermissionGroupSortField } from "@dashboard/graphql"; import { PermissionGroupListUrlSortField } from "@dashboard/permissionGroups/urls"; import { createGetSortQueryVariables } from "@dashboard/utils/sort"; @@ -10,7 +9,7 @@ export function getSortQueryField( case PermissionGroupListUrlSortField.name: return PermissionGroupSortField.NAME; default: - return undefined; + return "" as PermissionGroupSortField; } } diff --git a/src/types.ts b/src/types.ts index f9e48ac38..3421f77b6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -81,7 +81,7 @@ export interface SortPage { export interface ListActionsWithoutToolbar { toggle: (id: string) => void; - toggleAll: (items: React.ReactNodeArray, selected: number) => void; + toggleAll: (items: Node[], selected: number) => void; isChecked: (id: string) => boolean; selected: number; } diff --git a/src/utils/errors/permissionGroups.ts b/src/utils/errors/permissionGroups.ts index 59d248dc4..e8c8050d2 100644 --- a/src/utils/errors/permissionGroups.ts +++ b/src/utils/errors/permissionGroups.ts @@ -30,7 +30,7 @@ const messages = defineMessages({ }); function getPermissionGroupErrorMessage( - err: PermissionGroupErrorFragment, + err: PermissionGroupErrorFragment | undefined, intl: IntlShape, ): string | undefined { if (err) {