diff --git a/src/authz-module/audit-user/index.test.tsx b/src/authz-module/audit-user/index.test.tsx index 63fe18f6..5a26a6b8 100644 --- a/src/authz-module/audit-user/index.test.tsx +++ b/src/authz-module/audit-user/index.test.tsx @@ -1,9 +1,11 @@ import { render, screen, waitFor } from '@testing-library/react'; +import { AppContext } from '@edx/frontend-platform/react'; import userEvent from '@testing-library/user-event'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ToastManagerProvider } from '@src/components/ToastManager/ToastManagerContext'; import AuditUserPage from './index'; jest.mock('@edx/frontend-platform/auth', () => ({ @@ -11,6 +13,10 @@ jest.mock('@edx/frontend-platform/auth', () => ({ configure: jest.fn(), })); +jest.mock('@edx/frontend-platform/logging', () => ({ + logError: jest.fn(), +})); + const mockUser = { username: 'johndoe', email: 'john@example.com', @@ -40,17 +46,32 @@ const renderWithRouter = (route = '/audit/johndoe') => { }, }); + const mockAppContext = { + authenticatedUser: { + username: 'testuser', + email: 'testuser@example.com', + }, + config: { + // @ts-ignore + ...process.env, + }, + }; + return render( - - - - - } /> - Home Page} /> - - - - , + + + + + + + } /> + Home Page} /> + + + + + + , ); }; @@ -59,6 +80,11 @@ describe('AuditUserPage', () => { jest.clearAllMocks(); }); + beforeAll(() => { + // @ts-ignore + global.logError = jest.fn(); + }); + it('renders user info and table when data is loaded', async () => { (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ get: jest @@ -185,4 +211,177 @@ describe('AuditUserPage', () => { expect(screen.getByText(mockUser.username, { selector: 'li[aria-current="page"]' })).toBeInTheDocument(); }); }); + + it('opens and closes the ConfirmDeletionModal when delete is clicked and cancel is pressed', async () => { + (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ + get: jest + .fn() + .mockResolvedValueOnce({ data: mockUser }) + .mockResolvedValueOnce({ data: mockAssignments }), + }); + + renderWithRouter(); + + await waitFor(() => { + expect(screen.getByRole('button', { name: /delete role action/i })).toBeInTheDocument(); + }); + + const user = userEvent.setup(); + const deleteButton = screen.getByRole('button', { name: /delete role action/i }); + await user.click(deleteButton); + + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(screen.getByText(/remove role\?/i)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument(); + }); + + const cancelButton = screen.getByRole('button', { name: /cancel/i }); + await user.click(cancelButton); + + await waitFor(() => { + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + }); + + it('calls onSave when confirming deletion in ConfirmDeletionModal', async () => { + (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ + get: jest + .fn() + .mockResolvedValueOnce({ data: mockUser }) + .mockResolvedValueOnce({ data: mockAssignments }), + delete: jest.fn().mockResolvedValue({ data: { errors: [] } }), + }); + + renderWithRouter(); + + await waitFor(() => { + expect(screen.getByRole('button', { name: /delete role action/i })).toBeInTheDocument(); + }); + + const user = userEvent.setup(); + const deleteButton = screen.getByRole('button', { name: /delete role action/i }); + await user.click(deleteButton); + + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /remove/i })).toBeInTheDocument(); + }); + + const removeButton = screen.getByRole('button', { name: /remove/i }); + await user.click(removeButton); + + await waitFor(() => { + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + expect(screen.getByText(/role has been successfully removed/i)).toBeInTheDocument(); + }); + }); + + it('shows error toast when role revocation succeeds but returns errors', async () => { + (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ + get: jest + .fn() + .mockResolvedValueOnce({ data: mockUser }) + .mockResolvedValueOnce({ data: mockAssignments }), + delete: jest.fn().mockResolvedValue({ + data: { + errors: ['Failed to revoke user role'], + completed: [], + }, + }), + }); + + renderWithRouter(); + + await waitFor(() => { + expect(screen.getByRole('button', { name: /delete role action/i })).toBeInTheDocument(); + }); + + const user = userEvent.setup(); + const deleteButton = screen.getByRole('button', { name: /delete role action/i }); + await user.click(deleteButton); + + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /remove/i })).toBeInTheDocument(); + }); + + const removeButton = screen.getByRole('button', { name: /remove/i }); + await user.click(removeButton); + + await waitFor(() => { + expect(screen.getByText(/something went wrong/i)).toBeInTheDocument(); + }); + }); + + it('shows error toast with retry when role revocation fails', async () => { + (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ + get: jest + .fn() + .mockResolvedValueOnce({ data: mockUser }) + .mockResolvedValueOnce({ data: mockAssignments }), + delete: jest.fn().mockRejectedValue(new Error('Network error')), + }); + + renderWithRouter(); + + await waitFor(() => { + expect(screen.getByRole('button', { name: /delete role action/i })).toBeInTheDocument(); + }); + + const user = userEvent.setup(); + const deleteButton = screen.getByRole('button', { name: /delete role action/i }); + await user.click(deleteButton); + + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /remove/i })).toBeInTheDocument(); + }); + + const removeButton = screen.getByRole('button', { name: /remove/i }); + await user.click(removeButton); + + await waitFor(() => { + expect(screen.getByText(/something went wrong on our end/i)).toBeInTheDocument(); + expect(screen.getByText(/try again later/i)).toBeInTheDocument(); + }); + }); + + it('shows the extra warning when rolesCount is 1', async () => { + (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ + get: jest + .fn() + .mockResolvedValueOnce({ data: mockUser }) + .mockResolvedValueOnce({ + data: { + count: 1, + results: [ + { + id: '1', + role: 'library_admin', + org: 'Test Org', + scope: 'lib:test', + permissionCount: 5, + }, + ], + next: null, + previous: null, + }, + }), + }); + + renderWithRouter(); + + await waitFor(() => { + expect(screen.getByRole('button', { name: /delete role action/i })).toBeInTheDocument(); + }); + + const user = userEvent.setup(); + const deleteButton = screen.getByRole('button', { name: /delete role action/i }); + await user.click(deleteButton); + + await waitFor(() => { + expect(screen.getByText(/this is the user's only role/i)).toBeInTheDocument(); + }); + }); }); diff --git a/src/authz-module/audit-user/index.tsx b/src/authz-module/audit-user/index.tsx index 042d5b3b..465eae7d 100644 --- a/src/authz-module/audit-user/index.tsx +++ b/src/authz-module/audit-user/index.tsx @@ -1,5 +1,10 @@ -import { useEffect, useMemo } from 'react'; +import { + useCallback, + useContext, useEffect, useMemo, useState, +} from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; +import { AppContext } from '@edx/frontend-platform/react'; +import type { AppContextType } from '@edx/frontend-platform/react'; import debounce from 'lodash.debounce'; import { Container, DataTable, @@ -12,21 +17,32 @@ import { useUserAccount } from '@src/data/hooks'; import baseMessages from '@src/authz-module/messages'; import AddRoleButton from '@src/authz-module/components/AddRoleButton'; import { - OrgCell, RoleCell, ScopeCell, PermissionsCell, ViewAllPermissionsCell, ActionsCell, + OrgCell, RoleCell, ScopeCell, PermissionsCell, ViewAllPermissionsCell, + createActionsCell, } from '@src/authz-module/components/TableCells'; import { useQuerySettings } from '@src/authz-module/hooks/useQuerySettings'; -import { useUserAssignedRoles } from '@src/authz-module/data/hooks'; +import { useRevokeUserRoles, useUserAssignedRoles } from '@src/authz-module/data/hooks'; +import { RoleToDelete } from 'types'; +import { useToastManager } from '@src/components/ToastManager/ToastManagerContext'; import messages from './messages'; +import ConfirmDeletionModal from '../components/ConfirmDeletionModal'; const AuditUserPage = () => { const { formatMessage } = useIntl(); const { username } = useParams(); + const { authenticatedUser } = useContext(AppContext as React.Context); const navigate = useNavigate(); const { isLoading: isLoadingUser, data: user, isError: isErrorUser, error: errorUser, } = useUserAccount(username); const { querySettings, handleTableFetch } = useQuerySettings(); const { isLoading: isLoadingUserAssignments, data: { results: userAssignments, count } = { results: [], count: 0 } } = useUserAssignedRoles(username ?? '', querySettings); + const [roleToDelete, setRoleToDelete] = useState(null); + const [showConfirmDeletionModal, setShowConfirmDeletionModal] = useState(false); + const { + showToast, showErrorToast, Bold, Br, + } = useToastManager(); + const { mutate: revokeUserRoles, isPending: isRevokingUserRolePending } = useRevokeUserRoles(); const fetchData = useMemo(() => debounce(handleTableFetch, 500), [handleTableFetch]); @@ -41,12 +57,20 @@ const AuditUserPage = () => { useEffect(() => () => fetchData.cancel(), [fetchData]); + const handleShowConfirmDeletionModal = useCallback((role: RoleToDelete) => { + if (isRevokingUserRolePending) { return; } + + setRoleToDelete(role); + setShowConfirmDeletionModal(true); + }, [isRevokingUserRolePending]); + const navLinks = useMemo(() => [ { label: formatMessage(baseMessages['authz.management.home.nav.link']), to: AUTHZ_HOME_PATH, }, ], [formatMessage]); + const additionalColumns = useMemo(() => [ { id: 'view_permissions', @@ -56,9 +80,13 @@ const AuditUserPage = () => { { id: 'action', Header: formatMessage(messages['authz.user.table.action.column.header']), - Cell: ActionsCell, + Cell: createActionsCell({ + onClickDeleteButton: handleShowConfirmDeletionModal, + isUserAuthenticatedPage: username === authenticatedUser.username, + }), }, - ], [formatMessage]); + ], [authenticatedUser.username, formatMessage, handleShowConfirmDeletionModal, username]); + const columns = useMemo(() => [ { Header: formatMessage(messages['authz.user.table.role.column.header']), @@ -83,10 +111,83 @@ const AuditUserPage = () => { disableSortBy: true, }, ], [formatMessage]); + const pageCount = Math.ceil(count / TABLE_DEFAULT_PAGE_SIZE); + const handleCloseConfirmDeletionModal = () => { + setRoleToDelete(null); + setShowConfirmDeletionModal(false); + }; + + const handleRevokeUserRole = () => { + if (!user || !roleToDelete) { return; } + + const data = { + users: user.username, + role: roleToDelete.role, + scope: roleToDelete.scope, + }; + + const runRevokeRole = (variables) => { + const variablesData = { + data: { + ...variables.data, + querySettings, + }, + + }; + revokeUserRoles(variablesData, { + onSuccess: (response) => { + const { errors } = response; + + if (errors.length) { + showToast({ + type: 'error', + message: formatMessage( + baseMessages['authz.team.toast.default.error.message'], + { Bold, Br }, + ), + }); + return; + } + + const remainingRolesCount = count ? count - 1 : 0; + showToast({ + message: formatMessage( + baseMessages['authz.team.remove.user.toast.success.description'], + { + role: roleToDelete.name ?? roleToDelete.role, + rolesCount: remainingRolesCount, + }, + ), + type: 'success', + }); + handleCloseConfirmDeletionModal(); + }, + onError: (error, retryVariables) => { + showErrorToast(error, () => runRevokeRole(retryVariables)); + }, + }); + }; + + runRevokeRole({ data }); + }; + return (
+ { title: '', }} navLinks={navLinks} - activeLabel={username || ''} + activeLabel={user?.username || ''} pageTitle={user?.username || ''} pageSubtitle={user?.email || ''} actions={ diff --git a/src/authz-module/audit-user/messages.ts b/src/authz-module/audit-user/messages.ts index dbf9d7b3..dd3747fe 100644 --- a/src/authz-module/audit-user/messages.ts +++ b/src/authz-module/audit-user/messages.ts @@ -27,6 +27,21 @@ const messages = defineMessages( defaultMessage: 'Actions', description: 'Header for the actions column in the user table', }, + 'authz.user.table.view_all_permissions.link.text': { + id: 'authz.user.table.view_all_permissions.link.text', + defaultMessage: 'View all permissions', + description: 'Text for the link to view all permissions in the user table', + }, + 'authz.user.table.delete.action.alt': { + id: 'authz.user.table.delete.action.alt', + defaultMessage: 'Delete role action', + description: 'Alt description for delete button', + }, + 'authz.user.table.permissions.available.count': { + id: 'authz.user.table.permissions.available.count', + defaultMessage: '{count, plural, one {# permission available} other {# permissions available}}', + description: 'Text showing the number of permissions available, with proper pluralization', + }, }, ); diff --git a/src/authz-module/authz-home/index.test.tsx b/src/authz-module/authz-home/index.test.tsx index 818984da..d1526a23 100644 --- a/src/authz-module/authz-home/index.test.tsx +++ b/src/authz-module/authz-home/index.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { screen } from '@testing-library/react'; import { useAllRoleAssignments, useOrgs, useScopes } from '@src/authz-module/data/hooks'; -import { ToastManagerProvider } from '@src/authz-module/libraries-manager/ToastManagerContext'; +import { ToastManagerProvider } from '@src/components/ToastManager/ToastManagerContext'; import { renderWithAllProviders } from '@src/setupTest'; import userEvent from '@testing-library/user-event'; import AuthzHome from './index'; diff --git a/src/authz-module/components/ConfirmDeletionModal.tsx b/src/authz-module/components/ConfirmDeletionModal.tsx new file mode 100644 index 00000000..568ca8c5 --- /dev/null +++ b/src/authz-module/components/ConfirmDeletionModal.tsx @@ -0,0 +1,74 @@ +import { + ActionRow, AlertModal, Icon, ModalDialog, Stack, + StatefulButton, +} from '@openedx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { SpinnerSimple } from '@openedx/paragon/icons'; +import messages from './messages'; + +interface ConfirmDeletionModalProps { + isOpen: boolean; + close: () => void; + onSave: () => void; + isDeleting?: boolean; + context: { + userName: string; + scope: string; + role: string; + name?: string; + rolesCount: number; + } +} + +const ConfirmDeletionModal = ({ + isOpen, close, onSave, isDeleting, context, +}: ConfirmDeletionModalProps) => { + const intl = useIntl(); + return ( + + + {intl.formatMessage(messages['authz.manage.cancel.button'])} + + , + }} + state={isDeleting ? 'pending' : 'default'} + onClick={() => onSave()} + disabledStates={['pending']} + /> + + )} + isOverflowVisible={false} + > + +

{intl.formatMessage(messages['authz.team.remove.user.modal.body.1'], { + userName: context.userName, + scope: context.scope, + role: context.name ?? context.role, + })} +

+ {context.rolesCount === 1 && ( +

{intl.formatMessage(messages['authz.team.remove.user.modal.body.2'])}

+ )} +

{intl.formatMessage(messages['authz.team.remove.user.modal.body.3'])}

+
+ +
+ ); +}; + +export default ConfirmDeletionModal; diff --git a/src/authz-module/components/TableCells.test.tsx b/src/authz-module/components/TableCells.test.tsx index 062b8593..f43c3d94 100644 --- a/src/authz-module/components/TableCells.test.tsx +++ b/src/authz-module/components/TableCells.test.tsx @@ -9,8 +9,8 @@ import { OrgCell, ScopeCell, PermissionsCell, - ActionsCell, ViewAllPermissionsCell, + createActionsCell, } from './TableCells'; // TODO: remove console.log mocks and implement actual logic for these cells, then update tests accordingly @@ -481,56 +481,75 @@ describe('TableCells Components', () => { }); }); - describe('ActionsCell', () => { - const mockRow = { + describe('createActionsCell', () => { + const mockOnClickDeleteButton = jest.fn(); + const baseRow = { original: { - role: 'library_admin', id: '123', org: 'Test Org', scope: 'Test Scope', permissionCount: 1, + role: 'library_admin', + org: 'Test Org', + scope: 'Test Scope', + permissionCount: 1, }, }; - it('renders a delete button', () => { - const props = { - row: mockRow, - column: { id: 'actions' }, - }; + beforeEach(() => { + jest.clearAllMocks(); + }); - renderWrapper(); + it('renders a delete button and calls onClickDeleteButton when clicked', async () => { + const user = userEvent.setup(); + const CustomActionsCell = createActionsCell({ + onClickDeleteButton: mockOnClickDeleteButton, + isUserAuthenticatedPage: false, + }); + renderWrapper(); const deleteButton = screen.getByRole('button', { name: /delete role action/i }); expect(deleteButton).toBeInTheDocument(); + + await user.click(deleteButton); + expect(mockOnClickDeleteButton).toHaveBeenCalledWith({ name: 'Library Admin', role: 'library_admin', scope: 'Test Scope' }); }); - it('calls handleDelete when delete button is clicked', async () => { - const user = userEvent.setup(); - const props = { - row: mockRow, - column: { id: 'actions' }, + it('renders a disabled button for admin roles when isUserAuthenticatedPage is true', () => { + const adminRow = { + original: { + role: 'course_admin', + org: 'Test Org', + scope: 'Test Scope', + permissionCount: 1, + }, }; + const CustomActionsCell = createActionsCell({ + onClickDeleteButton: mockOnClickDeleteButton, + isUserAuthenticatedPage: true, + }); + renderWrapper(); - renderWrapper(); - - const deleteButton = screen.getByRole('button', { name: /delete role action/i }); - await user.click(deleteButton); - // TODO: replace console.log with actual delete logic and update this test accordingly - // eslint-disable-next-line no-console - expect(console.log).toHaveBeenCalledWith('Delete clicked for row:', mockRow); + const button = screen.getByRole('button', { name: /delete role action/i }); + expect(button).toBeDisabled(); }); - it('handles keyboard interaction for delete button', async () => { - const user = userEvent.setup(); - const props = { - row: mockRow, - column: { id: 'actions' }, + it('renders info icon with tooltip for Django managed roles', async () => { + const djangoRow = { + original: { + role: 'django.superuser', + org: 'Test Org', + scope: 'Test Scope', + permissionCount: 1, + }, }; + const user = userEvent.setup(); + const CustomActionsCell = createActionsCell({ + onClickDeleteButton: mockOnClickDeleteButton, + isUserAuthenticatedPage: true, + }); + renderWrapper(); - renderWrapper(); - - const deleteButton = screen.getByRole('button', { name: /delete role action/i }); - deleteButton.focus(); - await user.keyboard('{Enter}'); - // TODO: replace console.log with actual delete logic and update this test accordingly - // eslint-disable-next-line no-console - expect(console.log).toHaveBeenCalledWith('Delete clicked for row:', mockRow); + const infoIcon = screen.getByRole('img', { hidden: true }); + expect(infoIcon).toBeInTheDocument(); + await user.hover(infoIcon); + expect(screen.getByText(/Please go to Django Admin to manage it/i)).toBeInTheDocument(); }); }); diff --git a/src/authz-module/components/TableCells.tsx b/src/authz-module/components/TableCells.tsx index d15cb78f..3652324e 100644 --- a/src/authz-module/components/TableCells.tsx +++ b/src/authz-module/components/TableCells.tsx @@ -1,14 +1,19 @@ import { useIntl } from '@edx/frontend-platform/i18n'; -import { Icon, IconButton } from '@openedx/paragon'; import { AppContext } from '@edx/frontend-platform/react'; import { RemoveRedEye, Delete, ExpandMore, + Info, } from '@openedx/paragon/icons'; -import { TableCellValue, AppContextType, UserRole } from '@src/types'; +import { + TableCellValue, AppContextType, UserRole, RoleToDelete, +} from '@src/types'; import { useNavigate } from 'react-router-dom'; import { useContext, useMemo } from 'react'; -import { DJANGO_MANAGED_ROLES, MAP_ROLE_KEY_TO_LABEL } from '@src/authz-module/constants'; +import { ADMIN_ROLES, DJANGO_MANAGED_ROLES, MAP_ROLE_KEY_TO_LABEL } from '@src/authz-module/constants'; +import { + Icon, IconButton, OverlayTrigger, Tooltip, +} from '@openedx/paragon'; import { RESOURCE_ICONS } from './constants'; import messages from './messages'; import ViewMoreLink from './ViewMoreLink'; @@ -23,6 +28,13 @@ type ExtendedCellProps = CellPropsWithValue & { }; }; +type ActionsCellExtraProps = { + onClickDeleteButton: (role: RoleToDelete) => void; + isUserAuthenticatedPage: boolean; +}; + +type ActionsCellProps = CellProps & ActionsCellExtraProps; + const NameCell = ({ row }: CellProps) => { const intl = useIntl(); const { authenticatedUser } = useContext(AppContext) as AppContextType; @@ -109,19 +121,6 @@ const PermissionsCell = ({ row }: CellProps) => { ); }; -const ActionsCell = ({ row }: CellProps) => { - const { formatMessage } = useIntl(); - const handleDelete = () => { - // TODO: Implement delete functionality - // eslint-disable-next-line no-console - console.log('Delete clicked for row:', row); - }; - - return ( - - ); -}; - const ViewAllPermissionsCell = ({ row }: CellProps) => { const { formatMessage } = useIntl(); return ( @@ -135,6 +134,59 @@ const ViewAllPermissionsCell = ({ row }: CellProps) => { ); }; +const ActionsCell = ({ row, onClickDeleteButton, isUserAuthenticatedPage }: ActionsCellProps) => { + const { formatMessage } = useIntl(); + const { role } = row.original; + + const handleDelete = () => { + const roleToDelete = { + role, + scope: row.original.scope, + name: MAP_ROLE_KEY_TO_LABEL[role] || '', + } as RoleToDelete; + onClickDeleteButton(roleToDelete); + }; + + if (DJANGO_MANAGED_ROLES.includes(role)) { + return ( + + {formatMessage(messages['authz.user.table.delete.action.djangorole.tooltip'])} + + )} + > + + + ); + } + + if (ADMIN_ROLES.includes(role) && isUserAuthenticatedPage) { + return ( + + ); + } + + return ( + + ); +}; + +const createActionsCell = (extraProps: ActionsCellExtraProps) => function customActionsCell(cellProps) { + return ; +}; + export { NameCell, ViewActionCell, @@ -142,6 +194,6 @@ export { OrgCell, ScopeCell, PermissionsCell, - ActionsCell, ViewAllPermissionsCell, + createActionsCell, }; diff --git a/src/authz-module/components/messages.ts b/src/authz-module/components/messages.ts index 002d795f..3cb4a879 100644 --- a/src/authz-module/components/messages.ts +++ b/src/authz-module/components/messages.ts @@ -26,21 +26,11 @@ const messages = defineMessages({ defaultMessage: 'Clear filters', description: 'Button to clear all active filters in the table', }, - 'authz.user.table.org.all.organizations.label': { - id: 'authz.user.table.org.all.organizations.label', - defaultMessage: 'All Organizations', - description: 'Label for the "All Organizations" message on the user assignments table when a user has a django managed role assigned.', - }, 'authz.table.controlbar.search': { id: 'authz.table.controlbar.search', defaultMessage: 'Search', description: 'Search placeholder for two specific fields', }, - 'authz.user.table.scope.global.label': { - id: 'authz.user.table.scope.global.label', - defaultMessage: 'Global', - description: 'Label for the "Global" scope in the user assignments table when a user has a django managed role assigned.', - }, 'authz.table.controlbar.search.by.fields': { id: 'authz.table.controlbar.search.by.fields', defaultMessage: 'Search by {firstField} or {secondField}', @@ -64,7 +54,37 @@ const messages = defineMessages({ 'authz.table.footer.items.showing.text': { id: 'authz.table.footer.items.showing.text', defaultMessage: 'Showing {pageSize} of {itemCount}.', - description: 'Message displayed when the user reaches the applied filters limit', + description: 'Text in the table footer indicating how many items are being shown out of the total count.', + }, + 'authz.user.table.org.all.organizations.label': { + id: 'authz.user.table.org.all.organizations.label', + defaultMessage: 'All Organizations', + description: 'Label for the "All Organizations" message on the user assignments table when a user has a django managed role assigned.', + }, + 'authz.user.table.scope.global.label': { + id: 'authz.user.table.scope.global.label', + defaultMessage: 'Global', + description: 'Label for the "Global" scope in the user assignments table when a user has a django managed role assigned.', + }, + 'authz.user.table.permissions.access.label': { + id: 'authz.user.table.permissions.access.label', + defaultMessage: '{accessType, select, total {Total Access} partial {Partial Access} other {No Access}}', + description: 'Label for the permissions access level in the user assignments table, can be Total or Partial.', + }, + 'authz.user.table.permissions.available.count': { + id: 'authz.user.table.permissions.available.count', + defaultMessage: '{count, plural, one {# permission available} other {# permissions available}}', + description: 'Text showing the number of permissions available, with proper pluralization', + }, + 'authz.user.table.delete.action.alt': { + id: 'authz.user.table.delete.action.alt', + defaultMessage: 'Delete role action', + description: 'Alt description for delete button', + }, + 'authz.user.table.view_all_permissions.link.text': { + id: 'authz.user.table.view_all_permissions.link.text', + defaultMessage: 'View all permissions', + description: 'Text for the link to view all permissions in the user table', }, 'authz.table.username.current': { id: 'authz.table.username.current', @@ -97,40 +117,45 @@ const messages = defineMessages({ defaultMessage: 'Search to show more', description: 'Message displayed when there are more results available than currently shown', }, - 'authz.table.footer.items.showing.text': { - id: 'authz.table.footer.items.showing.text', - defaultMessage: 'Showing {pageSize} of {itemCount}.', - description: 'Text in the table footer indicating how many items are being shown out of the total count.', - }, - 'authz.user.table.org.all.organizations.label': { - id: 'authz.user.table.org.all.organizations.label', - defaultMessage: 'All Organizations', - description: 'Label for the "All Organizations" message on the user assignments table when a user has a django managed role assigned.', - }, - 'authz.user.table.scope.global.label': { - id: 'authz.user.table.scope.global.label', - defaultMessage: 'Global', - description: 'Label for the "Global" scope in the user assignments table when a user has a django managed role assigned.', - }, - 'authz.user.table.permissions.access.label': { - id: 'authz.user.table.permissions.access.label', - defaultMessage: '{accessType, select, total {Total Access} partial {Partial Access} other {No Access}}', - description: 'Label for the permissions access level in the user assignments table, can be Total or Partial.', - }, - 'authz.user.table.permissions.available.count': { - id: 'authz.user.table.permissions.available.count', - defaultMessage: '{count, plural, one {# permission available} other {# permissions available}}', - description: 'Text showing the number of permissions available, with proper pluralization', - }, - 'authz.user.table.delete.action.alt': { - id: 'authz.user.table.delete.action.alt', - defaultMessage: 'Delete role action', - description: 'Alt description for delete button', - }, - 'authz.user.table.view_all_permissions.link.text': { - id: 'authz.user.table.view_all_permissions.link.text', - defaultMessage: 'View all permissions', - description: 'Text for the link to view all permissions in the user table', + 'authz.team.remove.user.modal.title': { + id: 'authz.team.remove.user.modal.title', + defaultMessage: 'Remove role?', + description: 'AuthZ team management remove user modal title', + }, + 'authz.manage.cancel.button': { + id: 'authz.manage.cancel.button', + defaultMessage: 'Cancel', + description: 'AuthZ cancel button title', + }, + 'authz.manage.remove.button': { + id: 'authz.manage.remove.button', + defaultMessage: 'Remove', + description: 'AuthZ remove button title', + }, + 'authz.manage.removing.button': { + id: 'authz.manage.removing.button', + defaultMessage: 'Removing', + description: 'AuthZ removing button title', + }, + 'authz.team.remove.user.modal.body.1': { + id: 'authz.team.remove.user.modal.body.1', + defaultMessage: 'Are you sure you want to remove the {role} role from the user “{userName}” in the scope {scope}?', + description: 'AuthZ team management remove user modal body', + }, + 'authz.team.remove.user.modal.body.2': { + id: 'authz.team.remove.user.modal.body.2', + defaultMessage: "This is the user's only role in this scope. Removing it will revoke their access completely, and they will no longer appear in the scope's member list.", + description: 'AuthZ team management remove user modal body', + }, + 'authz.team.remove.user.modal.body.3': { + id: 'authz.team.remove.user.modal.body.3', + defaultMessage: 'Are you sure you want to proceed?', + description: 'AuthZ team management remove user modal body', + }, + 'authz.user.table.delete.action.djangorole.tooltip': { + id: 'authz.user.table.delete.action.djangorole.tooltip', + defaultMessage: 'You can’t remove this role here. Please go to Django Admin to manage it.', + description: 'Tooltip for delete button when hovering over Django roles', }, }); diff --git a/src/authz-module/constants.ts b/src/authz-module/constants.ts index 997b1366..f94c2c56 100644 --- a/src/authz-module/constants.ts +++ b/src/authz-module/constants.ts @@ -140,3 +140,4 @@ export const DJANGO_MANAGED_ROLES = ['django.superuser', 'django.globalstaff']; export const TABLE_DEFAULT_PAGE_SIZE = 10; export const DEFAULT_FILTER_PAGE_SIZE = 5; +export const ADMIN_ROLES = ['course_admin', 'library_admin']; diff --git a/src/authz-module/data/api.ts b/src/authz-module/data/api.ts index b9db8bbb..a2c1d477 100644 --- a/src/authz-module/data/api.ts +++ b/src/authz-module/data/api.ts @@ -33,6 +33,7 @@ export type RevokeUserRolesRequest = { users: string; role: string; scope: string; + querySettings?: QuerySettings; }; export interface DeleteRevokeUserRolesResponse { @@ -199,22 +200,22 @@ export const getScopes = async (search?: string, page?: number, pageSize?: numbe return camelCaseObject(data); }; -export const getUserAssignedRoles = async (username: string, querySettings: QuerySettings) +export const getUserAssignedRoles = async (username?: string, querySettings?: QuerySettings) : Promise => { const url = new URL(getApiUrl(`/api/authz/v1/users/${username}/assignments/`)); - if (querySettings.roles) { + if (querySettings?.roles) { url.searchParams.set('roles', querySettings.roles); } - if (querySettings.search) { + if (querySettings?.search) { url.searchParams.set('search', querySettings.search); } - if (querySettings.sortBy && querySettings.order) { + if (querySettings?.sortBy && querySettings?.order) { url.searchParams.set('sort_by', querySettings.sortBy); - url.searchParams.set('order', querySettings.order); + url.searchParams.set('order', querySettings?.order || ''); } - url.searchParams.set('page_size', querySettings.pageSize.toString()); - url.searchParams.set('page', (querySettings.pageIndex + 1).toString()); + url.searchParams.set('page_size', querySettings?.pageSize?.toString() || ''); + url.searchParams.set('page', ((querySettings?.pageIndex ?? 0) + 1).toString()); const { data } = await getAuthenticatedHttpClient().get(url); return camelCaseObject(data); diff --git a/src/authz-module/data/hooks.ts b/src/authz-module/data/hooks.ts index cfbc71ba..5b3ed7b4 100644 --- a/src/authz-module/data/hooks.ts +++ b/src/authz-module/data/hooks.ts @@ -3,6 +3,7 @@ import { } from '@tanstack/react-query'; import { appId } from '@src/constants'; import { LibraryMetadata } from '@src/types'; +import { useQuerySettings } from '@src/authz-module/hooks/useQuerySettings'; import { assignTeamMembersRole, AssignTeamMembersRoleRequest, getAllRoleAssignments, GetAllRoleAssignmentsResponse, getLibrary, getOrgs, GetOrgsResponse, @@ -21,7 +22,7 @@ const authzQueryKeys = { allRoleAssignments: (querySettings?: QuerySettings) => [...authzQueryKeys.all, 'allRoleAssignments', querySettings] as const, orgs: (search?: string, page?: number, pageSize?: number) => [...authzQueryKeys.all, 'organizations', search, page, pageSize] as const, scopes: (search?: string, page?: number, pageSize?: number) => [...authzQueryKeys.all, 'scopes', search, page, pageSize] as const, - userRoles: (username: string, querySettings?: QuerySettings) => [...authzQueryKeys.all, 'userRoles', username, querySettings] as const, + userRoles: (username?: string, querySettings?: QuerySettings) => [...authzQueryKeys.all, 'userRoles', username, querySettings] as const, }; /** @@ -107,13 +108,18 @@ export const useAssignTeamMembersRole = () => { */ export const useRevokeUserRoles = () => { const queryClient = useQueryClient(); + const { querySettings: defaultQuerySettings } = useQuerySettings(); return useMutation({ mutationFn: async ({ data }: { data: RevokeUserRolesRequest }) => revokeUserRoles(data), - onSettled: (_data, _error, { data: { scope } }) => { + onSettled: (_data, _error, { data: { scope, users, querySettings } }) => { queryClient.invalidateQueries({ queryKey: authzQueryKeys.teamMembersAll(scope) }); queryClient.invalidateQueries({ queryKey: authzQueryKeys.permissionsByRole(scope) }); + queryClient.invalidateQueries({ + queryKey: authzQueryKeys.userRoles(users, querySettings), + }); + queryClient.invalidateQueries({ queryKey: authzQueryKeys.allRoleAssignments(defaultQuerySettings) }); }, }); }; @@ -181,10 +187,12 @@ export const useScopes = (search?: string, page?: number, pageSize?: number) => * ``` */ export const useUserAssignedRoles = ( - username: string, - querySettings: QuerySettings, + username?: string, + querySettings?: QuerySettings, ) => useQuery({ queryKey: authzQueryKeys.userRoles(username, querySettings), queryFn: () => getUserAssignedRoles(username, querySettings), staleTime: 1000 * 60 * 30, // refetch after 30 minutes + enabled: !!username, + refetchOnWindowFocus: false, }); diff --git a/src/authz-module/index.tsx b/src/authz-module/index.tsx index 3a6de7f2..7fa21a68 100644 --- a/src/authz-module/index.tsx +++ b/src/authz-module/index.tsx @@ -5,7 +5,7 @@ import { QueryErrorResetBoundary } from '@tanstack/react-query'; import LoadingPage from '@src/components/LoadingPage'; import LibrariesErrorFallback from '@src/authz-module/libraries-manager/ErrorPage'; import { CustomErrors } from '@src/constants'; -import { ToastManagerProvider } from './libraries-manager/ToastManagerContext'; +import { ToastManagerProvider } from '@src/components/ToastManager/ToastManagerContext'; import { LibrariesUserManager, LibrariesLayout, LibrariesTeamManager } from './libraries-manager'; import AuthzHome from './authz-home'; import AuditUserPage from './audit-user'; @@ -35,6 +35,10 @@ const AuthZModule = () => ( element={} /> } /> + } + /> diff --git a/src/authz-module/libraries-manager/LibrariesTeamManager.test.tsx b/src/authz-module/libraries-manager/LibrariesTeamManager.test.tsx index 959c044e..f0f42a9a 100644 --- a/src/authz-module/libraries-manager/LibrariesTeamManager.test.tsx +++ b/src/authz-module/libraries-manager/LibrariesTeamManager.test.tsx @@ -3,9 +3,9 @@ import userEvent from '@testing-library/user-event'; import { renderWrapper } from '@src/setupTest'; import { initializeMockApp } from '@edx/frontend-platform/testing'; import { useLibrary } from '@src/authz-module/data/hooks'; +import { ToastManagerProvider } from '@src/components/ToastManager/ToastManagerContext'; import { useLibraryAuthZ } from './context'; import LibrariesTeamManager from './LibrariesTeamManager'; -import { ToastManagerProvider } from './ToastManagerContext'; import { CONTENT_LIBRARY_PERMISSIONS } from '../constants'; jest.mock('./context', () => { diff --git a/src/authz-module/libraries-manager/LibrariesUserManager.test.tsx b/src/authz-module/libraries-manager/LibrariesUserManager.test.tsx index 77ea2ca9..46553efb 100644 --- a/src/authz-module/libraries-manager/LibrariesUserManager.test.tsx +++ b/src/authz-module/libraries-manager/LibrariesUserManager.test.tsx @@ -2,10 +2,10 @@ import { useParams } from 'react-router-dom'; import { screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { renderWrapper } from '@src/setupTest'; +import { ToastManagerProvider } from '@src/components/ToastManager/ToastManagerContext'; import LibrariesUserManager from './LibrariesUserManager'; import { useLibraryAuthZ } from './context'; import { useLibrary, useTeamMembers, useRevokeUserRoles } from '../data/hooks'; -import { ToastManagerProvider } from './ToastManagerContext'; jest.mock('@edx/frontend-platform/logging', () => ({ logError: jest.fn(), diff --git a/src/authz-module/libraries-manager/LibrariesUserManager.tsx b/src/authz-module/libraries-manager/LibrariesUserManager.tsx index 7f495d42..10fab16e 100644 --- a/src/authz-module/libraries-manager/LibrariesUserManager.tsx +++ b/src/authz-module/libraries-manager/LibrariesUserManager.tsx @@ -4,7 +4,7 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { Container, Skeleton } from '@openedx/paragon'; import { ROUTES } from '@src/authz-module/constants'; import { Role } from 'types'; -import { useToastManager } from '@src/authz-module/libraries-manager/ToastManagerContext'; +import { useToastManager } from '@src/components/ToastManager/ToastManagerContext'; import AuthZLayout from '../components/AuthZLayout'; import { useLibraryAuthZ } from './context'; import RoleCard from '../components/RoleCard'; diff --git a/src/authz-module/libraries-manager/components/AddNewTeamMemberModal/AddNewTeamMemberTrigger.test.tsx b/src/authz-module/libraries-manager/components/AddNewTeamMemberModal/AddNewTeamMemberTrigger.test.tsx index 727d6a1f..c7bd1cee 100644 --- a/src/authz-module/libraries-manager/components/AddNewTeamMemberModal/AddNewTeamMemberTrigger.test.tsx +++ b/src/authz-module/libraries-manager/components/AddNewTeamMemberModal/AddNewTeamMemberTrigger.test.tsx @@ -3,7 +3,7 @@ import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { renderWrapper } from '@src/setupTest'; import { useAssignTeamMembersRole } from '@src/authz-module/data/hooks'; -import { ToastManagerProvider } from '@src/authz-module/libraries-manager/ToastManagerContext'; +import { ToastManagerProvider } from '@src/components/ToastManager/ToastManagerContext'; import AddNewTeamMemberTrigger from './AddNewTeamMemberTrigger'; jest.mock('@edx/frontend-platform/logging'); diff --git a/src/authz-module/libraries-manager/components/AddNewTeamMemberModal/AddNewTeamMemberTrigger.tsx b/src/authz-module/libraries-manager/components/AddNewTeamMemberModal/AddNewTeamMemberTrigger.tsx index e21da8d9..66c5c408 100644 --- a/src/authz-module/libraries-manager/components/AddNewTeamMemberModal/AddNewTeamMemberTrigger.tsx +++ b/src/authz-module/libraries-manager/components/AddNewTeamMemberModal/AddNewTeamMemberTrigger.tsx @@ -6,7 +6,7 @@ import { Plus } from '@openedx/paragon/icons'; import { PutAssignTeamMembersRoleResponse } from '@src/authz-module/data/api'; import { useAssignTeamMembersRole } from '@src/authz-module/data/hooks'; import { RoleOperationErrorStatus, DEFAULT_TOAST_DELAY } from '@src/authz-module/constants'; -import { AppToast, useToastManager } from '@src/authz-module/libraries-manager/ToastManagerContext'; +import { AppToast, useToastManager } from '@src/components/ToastManager/ToastManagerContext'; import baseMessages from '@src/authz-module/messages'; import AddNewTeamMemberModal from './AddNewTeamMemberModal'; import messages from './messages'; diff --git a/src/authz-module/libraries-manager/components/AssignNewRoleModal/AssignNewRoleTrigger.test.tsx b/src/authz-module/libraries-manager/components/AssignNewRoleModal/AssignNewRoleTrigger.test.tsx index a8d10830..069d762c 100644 --- a/src/authz-module/libraries-manager/components/AssignNewRoleModal/AssignNewRoleTrigger.test.tsx +++ b/src/authz-module/libraries-manager/components/AssignNewRoleModal/AssignNewRoleTrigger.test.tsx @@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event'; import { renderWrapper } from '@src/setupTest'; import { useLibraryAuthZ } from '@src/authz-module/libraries-manager/context'; import { useAssignTeamMembersRole } from '@src/authz-module/data/hooks'; -import { ToastManagerProvider } from '@src/authz-module/libraries-manager/ToastManagerContext'; +import { ToastManagerProvider } from '@src/components/ToastManager/ToastManagerContext'; import AssignNewRoleTrigger from './AssignNewRoleTrigger'; jest.mock('@edx/frontend-platform/logging'); diff --git a/src/authz-module/libraries-manager/components/AssignNewRoleModal/AssignNewRoleTrigger.tsx b/src/authz-module/libraries-manager/components/AssignNewRoleModal/AssignNewRoleTrigger.tsx index dc04c642..71aecb71 100644 --- a/src/authz-module/libraries-manager/components/AssignNewRoleModal/AssignNewRoleTrigger.tsx +++ b/src/authz-module/libraries-manager/components/AssignNewRoleModal/AssignNewRoleTrigger.tsx @@ -5,7 +5,7 @@ import { Plus } from '@openedx/paragon/icons'; import { useLibraryAuthZ } from '@src/authz-module/libraries-manager/context'; import { useAssignTeamMembersRole } from '@src/authz-module/data/hooks'; -import { useToastManager } from '@src/authz-module/libraries-manager/ToastManagerContext'; +import { useToastManager } from '@src/components/ToastManager/ToastManagerContext'; import AssignNewRoleModal from './AssignNewRoleModal'; import messages from '../messages'; diff --git a/src/authz-module/libraries-manager/components/TeamTable/index.test.tsx b/src/authz-module/libraries-manager/components/TeamTable/index.test.tsx index f1229274..247ede1c 100644 --- a/src/authz-module/libraries-manager/components/TeamTable/index.test.tsx +++ b/src/authz-module/libraries-manager/components/TeamTable/index.test.tsx @@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event'; import { renderWrapper } from '@src/setupTest'; import { useTeamMembers } from '@src/authz-module/data/hooks'; import { useLibraryAuthZ } from '@src/authz-module/libraries-manager/context'; -import { ToastManagerProvider } from '@src/authz-module/libraries-manager/ToastManagerContext'; +import { ToastManagerProvider } from '@src/components/ToastManager/ToastManagerContext'; import { CONTENT_LIBRARY_PERMISSIONS } from '@src/authz-module/constants'; import TeamTable from './index'; diff --git a/src/authz-module/libraries-manager/components/TeamTable/index.tsx b/src/authz-module/libraries-manager/components/TeamTable/index.tsx index 77efa3ad..99f38bc3 100644 --- a/src/authz-module/libraries-manager/components/TeamTable/index.tsx +++ b/src/authz-module/libraries-manager/components/TeamTable/index.tsx @@ -10,8 +10,8 @@ import { import { useTeamMembers } from '@src/authz-module/data/hooks'; import { useLibraryAuthZ } from '@src/authz-module/libraries-manager/context'; -import { useToastManager } from '@src/authz-module/libraries-manager/ToastManagerContext'; import { SKELETON_ROWS, TABLE_DEFAULT_PAGE_SIZE } from '@src/authz-module/constants'; +import { useToastManager } from '@src/components/ToastManager/ToastManagerContext'; import { useQuerySettings } from '@src/authz-module/hooks/useQuerySettings'; import TableControlBar from './components/TableControlBar'; import messages from './messages'; diff --git a/src/authz-module/libraries-manager/messages.ts b/src/authz-module/libraries-manager/messages.ts index bbb2ad41..10aba4f6 100644 --- a/src/authz-module/libraries-manager/messages.ts +++ b/src/authz-module/libraries-manager/messages.ts @@ -81,31 +81,6 @@ const messages = defineMessages({ defaultMessage: 'Something went wrong on our end.

Please try again later.', description: 'Libraries default error message', }, - 'library.authz.team.toast.500.error.message': { - id: 'library.authz.team.toast.500.error.message', - defaultMessage: 'We\'re experiencing technical difficulties.

Please try again later.', - description: 'Libraries internal server error message', - }, - 'library.authz.team.toast.502.error.message': { - id: 'library.authz.team.toast.502.error.message', - defaultMessage: 'We\'re having trouble connecting to our services.

Please try again later.', - description: 'Libraries bad gateway error message', - }, - 'library.authz.team.toast.503.error.message': { - id: 'library.authz.team.toast.503.error.message', - defaultMessage: 'The service is temporarily unavailable.

Please try again in a few moments.', - description: 'Libraries service temporary unavailable message', - }, - 'library.authz.team.toast.408.error.message': { - id: 'library.authz.team.toast.408.error.message', - defaultMessage: 'The request took too long.

Please check your connection and try again.', - description: 'Libraries request timeout message', - }, - 'library.authz.team.toast.retry.label': { - id: 'library.authz.team.toast.retry.label', - defaultMessage: 'Retry', - description: 'Label for retry button.', - }, }); export default messages; diff --git a/src/authz-module/messages.ts b/src/authz-module/messages.ts index 48d0a3c2..3b3916f6 100644 --- a/src/authz-module/messages.ts +++ b/src/authz-module/messages.ts @@ -7,16 +7,46 @@ const messages = defineMessages( defaultMessage: 'Roles and Permissions Management', description: 'Text for the roles and permissions management home page title navigation link', }, - 'authz.management.specific.user.nav.link': { - id: 'authz.management.specific.user.nav.link', - defaultMessage: 'Specific User', - description: 'Text for the specific user page navigation link', - }, 'authz.management.assign.role.title': { id: 'authz.management.assign.role.title', defaultMessage: 'Assign Role', description: 'Text for the assign role button', }, + 'authz.team.toast.default.error.message': { + id: 'authz.team.toast.default.error.message', + defaultMessage: 'Something went wrong on our end.

Please try again later.', + description: 'Default error message', + }, + 'authz.team.remove.user.toast.success.description': { + id: 'authz.team.remove.user.toast.success.description', + defaultMessage: 'The {role} role has been successfully removed.{rolesCount, plural, =0 { The user no longer has access to this library and has been removed from the member list.} other {}}', + description: 'Team management remove user toast success', + }, + 'authz.team.toast.500.error.message': { + id: 'authz.team.toast.500.error.message', + defaultMessage: 'We\'re experiencing technical difficulties.

Please try again later.', + description: 'Internal server error message', + }, + 'authz.team.toast.502.error.message': { + id: 'authz.team.toast.502.error.message', + defaultMessage: 'We\'re having trouble connecting to our services.

Please try again later.', + description: 'Bad gateway error message', + }, + 'authz.team.toast.503.error.message': { + id: 'authz.team.toast.503.error.message', + defaultMessage: 'The service is temporarily unavailable.

Please try again in a few moments.', + description: 'Service temporarily unavailable message', + }, + 'authz.team.toast.408.error.message': { + id: 'authz.team.toast.408.error.message', + defaultMessage: 'The request took too long.

Please check your connection and try again.', + description: 'Request timeout message', + }, + 'authz.team.toast.retry.label': { + id: 'authz.team.toast.retry.label', + defaultMessage: 'Retry', + description: 'Label for retry button.', + }, }, ); diff --git a/src/authz-module/roles-permissions/libraries/messages.ts b/src/authz-module/roles-permissions/libraries/messages.ts index bada55fd..13e102f4 100644 --- a/src/authz-module/roles-permissions/libraries/messages.ts +++ b/src/authz-module/roles-permissions/libraries/messages.ts @@ -61,31 +61,6 @@ const messages = defineMessages({ defaultMessage: 'Something went wrong on our end.

Please try again later.', description: 'Libraries default error message', }, - 'library.authz.team.toast.500.error.message': { - id: 'library.authz.team.toast.500.error.message', - defaultMessage: 'We\'re experiencing technical difficulties.

Please try again later.', - description: 'Libraries internal server error message', - }, - 'library.authz.team.toast.502.error.message': { - id: 'library.authz.team.toast.502.error.message', - defaultMessage: 'We\'re having trouble connecting to our services.

Please try again later.', - description: 'Libraries bad gateway error message', - }, - 'library.authz.team.toast.503.error.message': { - id: 'library.authz.team.toast.503.error.message', - defaultMessage: 'The service is temporarily unavailable.

Please try again in a few moments.', - description: 'Libraries service temporary unavailable message', - }, - 'library.authz.team.toast.408.error.message': { - id: 'library.authz.team.toast.408.error.message', - defaultMessage: 'The request took too long.

Please check your connection and try again.', - description: 'Libraries request timeout message', - }, - 'library.authz.team.toast.retry.label': { - id: 'library.authz.team.toast.retry.label', - defaultMessage: 'Retry', - description: 'Label for retry button.', - }, }); export default messages; diff --git a/src/authz-module/roles-permissions/libraries/utils.ts b/src/authz-module/roles-permissions/libraries/utils.ts index 7dd5cf53..4cedb3f3 100644 --- a/src/authz-module/roles-permissions/libraries/utils.ts +++ b/src/authz-module/roles-permissions/libraries/utils.ts @@ -4,7 +4,7 @@ import { EnrichedPermission, PermissionMetadata, PermissionsResourceGrouped, PermissionsRoleGrouped, ResourceMetadata, Role, RoleResourceGroup, } from '@src/types'; -import actionMessages from '../../components/RoleCard/messages'; +import actionMessages from '@src/authz-module/components/RoleCard/messages'; /** * Derives the localized label and action key for a given permission. diff --git a/src/authz-module/team-members/TeamMembersTable.test.tsx b/src/authz-module/team-members/TeamMembersTable.test.tsx index 7619b88c..4252a6db 100644 --- a/src/authz-module/team-members/TeamMembersTable.test.tsx +++ b/src/authz-module/team-members/TeamMembersTable.test.tsx @@ -3,7 +3,7 @@ import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { renderWithAllProviders } from '@src/setupTest'; import { useAllRoleAssignments, useOrgs, useScopes } from '@src/authz-module/data/hooks'; -import { ToastManagerProvider } from '@src/authz-module/libraries-manager/ToastManagerContext'; +import { ToastManagerProvider } from '@src/components/ToastManager/ToastManagerContext'; import TeamMembersTable from './TeamMembersTable'; const mockedAllRoleAssignments = { diff --git a/src/authz-module/team-members/TeamMembersTable.tsx b/src/authz-module/team-members/TeamMembersTable.tsx index efa930fe..4749dcd6 100644 --- a/src/authz-module/team-members/TeamMembersTable.tsx +++ b/src/authz-module/team-members/TeamMembersTable.tsx @@ -6,7 +6,7 @@ import { TextFilter, } from '@openedx/paragon'; -import { useToastManager } from '@src/authz-module/libraries-manager/ToastManagerContext'; +import { useToastManager } from '@src/components/ToastManager/ToastManagerContext'; import { useQuerySettings } from '@src/authz-module/hooks/useQuerySettings'; import OrgFilter from '@src/authz-module/components/TableControlBar/OrgFilter'; import RolesFilter from '@src/authz-module/components/TableControlBar/RolesFilter'; diff --git a/src/authz-module/libraries-manager/ToastManagerContext.test.tsx b/src/components/ToastManager/ToastManagerContext.test.tsx similarity index 100% rename from src/authz-module/libraries-manager/ToastManagerContext.test.tsx rename to src/components/ToastManager/ToastManagerContext.test.tsx diff --git a/src/authz-module/libraries-manager/ToastManagerContext.tsx b/src/components/ToastManager/ToastManagerContext.tsx similarity index 85% rename from src/authz-module/libraries-manager/ToastManagerContext.tsx rename to src/components/ToastManager/ToastManagerContext.tsx index 6871f2fd..dcf454d2 100644 --- a/src/authz-module/libraries-manager/ToastManagerContext.tsx +++ b/src/components/ToastManager/ToastManagerContext.tsx @@ -4,20 +4,20 @@ import { import { logError } from '@edx/frontend-platform/logging'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Toast } from '@openedx/paragon'; -import messages from './messages'; -import { DEFAULT_TOAST_DELAY, RETRY_TOAST_DELAY } from '../constants'; +import messages from '@src/authz-module/messages'; +import { DEFAULT_TOAST_DELAY, RETRY_TOAST_DELAY } from '@src/authz-module/constants'; type ToastType = 'success' | 'error' | 'error-retry'; export const ERROR_TOAST_MAP: Record = { // Transient (retryable) server errors - 500: { type: 'error-retry', messageId: 'library.authz.team.toast.500.error.message' }, - 502: { type: 'error-retry', messageId: 'library.authz.team.toast.502.error.message' }, - 503: { type: 'error-retry', messageId: 'library.authz.team.toast.503.error.message' }, - 408: { type: 'error-retry', messageId: 'library.authz.team.toast.408.error.message' }, + 500: { type: 'error-retry', messageId: 'authz.team.toast.500.error.message' }, + 502: { type: 'error-retry', messageId: 'authz.team.toast.502.error.message' }, + 503: { type: 'error-retry', messageId: 'authz.team.toast.503.error.message' }, + 408: { type: 'error-retry', messageId: 'authz.team.toast.408.error.message' }, // Generic fallback error - DEFAULT: { type: 'error-retry', messageId: 'library.authz.team.toast.default.error.message' }, + DEFAULT: { type: 'error-retry', messageId: 'authz.team.toast.default.error.message' }, }; export interface AppToast { @@ -108,7 +108,7 @@ export const ToastManagerProvider = ({ children }: ToastManagerProviderProps) => discardToast(toast.id); toast.onRetry?.(); }, - label: intl.formatMessage(messages['library.authz.team.toast.retry.label']), + label: intl.formatMessage(messages['authz.team.toast.retry.label']), } : undefined} > {toast.message} diff --git a/src/types.ts b/src/types.ts index 1ce1c194..2b269964 100644 --- a/src/types.ts +++ b/src/types.ts @@ -32,7 +32,9 @@ export interface RoleMetadata { name: string; description: string; } +// TODO: remove unnecessary fields when libraries gets removed export interface Role extends RoleMetadata { + scope: string; userCount: number; permissions: string[]; disabled?: boolean; @@ -117,3 +119,9 @@ export interface UserRole { username?: string; email?: string; } + +export type RoleToDelete = { + role: string; + name?: string; + scope: string; +};