+
onClick={open}
disabled={isPending}
>
- {intl.formatMessage(messages['authz.manage.assign.role.title'])}
+ {intl.formatMessage(baseMessages['authz.management.assign.role.title'])}
{isOpen && (
diff --git a/src/authz-module/libraries-manager/components/AddNewTeamMemberModal/messages.ts b/src/authz-module/libraries-manager/components/AddNewTeamMemberModal/messages.ts
index fb6c516f..05e28d5e 100644
--- a/src/authz-module/libraries-manager/components/AddNewTeamMemberModal/messages.ts
+++ b/src/authz-module/libraries-manager/components/AddNewTeamMemberModal/messages.ts
@@ -1,11 +1,6 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
- 'authz.manage.assign.role.title': {
- id: 'authz.manage.assign.role.title',
- defaultMessage: 'Assign Role',
- description: 'Text for the assign role button',
- },
'libraries.authz.manage.add.member.title': {
id: 'libraries.authz.manage.add.member.title',
defaultMessage: 'Add New Team Member',
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..c63f4832 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/authz-module/data/context/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..88023730 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/authz-module/data/context/ToastManagerContext';
import AssignNewRoleModal from './AssignNewRoleModal';
import messages from '../messages';
diff --git a/src/authz-module/libraries-manager/components/TeamTable/components/Cells.test.tsx b/src/authz-module/libraries-manager/components/TeamTable/components/Cells.test.tsx
index fe7c9404..ce627b32 100644
--- a/src/authz-module/libraries-manager/components/TeamTable/components/Cells.test.tsx
+++ b/src/authz-module/libraries-manager/components/TeamTable/components/Cells.test.tsx
@@ -23,7 +23,7 @@ jest.mock('@src/authz-module/data/hooks', () => ({
useTeamMembers: jest.fn(),
}));
-jest.mock('../hooks/useQuerySettings', () => ({
+jest.mock('@src/authz-module/hooks/useQuerySettings', () => ({
useQuerySettings: jest.fn(() => ({
querySettings: { page: 1, limit: 10 },
})),
diff --git a/src/authz-module/libraries-manager/components/TeamTable/components/Cells.tsx b/src/authz-module/libraries-manager/components/TeamTable/components/Cells.tsx
index 9f160994..9f0faa43 100644
--- a/src/authz-module/libraries-manager/components/TeamTable/components/Cells.tsx
+++ b/src/authz-module/libraries-manager/components/TeamTable/components/Cells.tsx
@@ -5,8 +5,8 @@ import { TableCellValue, TeamMember } from '@src/types';
import { useLibraryAuthZ } from '@src/authz-module/libraries-manager/context';
import { useNavigate } from 'react-router-dom';
import { useTeamMembers } from '@src/authz-module/data/hooks';
-import { SKELETON_ROWS } from '@src/authz-module/libraries-manager/constants';
-import { useQuerySettings } from '../hooks/useQuerySettings';
+import { SKELETON_ROWS } from '@src/authz-module/constants';
+import { useQuerySettings } from '@src/authz-module/hooks/useQuerySettings';
import messages from '../messages';
type CellProps = TableCellValue;
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 719edb87..3d809e58 100644
--- a/src/authz-module/libraries-manager/components/TeamTable/index.test.tsx
+++ b/src/authz-module/libraries-manager/components/TeamTable/index.test.tsx
@@ -3,8 +3,8 @@ 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 { CONTENT_LIBRARY_PERMISSIONS } from '@src/authz-module/libraries-manager/constants';
+import { ToastManagerProvider } from '@src/authz-module/data/context/ToastManagerContext';
+import { CONTENT_LIBRARY_PERMISSIONS } from '@src/authz-module/constants';
import TeamTable from './index';
const mockNavigate = jest.fn();
diff --git a/src/authz-module/libraries-manager/components/TeamTable/index.tsx b/src/authz-module/libraries-manager/components/TeamTable/index.tsx
index 3763bd8d..704fb1d6 100644
--- a/src/authz-module/libraries-manager/components/TeamTable/index.tsx
+++ b/src/authz-module/libraries-manager/components/TeamTable/index.tsx
@@ -10,9 +10,9 @@ 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 } from '@src/authz-module/libraries-manager/constants';
-import { useQuerySettings } from './hooks/useQuerySettings';
+import { useToastManager } from '@src/authz-module/data/context/ToastManagerContext';
+import { SKELETON_ROWS } from '@src/authz-module/constants';
+import { useQuerySettings } from '@src/authz-module/hooks/useQuerySettings';
import TableControlBar from './components/TableControlBar';
import messages from './messages';
import {
diff --git a/src/authz-module/libraries-manager/constants.ts b/src/authz-module/libraries-manager/constants.ts
deleted file mode 100644
index d1368961..00000000
--- a/src/authz-module/libraries-manager/constants.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { PermissionMetadata, ResourceMetadata, RoleMetadata } from 'types';
-
-export const CONTENT_LIBRARY_PERMISSIONS = {
- DELETE_LIBRARY: 'content_libraries.delete_library',
- MANAGE_LIBRARY_TAGS: 'content_libraries.manage_library_tags',
- VIEW_LIBRARY: 'content_libraries.view_library',
-
- EDIT_LIBRARY_CONTENT: 'content_libraries.edit_library_content',
- PUBLISH_LIBRARY_CONTENT: 'content_libraries.publish_library_content',
- REUSE_LIBRARY_CONTENT: 'content_libraries.reuse_library_content',
-
- CREATE_LIBRARY_COLLECTION: 'content_libraries.create_library_collection',
- EDIT_LIBRARY_COLLECTION: 'content_libraries.edit_library_collection',
- DELETE_LIBRARY_COLLECTION: 'content_libraries.delete_library_collection',
-
- MANAGE_LIBRARY_TEAM: 'content_libraries.manage_library_team',
- VIEW_LIBRARY_TEAM: 'content_libraries.view_library_team',
-};
-
-// Note: this information will eventually come from the backend API
-// but for the MVP we decided to manage it in the frontend
-export const libraryRolesMetadata: RoleMetadata[] = [
- { role: 'library_admin', name: 'Library Admin', description: 'The Library Admin has full control over the library, including managing users, modifying content, and handling publishing workflows. They ensure content is properly maintained and accessible as needed.' },
- { role: 'library_author', name: 'Library Author', description: 'The Library Author is responsible for creating, editing, and publishing content within a library. They can manage tags and collections but cannot delete libraries or manage users.' },
- { role: 'library_contributor', name: 'Library Contributor', description: 'The Library Contributor can create and edit content within a library but cannot publish it. They support the authoring process while leaving final publishing to Authors or Admins.' },
- { role: 'library_user', name: 'Library User', description: 'The Library User can view and reuse content but cannot edit or delete any resource.' },
-];
-
-export const libraryResourceTypes: ResourceMetadata[] = [
- { key: 'library', label: 'Library', description: 'Permissions related to the library as a whole.' },
- { key: 'library_content', label: 'Content', description: 'Permissions to create, edit, delete, and publish individual content items within the library.' },
- { key: 'library_collection', label: 'Collection', description: 'Permissions to create, edit, and delete content collections within the library.' },
- { key: 'library_team', label: 'Team', description: 'Permissions to manage user access and roles within the library.' },
-];
-
-export const libraryPermissions: PermissionMetadata[] = [
- { key: CONTENT_LIBRARY_PERMISSIONS.DELETE_LIBRARY, resource: 'library', description: 'Allows the user to delete the library and all its contents.' },
- { key: CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TAGS, resource: 'library', description: 'Add or remove tags from content.' },
- { key: CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY, resource: 'library', description: 'View content, search, filter, and sort within the library.' },
-
- { key: CONTENT_LIBRARY_PERMISSIONS.EDIT_LIBRARY_CONTENT, resource: 'library_content', description: 'Edit content in draft mode' },
- { key: CONTENT_LIBRARY_PERMISSIONS.PUBLISH_LIBRARY_CONTENT, resource: 'library_content', description: 'Publish content, making it available for reuse' },
- { key: CONTENT_LIBRARY_PERMISSIONS.REUSE_LIBRARY_CONTENT, resource: 'library_content', description: 'Reuse published content within a course.' },
-
- { key: CONTENT_LIBRARY_PERMISSIONS.CREATE_LIBRARY_COLLECTION, resource: 'library_collection', description: 'Create new collections within a library.' },
- { key: CONTENT_LIBRARY_PERMISSIONS.EDIT_LIBRARY_COLLECTION, resource: 'library_collection', description: 'Add or remove content from existing collections.' },
- { key: CONTENT_LIBRARY_PERMISSIONS.DELETE_LIBRARY_COLLECTION, resource: 'library_collection', description: 'Delete entire collections from the library.' },
-
- { key: CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TEAM, resource: 'library_team', description: 'View the list of users who have access to the library.' },
- { key: CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY_TEAM, resource: 'library_team', description: 'Add, remove, and assign roles to users within the library.' },
-];
-
-export const DEFAULT_TOAST_DELAY = 5000;
-export const RETRY_TOAST_DELAY = 120_000; // 2 minutes
-export const SKELETON_ROWS = Array.from({ length: 10 }).map(() => ({
- username: 'skeleton',
- name: '',
- email: '',
- roles: [],
-}));
diff --git a/src/authz-module/libraries-manager/context.test.tsx b/src/authz-module/libraries-manager/context.test.tsx
index 8025e6cf..d004b906 100644
--- a/src/authz-module/libraries-manager/context.test.tsx
+++ b/src/authz-module/libraries-manager/context.test.tsx
@@ -5,7 +5,7 @@ import { useValidateUserPermissions } from '@src/data/hooks';
import { renderWrapper } from '@src/setupTest';
import { usePermissionsByRole } from '@src/authz-module/data/hooks';
import { CustomErrors } from '@src/constants';
-import { CONTENT_LIBRARY_PERMISSIONS } from './constants';
+import { CONTENT_LIBRARY_PERMISSIONS } from '../constants';
import { LibraryAuthZProvider, useLibraryAuthZ } from './context';
jest.mock('react-router-dom', () => ({
diff --git a/src/authz-module/libraries-manager/context.tsx b/src/authz-module/libraries-manager/context.tsx
index 507e50b1..a6ad0035 100644
--- a/src/authz-module/libraries-manager/context.tsx
+++ b/src/authz-module/libraries-manager/context.tsx
@@ -9,7 +9,7 @@ import { PermissionMetadata, ResourceMetadata, Role } from 'types';
import { CustomErrors } from '@src/constants';
import {
CONTENT_LIBRARY_PERMISSIONS, libraryPermissions, libraryResourceTypes, libraryRolesMetadata,
-} from './constants';
+} from '../constants';
const LIBRARY_TEAM_PERMISSIONS = [
CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY_TEAM,
diff --git a/src/authz-module/libraries-manager/messages.ts b/src/authz-module/libraries-manager/messages.ts
index 9ed3f7d4..f298761d 100644
--- a/src/authz-module/libraries-manager/messages.ts
+++ b/src/authz-module/libraries-manager/messages.ts
@@ -36,28 +36,28 @@ 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',
+ 'authz.team.toast.500.error.message': {
+ id: 'authz.team.toast.500.error.message',
defaultMessage: 'We\'re experiencing technical difficulties.
Please try again later.',
- description: 'Libraries internal server error message',
+ description: 'Internal server error message',
},
- 'library.authz.team.toast.502.error.message': {
- id: 'library.authz.team.toast.502.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: 'Libraries bad getaway error message',
+ description: 'Bad gateway error message',
},
- 'library.authz.team.toast.503.error.message': {
- id: 'library.authz.team.toast.503.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: 'Libraries service temporary unabailable message',
+ description: 'Service temporarily unavailable message',
},
- 'library.authz.team.toast.408.error.message': {
- id: 'library.authz.team.toast.408.error.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: 'Libraries request timeout message',
+ description: 'Request timeout message',
},
- 'library.authz.team.toast.retry.label': {
- id: 'library.authz.team.toast.retry.label',
+ 'authz.team.toast.retry.label': {
+ id: 'authz.team.toast.retry.label',
defaultMessage: 'Retry',
description: 'Label for retry button.',
},
diff --git a/src/authz-module/messages.ts b/src/authz-module/messages.ts
new file mode 100644
index 00000000..3b3916f6
--- /dev/null
+++ b/src/authz-module/messages.ts
@@ -0,0 +1,53 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages(
+ {
+ 'authz.management.home.nav.link': {
+ id: 'authz.management.home.nav.link',
+ defaultMessage: 'Roles and Permissions Management',
+ description: 'Text for the roles and permissions management home page title 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.',
+ },
+ },
+);
+
+export default messages;
diff --git a/src/authz-module/roles-permissions/libraries/messages.ts b/src/authz-module/roles-permissions/libraries/messages.ts
index bada55fd..edbfae66 100644
--- a/src/authz-module/roles-permissions/libraries/messages.ts
+++ b/src/authz-module/roles-permissions/libraries/messages.ts
@@ -61,28 +61,28 @@ 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',
+ 'authz.team.toast.500.error.message': {
+ id: 'authz.team.toast.500.error.message',
defaultMessage: 'We\'re experiencing technical difficulties.
Please try again later.',
- description: 'Libraries internal server error message',
+ description: 'Internal server error message',
},
- 'library.authz.team.toast.502.error.message': {
- id: 'library.authz.team.toast.502.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: 'Libraries bad gateway error message',
+ description: 'Bad gateway error message',
},
- 'library.authz.team.toast.503.error.message': {
- id: 'library.authz.team.toast.503.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: 'Libraries service temporary unavailable message',
+ description: 'Service temporarily unavailable message',
},
- 'library.authz.team.toast.408.error.message': {
- id: 'library.authz.team.toast.408.error.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: 'Libraries request timeout message',
+ description: 'Request timeout message',
},
- 'library.authz.team.toast.retry.label': {
- id: 'library.authz.team.toast.retry.label',
+ 'authz.team.toast.retry.label': {
+ id: 'authz.team.toast.retry.label',
defaultMessage: 'Retry',
description: 'Label for retry button.',
},
diff --git a/src/data/api.ts b/src/data/api.ts
index b3d39531..c9289884 100644
--- a/src/data/api.ts
+++ b/src/data/api.ts
@@ -1,6 +1,8 @@
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { PermissionValidationRequest, PermissionValidationResponse } from '@src/types';
+import { camelCaseObject } from '@edx/frontend-platform';
import { getApiUrl } from './utils';
+import { UserAccount } from './types';
export const validateUserPermissions = async (
validations: PermissionValidationRequest[],
@@ -8,3 +10,9 @@ export const validateUserPermissions = async (
const { data } = await getAuthenticatedHttpClient().post(getApiUrl('/api/authz/v1/permissions/validate/me'), validations);
return data;
};
+
+export const getUserAccount = async (username: string): Promise => {
+ const url = new URL(getApiUrl(`/api/user/v1/accounts/${username}`));
+ const { data } = await getAuthenticatedHttpClient().get(url);
+ return camelCaseObject(data);
+};
diff --git a/src/data/hooks.test.tsx b/src/data/hooks.test.tsx
index 7dc20ec9..a2e284cc 100644
--- a/src/data/hooks.test.tsx
+++ b/src/data/hooks.test.tsx
@@ -2,7 +2,7 @@ import { act, ReactNode } from 'react';
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
-import { useValidateUserPermissions } from './hooks';
+import { useValidateUserPermissions, useUserAccount } from './hooks';
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedHttpClient: jest.fn(),
@@ -42,6 +42,46 @@ const mockInvalidPermissions = [
{ action: 'act:read', object: 'lib:test-lib', allowed: false },
];
+const mockUserAccountData = {
+ username: 'john.doe',
+ bio: 'Software Developer',
+ accountPrivacy: 'public',
+ country: 'US',
+ dateJoined: '2023-01-15T10:30:00Z',
+ levelOfEducation: 'bachelor',
+ timeZone: 'America/New_York',
+ profileImage: {
+ hasImage: true,
+ imageUrlFull: 'https://example.com/profile_full.jpg',
+ imageUrlLarge: 'https://example.com/profile_large.jpg',
+ imageUrlMedium: 'https://example.com/profile_medium.jpg',
+ imageUrlSmall: 'https://example.com/profile_small.jpg',
+ },
+ courseCertificates: null,
+ languageProficiencies: [],
+ socialLinks: [],
+};
+
+const mockEmptyUserData = {
+ username: 'jane.smith',
+ bio: null,
+ accountPrivacy: 'private',
+ country: null,
+ dateJoined: '2023-06-20T14:15:00Z',
+ levelOfEducation: null,
+ timeZone: null,
+ profileImage: {
+ hasImage: false,
+ imageUrlFull: '',
+ imageUrlLarge: '',
+ imageUrlMedium: '',
+ imageUrlSmall: '',
+ },
+ courseCertificates: null,
+ languageProficiencies: [],
+ socialLinks: [],
+};
+
describe('useValidateUserPermissions', () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -95,3 +135,102 @@ describe('useValidateUserPermissions', () => {
}
});
});
+
+describe('useUserAccount', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('fetches user account data successfully', async () => {
+ (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
+ get: jest.fn().mockResolvedValueOnce({ data: mockUserAccountData }),
+ });
+
+ const { result } = renderHook(() => useUserAccount('john.doe'), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+
+ expect(getAuthenticatedHttpClient).toHaveBeenCalled();
+ expect(result.current.data).toEqual(mockUserAccountData);
+ expect(result.current.data?.username).toBe('john.doe');
+ });
+
+ it('handles user account data with minimal information', async () => {
+ (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
+ get: jest.fn().mockResolvedValueOnce({ data: mockEmptyUserData }),
+ });
+
+ const { result } = renderHook(() => useUserAccount('jane.smith'), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+
+ expect(result.current.data?.username).toBe('jane.smith');
+ expect(result.current.data?.bio).toBeNull();
+ expect(result.current.data?.country).toBeNull();
+ });
+
+ it('handles API error gracefully', async () => {
+ const mockError = new Error('User not found');
+ (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
+ get: jest.fn().mockRejectedValueOnce(mockError),
+ });
+
+ const { result } = renderHook(() => useUserAccount('nonexistent.user'), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => expect(result.current.isError).toBe(true));
+
+ expect(result.current.error).toEqual(mockError);
+ expect(result.current.data).toBeUndefined();
+ });
+
+ it('does not refetch on window focus', async () => {
+ const mockGet = jest.fn().mockResolvedValueOnce({ data: mockUserAccountData });
+ (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
+ get: mockGet,
+ });
+
+ const { result } = renderHook(() => useUserAccount('john.doe'), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+
+ act(() => {
+ window.dispatchEvent(new Event('focus'));
+ });
+
+ expect(mockGet).toHaveBeenCalledTimes(1);
+ });
+
+ it('updates data when username changes', async () => {
+ const mockGet = jest.fn()
+ .mockResolvedValueOnce({ data: mockUserAccountData })
+ .mockResolvedValueOnce({ data: mockEmptyUserData });
+
+ (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
+ get: mockGet,
+ });
+
+ const { result, rerender } = renderHook(
+ ({ username }) => useUserAccount(username),
+ {
+ wrapper: createWrapper(),
+ initialProps: { username: 'john.doe' },
+ },
+ );
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+ expect(result.current.data?.username).toBe('john.doe');
+
+ rerender({ username: 'jane.smith' });
+
+ await waitFor(() => expect(result.current.data?.username).toBe('jane.smith'));
+ expect(mockGet).toHaveBeenCalledTimes(2);
+ });
+});
diff --git a/src/data/hooks.ts b/src/data/hooks.ts
index 80d4154e..833f1381 100644
--- a/src/data/hooks.ts
+++ b/src/data/hooks.ts
@@ -1,11 +1,12 @@
-import { useSuspenseQuery } from '@tanstack/react-query';
+import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
import { PermissionValidationRequest, PermissionValidationResponse } from '@src/types';
import { appId } from '@src/constants';
-import { validateUserPermissions } from './api';
+import { getUserAccount, validateUserPermissions } from './api';
const adminConsoleQueryKeys = {
all: [appId] as const,
permissions: (permissions: PermissionValidationRequest[]) => [...adminConsoleQueryKeys.all, 'validatePermissions', permissions] as const,
+ userAccount: (username: string) => [...adminConsoleQueryKeys.all, 'userAccount', username] as const,
};
/**
@@ -32,3 +33,11 @@ export const useValidateUserPermissions = (
queryFn: () => validateUserPermissions(permissions),
retry: false,
});
+
+export const useUserAccount = (username: string) => useQuery({
+ queryKey: adminConsoleQueryKeys.userAccount(username),
+ queryFn: async () => getUserAccount(username),
+ retry: false,
+ enabled: !!username,
+ refetchOnWindowFocus: false,
+});
diff --git a/src/data/types.ts b/src/data/types.ts
new file mode 100644
index 00000000..81c51c5c
--- /dev/null
+++ b/src/data/types.ts
@@ -0,0 +1,39 @@
+export type ProfileImage = {
+ has_image: boolean;
+ image_url_full: string;
+ image_url_large: string;
+ image_url_medium: string;
+ image_url_small: string;
+};
+
+export type UserAccount = {
+ account_privacy: string;
+ profile_image: ProfileImage;
+ username: string;
+ bio: string | null;
+ course_certificates: unknown | null; // Type unclear from data
+ country: string | null;
+ date_joined: string; // ISO date string
+ language_proficiencies: unknown[]; // Array type unclear from empty data
+ level_of_education: string | null;
+ social_links: unknown[]; // Array type unclear from empty data
+ time_zone: string | null;
+ name: string;
+ email: string;
+ id: number;
+ verified_name: string | null;
+ extended_profile: unknown[]; // Array type unclear from empty data
+ gender: string | null;
+ state: string | null;
+ goals: string;
+ is_active: boolean;
+ last_login: string; // ISO date string
+ mailing_address: string;
+ requires_parental_consent: boolean;
+ secondary_email: string | null;
+ secondary_email_enabled: boolean | null;
+ year_of_birth: number | null;
+ phone_number: string | null;
+ activation_key: string;
+ pending_name_change: string | null;
+};
diff --git a/src/index.tsx b/src/index.tsx
index cbd7cf93..7e5ee477 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -6,7 +6,7 @@ import { AppProvider, ErrorPage } from '@edx/frontend-platform/react';
import {
APP_INIT_ERROR, APP_READY, subscribe, initialize, mergeConfig,
} from '@edx/frontend-platform';
-import AuthZModule from 'authz-module';
+import AuthZModule from '@src/authz-module';
import messages from './i18n';
diff --git a/src/types.ts b/src/types.ts
index 27d78f9f..215ef265 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -29,7 +29,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;
@@ -49,6 +51,13 @@ export type PermissionMetadata = {
description?: string;
};
+export type UserRole = {
+ role: string;
+ org: string;
+ scope: string;
+ permissionCount: number;
+};
+
// Permissions Matrix
export type EnrichedPermission = PermissionMetadata & {