+
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/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/hooks/useQuerySettings.test.ts b/src/authz-module/libraries-manager/components/TeamTable/hooks/useQuerySettings.test.ts
deleted file mode 100644
index 6a9dc765..00000000
--- a/src/authz-module/libraries-manager/components/TeamTable/hooks/useQuerySettings.test.ts
+++ /dev/null
@@ -1,447 +0,0 @@
-import { renderHook, act } from '@testing-library/react';
-import { QuerySettings } from '@src/authz-module/data/api';
-import { useQuerySettings } from './useQuerySettings';
-
-describe('useQuerySettings', () => {
- const defaultQuerySettings: QuerySettings = {
- roles: null,
- search: null,
- pageSize: 10,
- pageIndex: 0,
- sortBy: null,
- order: null,
- };
-
- it('should initialize with default query settings when no initial settings provided', () => {
- const { result } = renderHook(() => useQuerySettings());
-
- expect(result.current.querySettings).toEqual(defaultQuerySettings);
- expect(typeof result.current.handleTableFetch).toBe('function');
- });
-
- it('should initialize with custom initial query settings', () => {
- const customInitialSettings: QuerySettings = {
- roles: 'admin,editor',
- search: 'test-user',
- pageSize: 20,
- pageIndex: 2,
- sortBy: 'username',
- order: 'asc',
- };
-
- const { result } = renderHook(() => useQuerySettings(customInitialSettings));
-
- expect(result.current.querySettings).toEqual(customInitialSettings);
- });
-
- it('should update query settings when handleTableFetch is called with new filters', () => {
- const { result } = renderHook(() => useQuerySettings());
-
- const tableFilters = {
- pageSize: 15,
- pageIndex: 1,
- sortBy: [{ id: 'username', desc: false }],
- filters: [
- { id: 'roles', value: ['admin', 'editor'] },
- { id: 'username', value: 'john' },
- ],
- };
-
- act(() => {
- result.current.handleTableFetch(tableFilters);
- });
-
- expect(result.current.querySettings).toEqual({
- roles: 'admin,editor',
- search: 'john',
- pageSize: 15,
- pageIndex: 1,
- sortBy: 'username',
- order: 'asc',
- });
- });
-
- it('should handle descending sort order by adding minus prefix', () => {
- const { result } = renderHook(() => useQuerySettings());
-
- const tableFilters = {
- pageSize: 10,
- pageIndex: 0,
- sortBy: [{ id: 'email', desc: true }],
- filters: [],
- };
-
- act(() => {
- result.current.handleTableFetch(tableFilters);
- });
-
- expect(result.current.querySettings.order).toBe('desc');
- });
-
- it('should convert camelCase sort field to snake_case', () => {
- const { result } = renderHook(() => useQuerySettings());
-
- const tableFilters = {
- pageSize: 10,
- pageIndex: 0,
- sortBy: [{ id: 'firstName', desc: false }],
- filters: [],
- };
-
- act(() => {
- result.current.handleTableFetch(tableFilters);
- });
-
- expect(result.current.querySettings.sortBy).toBe('first_name');
- });
-
- it('should convert camelCase sort field to snake_case with descending order', () => {
- const { result } = renderHook(() => useQuerySettings());
-
- const tableFilters = {
- pageSize: 10,
- pageIndex: 0,
- sortBy: [{ id: 'lastName', desc: true }],
- filters: [],
- };
-
- act(() => {
- result.current.handleTableFetch(tableFilters);
- });
-
- expect(result.current.querySettings.order).toBe('desc');
- });
-
- it('should handle empty filters by setting values to null', () => {
- const { result } = renderHook(() => useQuerySettings());
-
- const tableFilters = {
- pageSize: 10,
- pageIndex: 0,
- sortBy: [],
- filters: [],
- };
-
- act(() => {
- result.current.handleTableFetch(tableFilters);
- });
-
- expect(result.current.querySettings).toEqual({
- roles: null,
- search: null,
- pageSize: 10,
- pageIndex: 0,
- order: null,
- sortBy: null,
- });
- });
-
- it('should handle empty roles filter array by setting roles to null', () => {
- const { result } = renderHook(() => useQuerySettings());
-
- const tableFilters = {
- pageSize: 10,
- pageIndex: 0,
- sortBy: [],
- filters: [
- { id: 'roles', value: [] },
- { id: 'username', value: '' },
- ],
- };
-
- act(() => {
- result.current.handleTableFetch(tableFilters);
- });
-
- expect(result.current.querySettings).toEqual({
- roles: null,
- search: null,
- pageSize: 10,
- pageIndex: 0,
- order: null,
- sortBy: null,
- });
- });
-
- it('should handle missing filters by setting default values', () => {
- const { result } = renderHook(() => useQuerySettings());
-
- const tableFilters = {
- pageSize: 10,
- pageIndex: 0,
- sortBy: [],
- filters: [
- { id: 'roles', value: undefined },
- { id: 'username', value: undefined },
- ],
- };
-
- act(() => {
- result.current.handleTableFetch(tableFilters);
- });
-
- expect(result.current.querySettings).toEqual({
- roles: null,
- search: null,
- pageSize: 10,
- pageIndex: 0,
- order: null,
- sortBy: null,
- });
- });
-
- it('should use default pagination values when not provided', () => {
- const { result } = renderHook(() => useQuerySettings());
-
- const tableFilters = {
- sortBy: [],
- filters: [],
- } as any; // Missing pageSize and pageIndex
-
- act(() => {
- result.current.handleTableFetch(tableFilters);
- });
-
- expect(result.current.querySettings.pageSize).toBe(10);
- expect(result.current.querySettings.pageIndex).toBe(0);
- });
-
- it('should not update state if settings have not changed', () => {
- const { result } = renderHook(() => useQuerySettings());
-
- const tableFilters = {
- pageSize: 10,
- pageIndex: 0,
- sortBy: [],
- filters: [],
- };
-
- const initialSettings = result.current.querySettings;
-
- act(() => {
- result.current.handleTableFetch(tableFilters);
- });
-
- // Should be the same object reference since no changes occurred
- expect(result.current.querySettings).toBe(initialSettings);
- });
-
- it('should update state when settings have changed', () => {
- const { result } = renderHook(() => useQuerySettings());
-
- const initialSettings = result.current.querySettings;
-
- const tableFilters = {
- pageSize: 20, // Different from default
- pageIndex: 0,
- sortBy: [],
- filters: [],
- };
-
- act(() => {
- result.current.handleTableFetch(tableFilters);
- });
-
- // Should be a different object reference since pageSize changed
- expect(result.current.querySettings).not.toBe(initialSettings);
- expect(result.current.querySettings.pageSize).toBe(20);
- });
-
- it('should handle complex filter combinations', () => {
- const { result } = renderHook(() => useQuerySettings());
-
- const tableFilters = {
- pageSize: 25,
- pageIndex: 3,
- sortBy: [{ id: 'userRole', desc: true }],
- filters: [
- { id: 'roles', value: ['admin', 'editor', 'viewer'] },
- { id: 'username', value: 'test@example.com' },
- { id: 'otherFilter', value: 'ignored' }, // Should be ignored
- ],
- };
-
- act(() => {
- result.current.handleTableFetch(tableFilters);
- });
-
- expect(result.current.querySettings).toEqual({
- roles: 'admin,editor,viewer',
- search: 'test@example.com',
- pageSize: 25,
- pageIndex: 3,
- order: 'desc',
- sortBy: 'user_role',
- });
- });
-
- it('should handle multiple camelCase words in sort field', () => {
- const { result } = renderHook(() => useQuerySettings());
-
- const tableFilters = {
- pageSize: 10,
- pageIndex: 0,
- sortBy: [{ id: 'userFirstLastName', desc: false }],
- filters: [],
- };
-
- act(() => {
- result.current.handleTableFetch(tableFilters);
- });
-
- expect(result.current.querySettings.sortBy).toBe('user_first_last_name');
- });
-
- it('should preserve handleTableFetch function reference across renders', () => {
- const { result, rerender } = renderHook(() => useQuerySettings());
-
- const initialHandleTableFetch = result.current.handleTableFetch;
-
- rerender();
-
- expect(result.current.handleTableFetch).toBe(initialHandleTableFetch);
- });
-
- it('should handle whitespace-only search values as provided', () => {
- const { result } = renderHook(() => useQuerySettings());
-
- const tableFilters = {
- pageSize: 10,
- pageIndex: 0,
- sortBy: [],
- filters: [
- { id: 'username', value: ' ' }, // Whitespace only
- ],
- };
-
- act(() => {
- result.current.handleTableFetch(tableFilters);
- });
-
- expect(result.current.querySettings.search).toBe(' ');
- });
-
- it('should detect changes in roles filter', () => {
- const { result } = renderHook(() => useQuerySettings());
-
- // First set some roles
- act(() => {
- result.current.handleTableFetch({
- pageSize: 10,
- pageIndex: 0,
- sortBy: [],
- filters: [{ id: 'roles', value: ['admin'] }],
- });
- });
-
- const settingsAfterFirstUpdate = result.current.querySettings;
-
- // Then change roles
- act(() => {
- result.current.handleTableFetch({
- pageSize: 10,
- pageIndex: 0,
- sortBy: [],
- filters: [{ id: 'roles', value: ['editor'] }],
- });
- });
-
- expect(result.current.querySettings).not.toBe(settingsAfterFirstUpdate);
- expect(result.current.querySettings.roles).toBe('editor');
- });
-
- it('should detect changes in search filter', () => {
- const { result } = renderHook(() => useQuerySettings());
-
- // First set a search term
- act(() => {
- result.current.handleTableFetch({
- pageSize: 10,
- pageIndex: 0,
- sortBy: [],
- filters: [{ id: 'username', value: 'john' }],
- });
- });
-
- const settingsAfterFirstUpdate = result.current.querySettings;
-
- // Then change search term
- act(() => {
- result.current.handleTableFetch({
- pageSize: 10,
- pageIndex: 0,
- sortBy: [],
- filters: [{ id: 'username', value: 'jane' }],
- });
- });
-
- expect(result.current.querySettings).not.toBe(settingsAfterFirstUpdate);
- expect(result.current.querySettings.search).toBe('jane');
- });
-
- it('should detect changes in ordering', () => {
- const { result } = renderHook(() => useQuerySettings());
-
- // First set ordering
- act(() => {
- result.current.handleTableFetch({
- pageSize: 10,
- pageIndex: 0,
- sortBy: [{ id: 'username', desc: false }],
- filters: [],
- });
- });
-
- const settingsAfterFirstUpdate = result.current.querySettings;
-
- // Then change ordering
- act(() => {
- result.current.handleTableFetch({
- pageSize: 10,
- pageIndex: 0,
- sortBy: [{ id: 'email', desc: true }],
- filters: [],
- });
- });
-
- expect(result.current.querySettings).not.toBe(settingsAfterFirstUpdate);
- expect(result.current.querySettings.sortBy).toBe('email');
- expect(result.current.querySettings.order).toBe('desc');
- });
-
- it('should detect changes in pageSize', () => {
- const { result } = renderHook(() => useQuerySettings());
-
- const initialSettings = result.current.querySettings;
-
- act(() => {
- result.current.handleTableFetch({
- pageSize: 50,
- pageIndex: 0,
- sortBy: [],
- filters: [],
- });
- });
-
- expect(result.current.querySettings).not.toBe(initialSettings);
- expect(result.current.querySettings.pageSize).toBe(50);
- });
-
- it('should detect changes in pageIndex', () => {
- const { result } = renderHook(() => useQuerySettings());
-
- const initialSettings = result.current.querySettings;
-
- act(() => {
- result.current.handleTableFetch({
- pageSize: 10,
- pageIndex: 5,
- sortBy: [],
- filters: [],
- });
- });
-
- expect(result.current.querySettings).not.toBe(initialSettings);
- expect(result.current.querySettings.pageIndex).toBe(5);
- });
-});
diff --git a/src/authz-module/libraries-manager/components/TeamTable/hooks/useQuerySettings.ts b/src/authz-module/libraries-manager/components/TeamTable/hooks/useQuerySettings.ts
deleted file mode 100644
index 3c7d879f..00000000
--- a/src/authz-module/libraries-manager/components/TeamTable/hooks/useQuerySettings.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import { useCallback, useState } from 'react';
-import { QuerySettings } from '@src/authz-module/data/api';
-
-interface DataTableFilters {
- pageSize: number;
- pageIndex: number;
- sortBy: Array<{ id: string; desc: boolean }>;
- filters: Array<{ id: string; value: any }>;
-}
-
-interface UseQuerySettingsReturn {
- querySettings: QuerySettings;
- handleTableFetch: (tableFilters: DataTableFilters) => void;
-}
-
-enum SortOrderKeys {
- ASC = 'asc',
- DESC = 'desc',
-}
-
-/**
- * Custom hook to manage query settings for table data fetching
- * Converts DataTable filter/sort/pagination settings to API query parameters
- * and manages URL synchronization
- *
- * @param initialQuerySettings - Initial query settings
- * @returns Object containing querySettings and handleTableFetch function
- */
-export const useQuerySettings = (
- initialQuerySettings: QuerySettings = {
- roles: null,
- search: null,
- pageSize: 10,
- pageIndex: 0,
- order: null,
- sortBy: null,
- },
-): UseQuerySettingsReturn => {
- const [querySettings, setQuerySettings] = useState(initialQuerySettings);
-
- const handleTableFetch = useCallback((tableFilters: DataTableFilters) => {
- setQuerySettings((prevSettings) => {
- // Extract filters
- const rolesFilter = tableFilters.filters.find((filter) => filter.id === 'roles')?.value?.join(',') ?? '';
- const searchFilter = tableFilters.filters.find((filter) => filter.id === 'username')?.value ?? '';
-
- // Extract pagination
- const { pageSize = 10, pageIndex = 0 } = tableFilters;
-
- // Extract and convert sorting
- let sortByOption = '';
- let sortByOrder = '';
- if (tableFilters.sortBy.length) {
- sortByOption = tableFilters.sortBy[0].id.replace(/([A-Z])/g, '_$1').toLowerCase();
- sortByOrder = tableFilters.sortBy[0].desc ? SortOrderKeys.DESC : SortOrderKeys.ASC;
- }
-
- const newQuerySettings: QuerySettings = {
- roles: rolesFilter || null,
- search: searchFilter || null,
- sortBy: sortByOption || null,
- order: sortByOrder || null,
- pageSize,
- pageIndex,
- };
-
- const hasChanged = (
- prevSettings.roles !== newQuerySettings.roles
- || prevSettings.search !== newQuerySettings.search
- || prevSettings.pageSize !== newQuerySettings.pageSize
- || prevSettings.pageIndex !== newQuerySettings.pageIndex
- || prevSettings.sortBy !== newQuerySettings.sortBy
- || prevSettings.order !== newQuerySettings.order
- );
-
- if (!hasChanged) {
- return prevSettings; // No change, prevent unnecessary update
- }
-
- return newQuerySettings;
- });
- }, []);
-
- return {
- querySettings,
- handleTableFetch,
- };
-};
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..f1229274 100644
--- a/src/authz-module/libraries-manager/components/TeamTable/index.test.tsx
+++ b/src/authz-module/libraries-manager/components/TeamTable/index.test.tsx
@@ -4,7 +4,7 @@ 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 { 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 5b1bc893..77efa3ad 100644
--- a/src/authz-module/libraries-manager/components/TeamTable/index.tsx
+++ b/src/authz-module/libraries-manager/components/TeamTable/index.tsx
@@ -11,9 +11,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 } from '@src/authz-module/libraries-manager/constants';
-import { TABLE_DEFAULT_PAGE_SIZE } from '@src/authz-module/constants';
-import { useQuerySettings } from './hooks/useQuerySettings';
+import { SKELETON_ROWS, TABLE_DEFAULT_PAGE_SIZE } 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 787edfb9..00000000
--- a/src/authz-module/libraries-manager/constants.ts
+++ /dev/null
@@ -1,153 +0,0 @@
-import { PermissionMetadata, ResourceMetadata, RoleMetadata } from 'types';
-import {
- Group, CollectionsBookmark, Notes, AutoAwesomeMosaic,
-} from '@openedx/paragon/icons';
-
-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',
-
- CREATE_LIBRARY_CONTENT: 'content_libraries.create_library_content',
- EDIT_LIBRARY_CONTENT: 'content_libraries.edit_library_content',
- DELETE_LIBRARY_CONTENT: 'content_libraries.delete_library_content',
- PUBLISH_LIBRARY_CONTENT: 'content_libraries.publish_library_content',
- REUSE_LIBRARY_CONTENT: 'content_libraries.reuse_library_content',
- IMPORT_LIBRARY_CONTENT: 'content_libraries.import_library_content',
-
- MANAGE_LIBRARY_TEAM: 'content_libraries.manage_library_team',
- VIEW_LIBRARY_TEAM: 'content_libraries.view_library_team',
-
- CREATE_LIBRARY_COLLECTION: 'content_libraries.create_library_collection',
- EDIT_LIBRARY_COLLECTION: 'content_libraries.edit_library_collection',
- DELETE_LIBRARY_COLLECTION: 'content_libraries.delete_library_collection',
-};
-
-// 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.', icon: CollectionsBookmark,
- },
- {
- key: 'library_content', label: 'Content', description: 'Permissions to create, edit, delete, and publish individual content items within the library.', icon: Notes,
- },
- {
- key: 'library_team', label: 'Team', description: 'Permissions to manage user access and roles within the library.', icon: Group,
- },
- {
- key: 'library_collection', label: 'Collection', description: 'Permissions to create, edit, and delete content collections within the library.', icon: AutoAwesomeMosaic,
- },
-];
-
-export const libraryPermissions: PermissionMetadata[] = [
- { key: CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY, resource: 'library', description: 'View content, search, filter, and sort within the library.' },
- { key: CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TAGS, resource: 'library', description: 'Add or remove tags from content.' },
- { key: CONTENT_LIBRARY_PERMISSIONS.DELETE_LIBRARY, resource: 'library', description: 'Allows the user to delete the library and all its contents.' },
-
- { key: CONTENT_LIBRARY_PERMISSIONS.CREATE_LIBRARY_CONTENT, resource: 'library_content', description: 'Create content within the library.' },
- { key: CONTENT_LIBRARY_PERMISSIONS.EDIT_LIBRARY_CONTENT, resource: 'library_content', description: 'Edit content in draft mode' },
- { key: CONTENT_LIBRARY_PERMISSIONS.DELETE_LIBRARY_CONTENT, resource: 'library_content', description: 'Delete content within the library.' },
- { 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.IMPORT_LIBRARY_CONTENT, resource: 'library_content', description: 'Import content from courses.' },
-
- { key: CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY_TEAM, resource: 'library_team', description: 'View the list of users who have access to the library.' },
- { key: CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TEAM, resource: 'library_team', description: 'Add, remove, and assign roles to users within the library.' },
-
- { 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.' },
-];
-
-export const rolesLibraryObject = [
- {
- role: 'library_admin',
- permissions: [
- CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY,
- CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TAGS,
- CONTENT_LIBRARY_PERMISSIONS.DELETE_LIBRARY,
- CONTENT_LIBRARY_PERMISSIONS.EDIT_LIBRARY_CONTENT,
- CONTENT_LIBRARY_PERMISSIONS.PUBLISH_LIBRARY_CONTENT,
- CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY_TEAM,
- CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TEAM,
- CONTENT_LIBRARY_PERMISSIONS.CREATE_LIBRARY_COLLECTION,
- CONTENT_LIBRARY_PERMISSIONS.EDIT_LIBRARY_COLLECTION,
- CONTENT_LIBRARY_PERMISSIONS.DELETE_LIBRARY_COLLECTION,
- CONTENT_LIBRARY_PERMISSIONS.CREATE_LIBRARY_CONTENT,
- CONTENT_LIBRARY_PERMISSIONS.DELETE_LIBRARY_CONTENT,
- CONTENT_LIBRARY_PERMISSIONS.REUSE_LIBRARY_CONTENT,
- CONTENT_LIBRARY_PERMISSIONS.IMPORT_LIBRARY_CONTENT,
- ],
- userCount: 1,
- 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',
- permissions: [
- CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY,
- CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TAGS,
- CONTENT_LIBRARY_PERMISSIONS.EDIT_LIBRARY_CONTENT,
- CONTENT_LIBRARY_PERMISSIONS.PUBLISH_LIBRARY_CONTENT,
- CONTENT_LIBRARY_PERMISSIONS.REUSE_LIBRARY_CONTENT,
- CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY_TEAM,
- CONTENT_LIBRARY_PERMISSIONS.CREATE_LIBRARY_COLLECTION,
- CONTENT_LIBRARY_PERMISSIONS.EDIT_LIBRARY_COLLECTION,
- CONTENT_LIBRARY_PERMISSIONS.DELETE_LIBRARY_COLLECTION,
- CONTENT_LIBRARY_PERMISSIONS.CREATE_LIBRARY_CONTENT,
- CONTENT_LIBRARY_PERMISSIONS.DELETE_LIBRARY_CONTENT,
- CONTENT_LIBRARY_PERMISSIONS.IMPORT_LIBRARY_CONTENT,
- ],
- userCount: 1,
- 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',
- permissions: [
- CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY,
- CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TAGS,
- CONTENT_LIBRARY_PERMISSIONS.EDIT_LIBRARY_CONTENT,
- CONTENT_LIBRARY_PERMISSIONS.REUSE_LIBRARY_CONTENT,
- CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY_TEAM,
- CONTENT_LIBRARY_PERMISSIONS.CREATE_LIBRARY_COLLECTION,
- CONTENT_LIBRARY_PERMISSIONS.EDIT_LIBRARY_COLLECTION,
- CONTENT_LIBRARY_PERMISSIONS.DELETE_LIBRARY_COLLECTION,
- CONTENT_LIBRARY_PERMISSIONS.CREATE_LIBRARY_CONTENT,
- CONTENT_LIBRARY_PERMISSIONS.DELETE_LIBRARY_CONTENT,
- CONTENT_LIBRARY_PERMISSIONS.IMPORT_LIBRARY_CONTENT,
-
- ],
- userCount: 1,
- 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',
- permissions: [
- CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY,
- CONTENT_LIBRARY_PERMISSIONS.REUSE_LIBRARY_CONTENT,
- CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY_TEAM,
- ],
- userCount: 1,
- name: 'Library User',
- description: 'The Library User can view and reuse content but cannot edit or delete anything.',
- },
-];
-
-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/messages.ts b/src/authz-module/messages.ts
index 88229f8c..48d0a3c2 100644
--- a/src/authz-module/messages.ts
+++ b/src/authz-module/messages.ts
@@ -2,6 +2,16 @@ 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.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',
diff --git a/src/data/api.ts b/src/data/api.ts
index b3d39531..447ad72d 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..b6587c48 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..57429147
--- /dev/null
+++ b/src/data/types.ts
@@ -0,0 +1,39 @@
+export type ProfileImage = {
+ hasImage: boolean;
+ imageUrlFull: string;
+ imageUrlLarge: string;
+ imageUrlMedium: string;
+ imageUrlSmall: string;
+};
+
+export type UserAccount = {
+ accountPrivacy: string;
+ profileImage: ProfileImage;
+ username: string;
+ bio: string | null;
+ courseCertificates: unknown | null; // Type unclear from data
+ country: string | null;
+ dateJoined: string; // ISO date string
+ languageProficiencies: unknown[]; // Array type unclear from empty data
+ levelOfEducation: string | null;
+ socialLinks: unknown[]; // Array type unclear from empty data
+ timeZone: string | null;
+ name: string;
+ email: string;
+ id: number;
+ verifiedName: string | null;
+ extendedProfile: unknown[]; // Array type unclear from empty data
+ gender: string | null;
+ state: string | null;
+ goals: string;
+ isActive: boolean;
+ lastLogin: string; // ISO date string
+ mailingAddress: string;
+ requiresParentalConsent: boolean;
+ secondaryEmail: string | null;
+ secondaryEmailEnabled: boolean | null;
+ yearOfBirth: number | null;
+ phoneNumber: string | null;
+ activationKey: string;
+ pendingNameChange: 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';