Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/authz-module/components/AuthZTitle.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('AuthZTitle', () => {

it('renders page title', () => {
render(<AuthZTitle {...defaultProps} />);
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent(defaultProps.pageTitle);
expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(defaultProps.pageTitle);
});

it('renders page subtitle as ReactNode', () => {
Expand Down
13 changes: 8 additions & 5 deletions src/authz-module/components/AuthZTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
} from 'react';
import { Link } from 'react-router-dom';
import {
Breadcrumb, Col, Container, Row, Button, Badge,
Breadcrumb, Col, Container, Row, Button,
Stack,
useMediaQuery,
breakpoints,
Expand Down Expand Up @@ -50,10 +50,13 @@ const AuthZTitle = ({
/>
<Row className="mt-4">
<Col xs={12} md={7} className="mb-4">
<h1 className="text-primary">{pageTitle}</h1>
{typeof pageSubtitle === 'string'
? <h3><Badge className="py-2 px-3 font-weight-normal" variant="light">{pageSubtitle}</Badge></h3>
: pageSubtitle}
<div className="d-flex align-items-center">
<h2 className="text-primary mb-0">{pageTitle}</h2>
{typeof pageSubtitle === 'string'
? <><hr className="mx-lg-3" /><h3 className="mb-0 py-2 font-weight-light text-gray-700">{pageSubtitle}</h3></>
: <><hr className="mx-lg-3" /> <div className="mb-0">{pageSubtitle}</div></>}

</div>
</Col>
<Col xs={12} md={5}>
<Stack className="justify-content-end" direction={isDesktop ? 'horizontal' : 'vertical'}>
Expand Down
16 changes: 1 addition & 15 deletions src/authz-module/data/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { LibraryMetadata, TeamMember } from '@src/types';
import { camelCaseObject, snakeCaseObject } from '@edx/frontend-platform';
import { camelCaseObject } from '@edx/frontend-platform';
import { getApiUrl, getStudioApiUrl } from '@src/data/utils';

export interface QuerySettings {
Expand Down Expand Up @@ -108,17 +108,3 @@ export const revokeUserRoles = async (
const res = await getAuthenticatedHttpClient().delete(url.toString());
return camelCaseObject(res.data);
};

export const updateLibrary = async (libraryId, updatedData): Promise<LibraryMetadata> => {
const { data } = await getAuthenticatedHttpClient().patch(
getStudioApiUrl(`/api/libraries/v2/${libraryId}/`),
snakeCaseObject(updatedData),
);
return {
id: data.id,
org: data.org,
title: data.title,
slug: data.slug,
allowPublicRead: data.allow_public_read,
};
};
78 changes: 0 additions & 78 deletions src/authz-module/data/hooks.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import {
useLibrary, usePermissionsByRole, useTeamMembers, useAssignTeamMembersRole, useRevokeUserRoles,
useUpdateLibrary,
} from './hooks';

jest.mock('@edx/frontend-platform/auth', () => ({
Expand Down Expand Up @@ -341,80 +340,3 @@ describe('useRevokeUserRoles', () => {
expect(calledUrl.searchParams.get('scope')).toBe(revokeRoleData.scope);
});
});

describe('useUpdateLibrary', () => {
const queryKeyTest = ['org.openedx.frontend.app.adminConsole', 'authz', 'library', 'lib:123'];

beforeEach(() => {
jest.clearAllMocks();
});

it('calls updateLibrary with correct params and updates cache', async () => {
const mockData = { id: 'lib:123', title: 'Library Test' };
getAuthenticatedHttpClient.mockReturnValue({
patch: jest.fn().mockResolvedValue({ data: mockData }),
});
const { result } = renderHook(() => useUpdateLibrary(), { wrapper: createWrapper() });

await act(async () => {
await result.current.mutateAsync({
libraryId: 'lib:123',
updatedData: { title: 'Library Test' },
});
});

expect(getAuthenticatedHttpClient).toHaveBeenCalled();
});

it('sets query data on success', async () => {
const mockData = { id: 'lib:123', title: 'Updated Library' };
getAuthenticatedHttpClient.mockReturnValue({
patch: jest.fn().mockResolvedValue({ data: mockData }),
});

const queryClient = new QueryClient();
const wrapper = ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);

const { result } = renderHook(() => useUpdateLibrary(), { wrapper });

await act(async () => {
await result.current.mutateAsync({
libraryId: 'lib:123',
updatedData: { title: 'Updated Library' },
});
});

// verify cache updated with the returned data
expect(queryClient.getQueryData(queryKeyTest)).toEqual(mockData);
expect(getAuthenticatedHttpClient).toHaveBeenCalled();
});

it('invalidates query on settled', async () => {
const mockData = { id: 'lib:123', title: 'Final Title' };
getAuthenticatedHttpClient.mockReturnValue({
patch: jest.fn().mockResolvedValue({ data: mockData }),
});
const queryClient = new QueryClient();
const invalidateSpy = jest.spyOn(queryClient, 'invalidateQueries');

const wrapper = ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);

const { result } = renderHook(() => useUpdateLibrary(), { wrapper });

await act(async () => {
await result.current.mutateAsync({
libraryId: 'lib:123',
updatedData: { title: 'Final Title' },
});
});

expect(invalidateSpy).toHaveBeenCalledWith({
queryKey: queryKeyTest,
});
expect(getAuthenticatedHttpClient).toHaveBeenCalled();
});
});
26 changes: 0 additions & 26 deletions src/authz-module/data/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { LibraryMetadata } from '@src/types';
import {
assignTeamMembersRole, AssignTeamMembersRoleRequest, getLibrary, getPermissionsByRole, getTeamMembers,
GetTeamMembersResponse, PermissionsByRole, QuerySettings, revokeUserRoles, RevokeUserRolesRequest,
updateLibrary,
} from './api';

const authzQueryKeys = {
Expand Down Expand Up @@ -111,28 +110,3 @@ export const useRevokeUserRoles = () => {
},
});
};

/**
* React Query hook to update the library metadata.
*
* @example
* const { mutate: updateLibrary } = useUpdateLibrary();
* updateLibrary({ libraryId: 'lib:123', updatedData: { title: 'Library Test' }});
*/

export const useUpdateLibrary = () => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: ({ libraryId, updatedData }: {
libraryId: string;
updatedData: Partial<LibraryMetadata>
}) => updateLibrary(libraryId, updatedData),
onSuccess: (data) => {
queryClient.setQueryData(authzQueryKeys.library(data.id), data);
},
onSettled: (_data, _error, variables) => {
queryClient.invalidateQueries({ queryKey: authzQueryKeys.library(variables.libraryId) });
},
});
};
43 changes: 0 additions & 43 deletions src/authz-module/libraries-manager/LibrariesTeamManager.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,49 +150,6 @@ describe('LibrariesTeamManager', () => {
expect(matrixScope.getByText('view')).toBeInTheDocument();
});

it('renders allow public library read toggle and change the value by user interaction', async () => {
const user = userEvent.setup();

renderTeamManager();

const readPublicToggle = await screen.findByRole('switch', { name: /Allow public read/i });

await user.click(readPublicToggle);
expect(mutate).toHaveBeenCalledWith(
{
libraryId: 'lib-001',
updatedData: { allowPublicRead: !libraryData.allowPublicRead },
},
expect.objectContaining({
onSuccess: expect.any(Function),
}),
);
const { onSuccess } = (mutate as jest.Mock).mock.calls[0][1];
onSuccess?.();

expect(await screen.findByText(/updated successfully/i)).toBeInTheDocument();
});

it('should not render the toggle if the user can not manage team and the Library Public Read is disabled', () => {
(useLibrary as jest.Mock).mockReturnValue({ data: { ...libraryData, allowPublicRead: false } });
(useLibraryAuthZ as jest.Mock).mockReturnValue({ ...libraryAuthZContext, canManageTeam: false });

renderTeamManager();
expect(screen.queryByRole('switch', { name: /Allow public read/i })).not.toBeInTheDocument();
});

it('should render the toggle as disabled if the user can not manage team but the Library Public Read is enabled', async () => {
(useLibrary as jest.Mock).mockReturnValue({ data: { ...libraryData, allowPublicRead: true } });
(useLibraryAuthZ as jest.Mock).mockReturnValue({ ...libraryAuthZContext, canManageTeam: false });

renderTeamManager();

const readPublicToggle = await screen.findByRole('switch', { name: /Allow public read/i });

expect(readPublicToggle).toBeInTheDocument();
expect(readPublicToggle).toBeDisabled();
});

it('renders correct navigation link label and URL on breadcrumb', () => {
renderTeamManager();
const navLink = screen.getByRole('link', { name: 'Manage Access' });
Expand Down
3 changes: 1 addition & 2 deletions src/authz-module/libraries-manager/LibrariesTeamManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { AddNewTeamMemberTrigger } from './components/AddNewTeamMemberModal';
import { buildPermissionMatrixByResource, buildPermissionMatrixByRole } from './utils';

import messages from './messages';
import PublicReadToggle from './components/PublicReadToggle';

const LibrariesTeamManager = () => {
const intl = useIntl();
Expand Down Expand Up @@ -51,7 +50,7 @@ const LibrariesTeamManager = () => {
pageTitle={pageTitle}
pageSubtitle={libraryId}
actions={
[<PublicReadToggle libraryId={libraryId} canEditToggle={canManageTeam} key="allow-public-read" />,
[
...(canManageTeam ? [<AddNewTeamMemberTrigger libraryId={libraryId} key="add-new-member" />] : []),
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,8 @@ describe('LibrariesUserManager', () => {
expect(screen.getByText('Library Team Management')).toBeInTheDocument();
expect(screen.getByRole('listitem', { current: 'page' })).toHaveTextContent('testuser');
// Page title and subtitle
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('testuser');
expect(screen.getByRole('paragraph')).toHaveTextContent('testuser@example.com');
expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent('testuser');
expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent('testuser@example.com');

expect(screen.getByText('Admin')).toBeInTheDocument();
expect(screen.getByText('Instructor')).toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ const LibrariesUserManager = () => {
navLinks={[{ label: rootBreadcrumb, to: teamMembersPath }, { label: pageManageTitle, to: teamMembersPath }]}
activeLabel={user?.username || ''}
pageTitle={user?.username || ''}
pageSubtitle={<p>{user?.email}</p>}
pageSubtitle={user?.email || ''}
actions={user && canManageTeam
? [<AssignNewRoleTrigger
username={user.username}
Expand Down
Loading
Loading