From 3ccc8090e6c934af31824a8dd8844cfecdb75dc1 Mon Sep 17 00:00:00 2001 From: Diana Olarte Date: Sun, 19 Apr 2026 14:48:05 +1000 Subject: [PATCH] fix: update type declaration and validation --- .github/workflows/ci.yml | 2 + src/authz-module/audit-user/index.tsx | 4 +- .../components/AuthZTitle.test.tsx | 4 +- .../components/PermissionTable.tsx | 4 +- .../components/ResourceTooltip.tsx | 2 +- .../components/RoleCard/index.test.tsx | 5 +- .../components/RoleCard/index.tsx | 3 +- src/authz-module/components/TableCells.tsx | 11 +++-- .../TableControlBar/MultipleChoiceFilter.tsx | 4 +- .../TableControlBar/SearchFilter.tsx | 4 +- .../TableControlBar/TableControlBar.test.tsx | 2 +- .../TableControlBar/TableControlBar.tsx | 47 ++++++++++--------- .../components/TableFooter/TableFooter.tsx | 8 ++-- src/authz-module/components/messages.ts | 17 +------ src/authz-module/constants.ts | 2 +- src/authz-module/data/hooks.test.tsx | 27 ++++++----- src/authz-module/hooks/useQuerySettings.tsx | 10 ++-- .../libraries-manager/ErrorPage/index.tsx | 2 +- .../LibrariesTeamManager.tsx | 2 +- .../LibrariesUserManager.tsx | 6 ++- .../libraries-manager/ToastManagerContext.tsx | 22 +++++---- .../AddNewTeamMemberModal.tsx | 6 +-- .../AddNewTeamMemberTrigger.test.tsx | 3 +- .../AddNewTeamMemberTrigger.tsx | 2 +- .../AssignNewRoleModal.test.tsx | 2 +- .../AssignNewRoleModal/AssignNewRoleModal.tsx | 2 +- .../TeamTable/components/Cells.test.tsx | 6 +++ .../components/TeamTable/components/Cells.tsx | 15 +++--- .../components/MultipleChoiceFilter.tsx | 2 +- .../components/SearchFilter.test.tsx | 2 +- .../TeamTable/components/SearchFilter.tsx | 6 +-- .../TeamTable/components/SortDropdown.tsx | 4 +- .../components/TableControlBar.test.tsx | 4 +- .../TeamTable/components/TableControlBar.tsx | 13 ++--- .../libraries-manager/context.tsx | 15 ++---- .../libraries-manager/utils.test.ts | 3 +- src/authz-module/libraries-manager/utils.ts | 4 +- .../roles-permissions/courses/constants.ts | 2 +- .../roles-permissions/libraries/constants.ts | 2 +- .../roles-permissions/libraries/utils.test.ts | 3 +- .../roles-permissions/libraries/utils.ts | 2 +- .../team-members/TeamMembersTable.test.tsx | 11 +++-- src/data/hooks.test.tsx | 6 +-- src/global.d.ts | 10 ++++ src/index.tsx | 7 +-- src/paragon.d.ts | 35 ++++++++++++++ src/setupTest.tsx | 10 ++-- src/types.ts | 16 +------ tsconfig.json | 16 +++---- 49 files changed, 221 insertions(+), 176 deletions(-) create mode 100644 src/global.d.ts create mode 100644 src/paragon.d.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2532a00..21c1cfd0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,8 @@ jobs: run: npm ci - name: Lint run: npm run lint + - name: Types + run: npm run types - name: Test run: npm run test - name: Build diff --git a/src/authz-module/audit-user/index.tsx b/src/authz-module/audit-user/index.tsx index 042d5b3b..ae3d3379 100644 --- a/src/authz-module/audit-user/index.tsx +++ b/src/authz-module/audit-user/index.tsx @@ -32,8 +32,8 @@ const AuditUserPage = () => { useEffect(() => { if (!user && !isLoadingUser) { - // @ts-ignore - if (!isErrorUser || errorUser?.customAttributes?.httpErrorStatus === 404) { + const err = errorUser as { customAttributes?: { httpErrorStatus?: number } } | null; + if (!isErrorUser || err?.customAttributes?.httpErrorStatus === 404) { navigate(AUTHZ_HOME_PATH); } } diff --git a/src/authz-module/components/AuthZTitle.test.tsx b/src/authz-module/components/AuthZTitle.test.tsx index b67656fe..58a02dc1 100644 --- a/src/authz-module/components/AuthZTitle.test.tsx +++ b/src/authz-module/components/AuthZTitle.test.tsx @@ -17,7 +17,7 @@ describe('AuthZTitle', () => { it('renders without optional fields', () => { render(); - expect(screen.getByText(defaultProps.activeLabel)).toBeInTheDocument(); + expect(screen.getByText(defaultProps.activeLabel as string)).toBeInTheDocument(); expect(screen.getByText(defaultProps.pageTitle)).toBeInTheDocument(); expect(screen.getByText(defaultProps.pageSubtitle as string)).toBeInTheDocument(); }); @@ -35,7 +35,7 @@ describe('AuthZTitle', () => { expect(screen.getByText(label)).toHaveAttribute('href', expect.stringContaining(to)); }); - expect(screen.getByText(defaultProps.activeLabel)).toBeInTheDocument(); + expect(screen.getByText(defaultProps.activeLabel as string)).toBeInTheDocument(); }); it('renders page title', () => { diff --git a/src/authz-module/components/PermissionTable.tsx b/src/authz-module/components/PermissionTable.tsx index fd5e75c6..f33c403a 100644 --- a/src/authz-module/components/PermissionTable.tsx +++ b/src/authz-module/components/PermissionTable.tsx @@ -5,7 +5,7 @@ import { Card, Icon, OverlayTrigger, Tooltip, } from '@openedx/paragon'; import { PermissionsResourceGrouped, Role } from '@src/types'; -import { actionsDictionary } from './RoleCard/constants'; +import { actionsDictionary, ActionKey } from './RoleCard/constants'; import ResourceTooltip from './ResourceTooltip'; import messages from './messages'; @@ -66,7 +66,7 @@ const PermissionTable = ({ permissionsTable, roles, title }: PermissionTableProp {resourceGroup.permissions.map(permission => ( - + {permission.label} {roles.map(role => ( diff --git a/src/authz-module/components/ResourceTooltip.tsx b/src/authz-module/components/ResourceTooltip.tsx index 10fa4d2c..2ec3e500 100644 --- a/src/authz-module/components/ResourceTooltip.tsx +++ b/src/authz-module/components/ResourceTooltip.tsx @@ -17,7 +17,7 @@ const ResourceTooltip = ({ resourceGroup }:ResourceTooltipProps) => (

{resourceGroup.description}

    {resourceGroup.permissions.map(permission => ( -
  • {permission.label.trim()}: {permission.description}
  • +
  • {permission.label?.trim()}: {permission.description}
  • ))}
diff --git a/src/authz-module/components/RoleCard/index.test.tsx b/src/authz-module/components/RoleCard/index.test.tsx index 5508b9cd..5faccfb9 100644 --- a/src/authz-module/components/RoleCard/index.test.tsx +++ b/src/authz-module/components/RoleCard/index.test.tsx @@ -26,12 +26,13 @@ describe('RoleCard', () => { { key: 'library', label: 'Library Resource', + description: 'Library resource description', permissions: [ { - key: 'view', label: 'View', actionKey: 'view', disabled: false, + key: 'view', resource: 'library', label: 'View', actionKey: 'view', disabled: false, }, { - key: 'manage', label: 'Manage', actionKey: 'manage', disabled: true, + key: 'manage', resource: 'library', label: 'Manage', actionKey: 'manage', disabled: true, }, ], }, diff --git a/src/authz-module/components/RoleCard/index.tsx b/src/authz-module/components/RoleCard/index.tsx index 59b3ac20..f077081a 100644 --- a/src/authz-module/components/RoleCard/index.tsx +++ b/src/authz-module/components/RoleCard/index.tsx @@ -3,6 +3,7 @@ import { Card, Collapsible, Container, Icon, IconButton, } from '@openedx/paragon'; import { Delete, Person } from '@openedx/paragon/icons'; +import { RoleResourceGroup } from '@src/types'; import PermissionRow from './PermissionsRow'; import messages from './messages'; @@ -15,7 +16,7 @@ interface RoleCardProps extends CardTitleProps { objectName?: string | null; description: string; handleDelete?: () => void; - permissionsByResource: any[]; + permissionsByResource: RoleResourceGroup[]; } const CardTitle = ({ title, userCounter = null }: CardTitleProps) => { diff --git a/src/authz-module/components/TableCells.tsx b/src/authz-module/components/TableCells.tsx index d15cb78f..ca9fcad7 100644 --- a/src/authz-module/components/TableCells.tsx +++ b/src/authz-module/components/TableCells.tsx @@ -1,3 +1,5 @@ +import { ContextType, useContext, useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Icon, IconButton } from '@openedx/paragon'; import { AppContext } from '@edx/frontend-platform/react'; @@ -5,9 +7,8 @@ import { RemoveRedEye, Delete, ExpandMore, } from '@openedx/paragon/icons'; -import { TableCellValue, AppContextType, UserRole } from '@src/types'; -import { useNavigate } from 'react-router-dom'; -import { useContext, useMemo } from 'react'; +import { UserRole } from '@src/types'; +import type { TableCellValue } from '@src/paragon'; import { DJANGO_MANAGED_ROLES, MAP_ROLE_KEY_TO_LABEL } from '@src/authz-module/constants'; import { RESOURCE_ICONS } from './constants'; import messages from './messages'; @@ -25,7 +26,7 @@ type ExtendedCellProps = CellPropsWithValue & { const NameCell = ({ row }: CellProps) => { const intl = useIntl(); - const { authenticatedUser } = useContext(AppContext) as AppContextType; + const { authenticatedUser } = useContext(AppContext) as ContextType; const username = authenticatedUser?.username; if (row.original.username === username) { @@ -36,7 +37,7 @@ const NameCell = ({ row }: CellProps) => { ); } - return row.original.fullName || row.original.username; + return {row.original.fullName || row.original.username}; }; const ViewActionCell = ({ row }: CellProps) => { diff --git a/src/authz-module/components/TableControlBar/MultipleChoiceFilter.tsx b/src/authz-module/components/TableControlBar/MultipleChoiceFilter.tsx index cb77b67c..82454294 100644 --- a/src/authz-module/components/TableControlBar/MultipleChoiceFilter.tsx +++ b/src/authz-module/components/TableControlBar/MultipleChoiceFilter.tsx @@ -22,7 +22,7 @@ const MultipleChoiceFilter = ({ const { formatMessage } = useIntl(); const checkedBoxes = filterValue || []; - const handleClickCheckbox = (value, displayName) => { + const handleClickCheckbox = (value: string, displayName: string) => { const newValue = { groupName: filterButtonText?.toLocaleLowerCase() || '', value, @@ -71,7 +71,7 @@ const MultipleChoiceFilter = ({ type="text" trailingElement={} placeholder={formatMessage(messages['authz.table.controlbar.search'])} - onChange={(e) => { + onChange={(e: React.ChangeEvent) => { setSearchValue(e.target.value); onSearchChange?.(e.target.value); }} diff --git a/src/authz-module/components/TableControlBar/SearchFilter.tsx b/src/authz-module/components/TableControlBar/SearchFilter.tsx index b2480f1d..4baa77bb 100644 --- a/src/authz-module/components/TableControlBar/SearchFilter.tsx +++ b/src/authz-module/components/TableControlBar/SearchFilter.tsx @@ -6,7 +6,7 @@ import { Search } from '@openedx/paragon/icons'; interface SearchFilterProps { filterValue: string; - setFilter: (value: string) => void; + setFilter: (value: string | undefined) => void; placeholder: string; } @@ -19,7 +19,7 @@ const SearchFilter = ({ trailingElement={} value={filterValue || ''} type="text" - onChange={e => { + onChange={(e: React.ChangeEvent) => { setFilter(e.target.value || undefined); // Set undefined to remove the filter entirely }} placeholder={placeholder} diff --git a/src/authz-module/components/TableControlBar/TableControlBar.test.tsx b/src/authz-module/components/TableControlBar/TableControlBar.test.tsx index a706e15e..5200c913 100644 --- a/src/authz-module/components/TableControlBar/TableControlBar.test.tsx +++ b/src/authz-module/components/TableControlBar/TableControlBar.test.tsx @@ -48,7 +48,7 @@ describe('TableControlBar', () => { state: mockState, }; - const renderWithContext = (component, contextOverride = {}) => { + const renderWithContext = (component: React.ReactNode, contextOverride = {}) => { const context = { ...mockDataTableContext, ...contextOverride }; return renderWrapper( diff --git a/src/authz-module/components/TableControlBar/TableControlBar.tsx b/src/authz-module/components/TableControlBar/TableControlBar.tsx index 3d937c0b..c0e004e3 100644 --- a/src/authz-module/components/TableControlBar/TableControlBar.tsx +++ b/src/authz-module/components/TableControlBar/TableControlBar.tsx @@ -1,4 +1,6 @@ -import { useContext, useEffect, useState } from 'react'; +import { + Context, useContext, useEffect, useState, +} from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { DataTableContext, @@ -13,6 +15,7 @@ import { Business, Close, LocationOn, Person, Warning, } from '@openedx/paragon/icons'; +import type { DataTableContextShape } from '@src/paragon'; import { MAX_TABLE_FILTERS_APPLIED } from '@src/authz-module/constants'; import MultipleChoiceFilter from './MultipleChoiceFilter'; @@ -48,8 +51,7 @@ const TableControlBar = ({ onFilterChange }: TableControlBarProps) => { columns, setAllFilters, state, - // @ts-ignore-next-line - Paragon's DataTableContext is not typed - } = useContext(DataTableContext); + } = useContext(DataTableContext as unknown as Context); useEffect(() => { if (state.filters.length > 0) { @@ -78,20 +80,23 @@ const TableControlBar = ({ onFilterChange }: TableControlBarProps) => { .map((column) => column.Header); const getSearchPlaceholder = () => intl.formatMessage(messages['authz.table.controlbar.search.by.fields'], { - firstField: columnTextFilterHeaders[0] || '', - secondField: columnTextFilterHeaders[1] || '', + firstField: String(columnTextFilterHeaders[0] || ''), + secondField: String(columnTextFilterHeaders[1] || ''), }); - const handleCloseFilter = (filterName, filterValue) => { - const actualFilterId = FILTER_GROUP_TO_ID[filterName] || filterName; + const handleCloseFilter = (filterName: string, filterValue: string) => { + const actualFilterId = FILTER_GROUP_TO_ID[filterName as keyof typeof FILTER_GROUP_TO_ID] || filterName; const filterGroup = state.filters.find((filter) => filter.id === actualFilterId); - const newFilterValue = filterGroup?.value.filter(item => item !== filterValue) || []; - setAllFilters(state.filters.map(item => ( + const newFilterValue = filterGroup?.value.filter((item: string) => item !== filterValue) || []; + setAllFilters(state.filters.map((item) => ( item.id !== actualFilterId ? item : { id: item.id, value: newFilterValue }))); setChronologicalFilters((prevFilters) => prevFilters.filter((filter) => filter.value !== filterValue)); }; - const handleSetFilters = (setFilter) => (allFilters: string[], newFilter: FilterChoice) => { + const handleSetFilters = (setFilter: (value: string[]) => void) => ( + allFilters: string[], + newFilter: FilterChoice, + ) => { setFilter(allFilters); setChronologicalFilters((prevFilters) => { if (!prevFilters.find((filter) => filter.value === newFilter.value)) { @@ -113,8 +118,8 @@ const TableControlBar = ({ onFilterChange }: TableControlBarProps) => { if (Filter === RolesFilter) { return ( )} + setFilter={handleSetFilters(column.setFilter!)} disabled={filtersLimitReached} /> ); @@ -122,8 +127,8 @@ const TableControlBar = ({ onFilterChange }: TableControlBarProps) => { if (Filter === OrgFilter) { return ( )} + setFilter={handleSetFilters(column.setFilter!)} disabled={filtersLimitReached} /> ); @@ -131,8 +136,8 @@ const TableControlBar = ({ onFilterChange }: TableControlBarProps) => { if (Filter === MultipleChoiceFilter) { return ( )} + setFilter={handleSetFilters(column.setFilter!)} disabled={filtersLimitReached} /> ); @@ -140,8 +145,8 @@ const TableControlBar = ({ onFilterChange }: TableControlBarProps) => { if (Filter === ScopesFilter) { return ( )} + setFilter={handleSetFilters(column.setFilter!)} disabled={filtersLimitReached} filterValue={state.filters.find(filter => filter.id === 'scope')?.value || null} /> @@ -153,7 +158,7 @@ const TableControlBar = ({ onFilterChange }: TableControlBarProps) => { ); @@ -169,9 +174,9 @@ const TableControlBar = ({ onFilterChange }: TableControlBarProps) => { {chronologicalFilters.map((filter) => ( handleCloseFilter(filter.groupName, filter.value)} + onIconAfterClick={() => handleCloseFilter(filter.groupName || '', filter.value)} > {filter.displayName} diff --git a/src/authz-module/components/TableFooter/TableFooter.tsx b/src/authz-module/components/TableFooter/TableFooter.tsx index 26b3a974..9570686c 100644 --- a/src/authz-module/components/TableFooter/TableFooter.tsx +++ b/src/authz-module/components/TableFooter/TableFooter.tsx @@ -1,4 +1,5 @@ -import React, { useContext } from 'react'; +import { Context, useContext } from 'react'; +import { DataTableContextShape } from '@src/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; import { DataTableContext, Pagination, TableFooter } from '@openedx/paragon'; import messages from '../messages'; @@ -7,8 +8,7 @@ const Footer = () => { const { formatMessage } = useIntl(); const { pageCount, gotoPage, state, itemCount, rows, - // @ts-ignore-next-line - Paragon's DataTableContext is not typed - } = useContext(DataTableContext); + } = useContext(DataTableContext as unknown as Context); const { pageIndex } = state; return ( @@ -19,7 +19,7 @@ const Footer = () => { variant="reduced" currentPage={pageIndex + 1} pageCount={pageCount} - onPageSelect={(pageNum) => gotoPage(pageNum - 1)} + onPageSelect={(pageNum: number) => gotoPage(pageNum - 1)} /> ); diff --git a/src/authz-module/components/messages.ts b/src/authz-module/components/messages.ts index 002d795f..97028948 100644 --- a/src/authz-module/components/messages.ts +++ b/src/authz-module/components/messages.ts @@ -64,7 +64,7 @@ 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.table.username.current': { id: 'authz.table.username.current', @@ -97,21 +97,6 @@ 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}}', diff --git a/src/authz-module/constants.ts b/src/authz-module/constants.ts index 997b1366..e18dd5dc 100644 --- a/src/authz-module/constants.ts +++ b/src/authz-module/constants.ts @@ -1,4 +1,4 @@ -import { PermissionMetadata, ResourceMetadata, RoleMetadata } from 'types'; +import { PermissionMetadata, ResourceMetadata, RoleMetadata } from '@src/types'; export const CONTENT_LIBRARY_PERMISSIONS = { DELETE_LIBRARY: 'content_libraries.delete_library', diff --git a/src/authz-module/data/hooks.test.tsx b/src/authz-module/data/hooks.test.tsx index 0736be91..c0da665c 100644 --- a/src/authz-module/data/hooks.test.tsx +++ b/src/authz-module/data/hooks.test.tsx @@ -2,6 +2,7 @@ import { ReactNode } from 'react'; import { act, renderHook, waitFor } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { QuerySettings } from './api'; import { useLibrary, usePermissionsByRole, @@ -107,8 +108,10 @@ const mockScopes = { ], }; -const mockQuerySettings = { +const mockQuerySettings: QuerySettings = { roles: null, + scopes: null, + organizations: null, search: null, order: null, sortBy: null, @@ -188,7 +191,7 @@ describe('useTeamMembers', () => { }); it('returns data when API call succeeds', async () => { - getAuthenticatedHttpClient.mockReturnValue({ + (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ get: jest.fn().mockResolvedValue({ data: mockMembers }), }); @@ -203,7 +206,7 @@ describe('useTeamMembers', () => { }); it('handles error when API call fails', async () => { - getAuthenticatedHttpClient.mockReturnValue({ + (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ get: jest.fn().mockRejectedValue(new Error('API failure')), }); @@ -225,7 +228,7 @@ describe('useLibrary', () => { }); it('returns metadata on success', async () => { - getAuthenticatedHttpClient.mockReturnValue({ + (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ get: jest.fn().mockResolvedValueOnce({ data: mockLibrary }), }); @@ -240,7 +243,7 @@ describe('useLibrary', () => { }); it('throws on error', () => { - getAuthenticatedHttpClient.mockReturnValue({ + (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ get: jest.fn().mockRejectedValue(new Error('Not found')), }); @@ -264,7 +267,7 @@ describe('usePermissionsByRole', () => { { role: 'user', permissions: ['perm2'], userCount: 2 }, ]; - getAuthenticatedHttpClient.mockReturnValue({ + (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ get: jest.fn().mockResolvedValue({ data: { results: mockRoles } }), }); @@ -276,7 +279,7 @@ describe('usePermissionsByRole', () => { }); it('returns error if getRoles fails', async () => { - getAuthenticatedHttpClient.mockReturnValue({ + (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ get: jest.fn().mockRejectedValue(new Error('Not found')), }); const wrapper = createWrapper(); @@ -309,7 +312,7 @@ describe('usePermissionsByRole', () => { errors: [], }; - getAuthenticatedHttpClient.mockReturnValue({ + (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ put: jest.fn().mockResolvedValue({ data: mockResponse }), }); @@ -334,7 +337,7 @@ describe('usePermissionsByRole', () => { }); it('handles error when adding team members fails', async () => { - getAuthenticatedHttpClient.mockReturnValue({ + (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ put: jest.fn().mockRejectedValue(new Error('Failed to add members')), }); @@ -376,7 +379,7 @@ describe('useRevokeUserRoles', () => { errors: [], }; - getAuthenticatedHttpClient.mockReturnValue({ + (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ delete: jest.fn().mockResolvedValue({ data: mockResponse }), }); @@ -401,7 +404,7 @@ describe('useRevokeUserRoles', () => { }); it('handles error when revoking roles fails', async () => { - getAuthenticatedHttpClient.mockReturnValue({ + (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ delete: jest.fn().mockRejectedValue(new Error('Failed to revoke roles')), }); @@ -430,7 +433,7 @@ describe('useRevokeUserRoles', () => { data: { completed: [], errors: [] }, }); - getAuthenticatedHttpClient.mockReturnValue({ + (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ delete: mockDelete, }); diff --git a/src/authz-module/hooks/useQuerySettings.tsx b/src/authz-module/hooks/useQuerySettings.tsx index 33675c82..d07829bb 100644 --- a/src/authz-module/hooks/useQuerySettings.tsx +++ b/src/authz-module/hooks/useQuerySettings.tsx @@ -1,12 +1,10 @@ import { useCallback, useState } from 'react'; import { QuerySettings } from '@src/authz-module/data/api'; +import type { DataTableContextShape } from '@src/paragon'; -interface DataTableFilters { - pageSize: number; - pageIndex: number; - sortBy: Array<{ id: string; desc: boolean }>; - filters: Array<{ id: string; value: any }>; -} +type DataTableFilters = Pick & { + sortBy: NonNullable; +}; interface UseQuerySettingsReturn { querySettings: QuerySettings; diff --git a/src/authz-module/libraries-manager/ErrorPage/index.tsx b/src/authz-module/libraries-manager/ErrorPage/index.tsx index e93a7d7f..458187ba 100644 --- a/src/authz-module/libraries-manager/ErrorPage/index.tsx +++ b/src/authz-module/libraries-manager/ErrorPage/index.tsx @@ -11,7 +11,7 @@ import { import messages from './messages'; -const getErrorConfig = ({ errorMessage, errorStatus }) => { +const getErrorConfig = ({ errorMessage, errorStatus }: { errorMessage: string; errorStatus: number }) => { if (errorMessage === CustomErrors.NO_ACCESS || ERROR_STATUS.NO_ACCESS.includes(errorStatus)) { return ({ title: messages['error.page.title.noAccess'], diff --git a/src/authz-module/libraries-manager/LibrariesTeamManager.tsx b/src/authz-module/libraries-manager/LibrariesTeamManager.tsx index f8a7ed46..f40a204a 100644 --- a/src/authz-module/libraries-manager/LibrariesTeamManager.tsx +++ b/src/authz-module/libraries-manager/LibrariesTeamManager.tsx @@ -72,7 +72,7 @@ const LibrariesTeamManager = () => { title={role.name} userCounter={role.userCount} description={role.description} - permissionsByResource={role.resources as any[]} + permissionsByResource={role.resources} /> ))} diff --git a/src/authz-module/libraries-manager/LibrariesUserManager.tsx b/src/authz-module/libraries-manager/LibrariesUserManager.tsx index 7f495d42..b8c75d6a 100644 --- a/src/authz-module/libraries-manager/LibrariesUserManager.tsx +++ b/src/authz-module/libraries-manager/LibrariesUserManager.tsx @@ -3,7 +3,7 @@ import { useNavigate, useParams } from 'react-router-dom'; 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 { Role } from '@src/types'; import { useToastManager } from '@src/authz-module/libraries-manager/ToastManagerContext'; import AuthZLayout from '../components/AuthZLayout'; import { useLibraryAuthZ } from './context'; @@ -40,6 +40,8 @@ const LibrariesUserManager = () => { pageIndex: 0, pageSize: 1, roles: null, + scopes: null, + organizations: null, search: username || null, sortBy: null, }; @@ -170,7 +172,7 @@ const LibrariesUserManager = () => { objectName={library.title} description={role.description} handleDelete={() => handleShowConfirmDeletionModal(role)} - permissionsByResource={role.resources as any[]} + permissionsByResource={role.resources} /> ))} diff --git a/src/authz-module/libraries-manager/ToastManagerContext.tsx b/src/authz-module/libraries-manager/ToastManagerContext.tsx index 6871f2fd..dd4f36ae 100644 --- a/src/authz-module/libraries-manager/ToastManagerContext.tsx +++ b/src/authz-module/libraries-manager/ToastManagerContext.tsx @@ -1,5 +1,6 @@ import { createContext, useContext, useState, useMemo, + ReactNode, } from 'react'; import { logError } from '@edx/frontend-platform/logging'; import { useIntl } from '@edx/frontend-platform/i18n'; @@ -22,19 +23,19 @@ export const ERROR_TOAST_MAP: Record void; delay?: number; } -const Bold = (chunk: string) => {chunk}; +const Bold = (chunks: ReactNode) => {chunks}; const Br = () =>
; type ToastManagerContextType = { showToast: (toast: Omit) => void; - showErrorToast: (error, retryFn?: () => void) => void; - Bold: (chunk: string) => JSX.Element; + showErrorToast: (error: unknown, retryFn?: () => void) => void; + Bold: (chunks: ReactNode) => JSX.Element; Br: () => JSX.Element; }; @@ -63,11 +64,12 @@ export const ToastManagerProvider = ({ children }: ToastManagerProviderProps) => }; const value = useMemo(() => { - const showErrorToast = (error, retryFn?: () => void) => { - logError(error); - const errorStatus = error?.customAttributes?.httpErrorStatus; - const toastConfig = ERROR_TOAST_MAP[errorStatus] || ERROR_TOAST_MAP.DEFAULT; - const message = intl.formatMessage(messages[toastConfig.messageId], { Bold, Br }); + const showErrorToast = (error: unknown, retryFn?: () => void) => { + logError(error as string | Error); + const errorStatus = (error as { customAttributes?: { httpErrorStatus?: number } } | null) + ?.customAttributes?.httpErrorStatus; + const toastConfig = ERROR_TOAST_MAP[errorStatus ?? 'DEFAULT'] || ERROR_TOAST_MAP.DEFAULT; + const message = intl.formatMessage(messages[toastConfig.messageId as keyof typeof messages], { Bold, Br }); /** * For retryable errors, we set a longer delay to give users more time to read the message * and decide to retry, while for non-retryable errors we use the default delay. @@ -111,7 +113,7 @@ export const ToastManagerProvider = ({ children }: ToastManagerProviderProps) => label: intl.formatMessage(messages['library.authz.team.toast.retry.label']), } : undefined} > - {toast.message} + {toast.message as string} ))} diff --git a/src/authz-module/libraries-manager/components/AddNewTeamMemberModal/AddNewTeamMemberModal.tsx b/src/authz-module/libraries-manager/components/AddNewTeamMemberModal/AddNewTeamMemberModal.tsx index 67a85fd7..c1ebe66d 100644 --- a/src/authz-module/libraries-manager/components/AddNewTeamMemberModal/AddNewTeamMemberModal.tsx +++ b/src/authz-module/libraries-manager/components/AddNewTeamMemberModal/AddNewTeamMemberModal.tsx @@ -11,7 +11,7 @@ import { useLibraryAuthZ } from '@src/authz-module/libraries-manager/context'; import { Info, SpinnerSimple } from '@openedx/paragon/icons'; import messages from './messages'; -interface AddNewTeamMemberModalProps { +export interface AddNewTeamMemberModalProps { isOpen: boolean; isError: boolean; isLoading: boolean; @@ -78,7 +78,7 @@ const AddNewTeamMemberModal = ({ name="users" rows="3" value={formValues.users} - onChange={(e) => handleChangeForm(e)} + onChange={handleChangeForm} placeholder={intl.formatMessage(messages['libraries.authz.manage.add.member.users.placeholder'])} style={{ color: isError && 'var(--pgn-color-form-feedback-invalid)' }} /> @@ -89,7 +89,7 @@ const AddNewTeamMemberModal = ({ {intl.formatMessage(messages['libraries.authz.manage.add.member.roles.label'])} - handleChangeForm(e)}> + 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..ebec6d36 100644 --- a/src/authz-module/libraries-manager/components/AddNewTeamMemberModal/AddNewTeamMemberTrigger.test.tsx +++ b/src/authz-module/libraries-manager/components/AddNewTeamMemberModal/AddNewTeamMemberTrigger.test.tsx @@ -4,6 +4,7 @@ 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 type { AddNewTeamMemberModalProps } from './AddNewTeamMemberModal'; import AddNewTeamMemberTrigger from './AddNewTeamMemberTrigger'; jest.mock('@edx/frontend-platform/logging'); @@ -19,7 +20,7 @@ jest.mock('./AddNewTeamMemberModal', () => { /* eslint-disable react/prop-types */ const MockModal = ({ isOpen, close, onSave, isLoading, formValues, handleChangeForm, - }) => ( + }: AddNewTeamMemberModalProps) => ( isOpen ? (
diff --git a/src/authz-module/libraries-manager/components/AddNewTeamMemberModal/AddNewTeamMemberTrigger.tsx b/src/authz-module/libraries-manager/components/AddNewTeamMemberModal/AddNewTeamMemberTrigger.tsx index e21da8d9..02cc33a1 100644 --- a/src/authz-module/libraries-manager/components/AddNewTeamMemberModal/AddNewTeamMemberTrigger.tsx +++ b/src/authz-module/libraries-manager/components/AddNewTeamMemberModal/AddNewTeamMemberTrigger.tsx @@ -87,7 +87,7 @@ const AddNewTeamMemberTrigger = ({ libraryId }: AddNewTeamMemberTriggerProps) => errorTypes.forEach(({ errorMessageId, users }) => { if (users.length === 0) { return; } - const errorMessage = intl.formatMessage(messages[errorMessageId], { + const errorMessage = intl.formatMessage(messages[errorMessageId as keyof typeof messages], { count: users.length, userIds: users.join(', '), Bold, diff --git a/src/authz-module/libraries-manager/components/AssignNewRoleModal/AssignNewRoleModal.test.tsx b/src/authz-module/libraries-manager/components/AssignNewRoleModal/AssignNewRoleModal.test.tsx index e8780570..8ccad40f 100644 --- a/src/authz-module/libraries-manager/components/AssignNewRoleModal/AssignNewRoleModal.test.tsx +++ b/src/authz-module/libraries-manager/components/AssignNewRoleModal/AssignNewRoleModal.test.tsx @@ -1,7 +1,7 @@ import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { renderWrapper } from '@src/setupTest'; -import { Role } from 'types'; +import { Role } from '@src/types'; import AssignNewRoleModal from './AssignNewRoleModal'; describe('AssignNewRoleModal', () => { diff --git a/src/authz-module/libraries-manager/components/AssignNewRoleModal/AssignNewRoleModal.tsx b/src/authz-module/libraries-manager/components/AssignNewRoleModal/AssignNewRoleModal.tsx index 634f42fe..c971f0ae 100644 --- a/src/authz-module/libraries-manager/components/AssignNewRoleModal/AssignNewRoleModal.tsx +++ b/src/authz-module/libraries-manager/components/AssignNewRoleModal/AssignNewRoleModal.tsx @@ -2,7 +2,7 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { ActionRow, Button, Form, ModalDialog, } from '@openedx/paragon'; -import { Role } from 'types'; +import { Role } from '@src/types'; import messages from '../messages'; interface AssignNewRoleModalProps { 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 ce627b32..d2f87775 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 @@ -45,6 +45,9 @@ const mockTeamMember = { email: 'john.doe@example.com', roles: ['instructor', 'author'], createdAt: '2023-01-01T00:00:00Z', + scope: { resource: 'lib123', type: 'LIBRARY' as const }, + organization: 'OpenedX', + role: 'instructor', }; const mockSkeletonMember = { @@ -53,6 +56,9 @@ const mockSkeletonMember = { email: '', roles: [], createdAt: '', + scope: { resource: '', type: 'LIBRARY' as const }, + organization: '', + role: '', }; const mockCellProps = { 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 9f0faa43..63a0a13c 100644 --- a/src/authz-module/libraries-manager/components/TeamTable/components/Cells.tsx +++ b/src/authz-module/libraries-manager/components/TeamTable/components/Cells.tsx @@ -1,7 +1,8 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { Button, Chip, Skeleton } from '@openedx/paragon'; import { Edit } from '@openedx/paragon/icons'; -import { TableCellValue, TeamMember } from '@src/types'; +import { TeamMember } from '@src/types'; +import type { TableCellValue } from '@src/paragon'; import { useLibraryAuthZ } from '@src/authz-module/libraries-manager/context'; import { useNavigate } from 'react-router-dom'; import { useTeamMembers } from '@src/authz-module/data/hooks'; @@ -14,7 +15,7 @@ type CellProps = TableCellValue; const EmailCell = ({ row }: CellProps) => (row.original?.username === SKELETON_ROWS[0].username ? ( ) : ( - row.original.email + {row.original.email} )); const NameCell = ({ row }: CellProps) => { @@ -33,7 +34,7 @@ const NameCell = ({ row }: CellProps) => { ); } - return row.original.username; + return {row.original.username}; }; const ActionCell = ({ row }: CellProps) => { @@ -63,9 +64,11 @@ const RolesCell = ({ row }: CellProps) => { return (row.original.username === SKELETON_ROWS[0].username ? ( ) : ( - row.original.roles.map((role) => ( - {roleLabels[role]} - )) + + {row.original.roles.map((role) => ( + {roleLabels[role]} + ))} + )); }; diff --git a/src/authz-module/libraries-manager/components/TeamTable/components/MultipleChoiceFilter.tsx b/src/authz-module/libraries-manager/components/TeamTable/components/MultipleChoiceFilter.tsx index 3821f05b..c8e55615 100644 --- a/src/authz-module/libraries-manager/components/TeamTable/components/MultipleChoiceFilter.tsx +++ b/src/authz-module/libraries-manager/components/TeamTable/components/MultipleChoiceFilter.tsx @@ -15,7 +15,7 @@ const MultipleChoiceFilter = ({ }: MultipleChoiceFilterProps) => { const checkedBoxes = filterValue || []; - const changeCheckbox = (value) => { + const changeCheckbox = (value: string) => { if (checkedBoxes.includes(value)) { const newCheckedBoxes = checkedBoxes.filter((val) => val !== value); return setFilter(newCheckedBoxes); diff --git a/src/authz-module/libraries-manager/components/TeamTable/components/SearchFilter.test.tsx b/src/authz-module/libraries-manager/components/TeamTable/components/SearchFilter.test.tsx index f216f649..d7f18476 100644 --- a/src/authz-module/libraries-manager/components/TeamTable/components/SearchFilter.test.tsx +++ b/src/authz-module/libraries-manager/components/TeamTable/components/SearchFilter.test.tsx @@ -11,7 +11,7 @@ describe('SearchFilter', () => { const SearchFilterWrapper = ({ initFilterValue = '', customPlaceholder = 'Search placeholder', }:{ initFilterValue?: string; customPlaceholder?:string }) => { - const [filter, setFilter] = useState(initFilterValue); + const [filter, setFilter] = useState(initFilterValue); return ( void; + filterValue: string | undefined; + setFilter: (value: string | undefined) => void; placeholder: string; } @@ -18,7 +18,7 @@ const SearchFilter = ({ trailingElement={} value={filterValue || ''} type="text" - onChange={e => { + onChange={(e: React.ChangeEvent) => { setFilter(e.target.value || undefined); // Set undefined to remove the filter entirely }} placeholder={placeholder} diff --git a/src/authz-module/libraries-manager/components/TeamTable/components/SortDropdown.tsx b/src/authz-module/libraries-manager/components/TeamTable/components/SortDropdown.tsx index 7f2bef04..174a5927 100644 --- a/src/authz-module/libraries-manager/components/TeamTable/components/SortDropdown.tsx +++ b/src/authz-module/libraries-manager/components/TeamTable/components/SortDropdown.tsx @@ -1,4 +1,5 @@ import { + Context, useContext, useState, useMemo, useCallback, useEffect, FC, @@ -11,6 +12,7 @@ import { Stack, } from '@openedx/paragon'; import { SwapVert } from '@openedx/paragon/icons'; +import type { DataTableContextShape } from '@src/paragon'; interface SortOption { id: string; @@ -29,7 +31,7 @@ const SORT_BY_OPTIONS: SortByOptions = { const SortDropdown: FC = () => { const intl = useIntl(); - const { toggleSortBy, state } = useContext(DataTableContext); + const { toggleSortBy, state } = useContext(DataTableContext as unknown as Context); const [sortOrder, setSortOrder] = useState(undefined); const SORT_LABELS: Record = useMemo(() => ({ diff --git a/src/authz-module/libraries-manager/components/TeamTable/components/TableControlBar.test.tsx b/src/authz-module/libraries-manager/components/TeamTable/components/TableControlBar.test.tsx index 0c814776..6ae165ae 100644 --- a/src/authz-module/libraries-manager/components/TeamTable/components/TableControlBar.test.tsx +++ b/src/authz-module/libraries-manager/components/TeamTable/components/TableControlBar.test.tsx @@ -28,7 +28,9 @@ jest.mock('./SortDropdown', () => { jest.mock('./SearchFilter', () => { // eslint-disable-next-line react/prop-types - const MockSearchFilter = (props) => ( + const MockSearchFilter = (props: { + placeholder?: string; filterValue?: string; setFilter: (value: string) => void; + }) => (
{ columns, setAllFilters, state, - } = useContext(DataTableContext); + } = useContext(DataTableContext as unknown as Context); const availableFilters = columns.filter((column) => column.canFilter); @@ -28,8 +29,8 @@ const TableControlBar = () => { .map((column) => column.Header); const getSearchPlaceholder = () => intl.formatMessage(messages['authz.libraries.team.table.search'], { - firstField: columnTextFilterHeaders[0] || 'field', - secondField: columnTextFilterHeaders[1] || 'field', + firstField: String(columnTextFilterHeaders[0] || 'field'), + secondField: String(columnTextFilterHeaders[1] || 'field'), }); return ( @@ -37,7 +38,7 @@ const TableControlBar = () => { {availableFilters.map((column) => { if (column.Filter === CheckboxFilter) { - return ; + return )} />; } if (column.Filter === TextFilter) { @@ -45,7 +46,7 @@ const TableControlBar = () => { ); diff --git a/src/authz-module/libraries-manager/context.tsx b/src/authz-module/libraries-manager/context.tsx index a6ad0035..b38e68f0 100644 --- a/src/authz-module/libraries-manager/context.tsx +++ b/src/authz-module/libraries-manager/context.tsx @@ -5,7 +5,7 @@ import { useParams } from 'react-router-dom'; import { AppContext } from '@edx/frontend-platform/react'; import { useValidateUserPermissions } from '@src/data/hooks'; import { usePermissionsByRole } from '@src/authz-module/data/hooks'; -import { PermissionMetadata, ResourceMetadata, Role } from 'types'; +import { PermissionMetadata, ResourceMetadata, Role } from '@src/types'; import { CustomErrors } from '@src/constants'; import { CONTENT_LIBRARY_PERMISSIONS, libraryPermissions, libraryResourceTypes, libraryRolesMetadata, @@ -16,13 +16,6 @@ const LIBRARY_TEAM_PERMISSIONS = [ CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TEAM, ]; -export type AppContextType = { - authenticatedUser: { - username: string; - email: string; - }; -}; - type LibraryAuthZContextType = { canManageTeam: boolean; username: string; @@ -40,7 +33,7 @@ type AuthZProviderProps = { export const LibraryAuthZProvider = ({ children }: AuthZProviderProps) => { const { libraryId } = useParams<{ libraryId: string }>(); - const { authenticatedUser } = useContext(AppContext) as AppContextType; + const { authenticatedUser } = useContext(AppContext) as React.ContextType; // TODO: Implement a custom error view if (!libraryId) { @@ -62,13 +55,13 @@ export const LibraryAuthZProvider = ({ children }: AuthZProviderProps) => { } as Role)); const value = useMemo((): LibraryAuthZContextType => ({ - username: authenticatedUser.username, + username: authenticatedUser?.username ?? '', libraryId, roles, permissions: libraryPermissions, resources: libraryResourceTypes, canManageTeam, - }), [libraryId, authenticatedUser.username, canManageTeam, roles]); + }), [libraryId, authenticatedUser?.username, canManageTeam, roles]); return ( diff --git a/src/authz-module/libraries-manager/utils.test.ts b/src/authz-module/libraries-manager/utils.test.ts index 07d6600a..cd444dc1 100644 --- a/src/authz-module/libraries-manager/utils.test.ts +++ b/src/authz-module/libraries-manager/utils.test.ts @@ -1,6 +1,7 @@ +import type { IntlShape } from '@edx/frontend-platform/i18n'; import { buildPermissionMatrixByResource, buildPermissionMatrixByRole } from './utils'; -const intl = { formatMessage: jest.fn((msg: any) => msg) }; +const intl = { formatMessage: jest.fn((msg: any) => msg) } as unknown as IntlShape; const permissions = [ { diff --git a/src/authz-module/libraries-manager/utils.ts b/src/authz-module/libraries-manager/utils.ts index 17ff64cc..6db95b3e 100644 --- a/src/authz-module/libraries-manager/utils.ts +++ b/src/authz-module/libraries-manager/utils.ts @@ -1,4 +1,4 @@ -import { IntlShape } from '@edx/frontend-platform/i18n'; +import type { IntlShape } from '@edx/frontend-platform/i18n'; import { actionKeys } from '@src/authz-module/components/RoleCard/constants'; import { EnrichedPermission, PermissionMetadata, PermissionsResourceGrouped, @@ -33,7 +33,7 @@ const getPermissionMetadata = (permission: PermissionMetadata, intl: IntlShape): messageResource = actionKey === 'tag' ? 'Tags' : ''; } - const messageDescriptor = actionMessages[messageKey]; + const messageDescriptor = actionMessages[messageKey as keyof typeof actionMessages]; const label = permission.label || (messageDescriptor ? intl.formatMessage(messageDescriptor, { resource: messageResource }) : permission.key); diff --git a/src/authz-module/roles-permissions/courses/constants.ts b/src/authz-module/roles-permissions/courses/constants.ts index f8b498e8..effdc110 100644 --- a/src/authz-module/roles-permissions/courses/constants.ts +++ b/src/authz-module/roles-permissions/courses/constants.ts @@ -1,4 +1,4 @@ -import { PermissionMetadata, ResourceMetadata } from 'types'; +import { PermissionMetadata, ResourceMetadata } from '@src/types'; import { LibraryBooks, Article, Group, LocalOffer, BookOpen, diff --git a/src/authz-module/roles-permissions/libraries/constants.ts b/src/authz-module/roles-permissions/libraries/constants.ts index 9c2b3dc9..4eac8927 100644 --- a/src/authz-module/roles-permissions/libraries/constants.ts +++ b/src/authz-module/roles-permissions/libraries/constants.ts @@ -1,4 +1,4 @@ -import { PermissionMetadata, ResourceMetadata, RoleMetadata } from 'types'; +import { PermissionMetadata, ResourceMetadata, RoleMetadata } from '@src/types'; import { Group, CollectionsBookmark, Notes, AutoAwesomeMosaic, } from '@openedx/paragon/icons'; diff --git a/src/authz-module/roles-permissions/libraries/utils.test.ts b/src/authz-module/roles-permissions/libraries/utils.test.ts index 07d6600a..cd444dc1 100644 --- a/src/authz-module/roles-permissions/libraries/utils.test.ts +++ b/src/authz-module/roles-permissions/libraries/utils.test.ts @@ -1,6 +1,7 @@ +import type { IntlShape } from '@edx/frontend-platform/i18n'; import { buildPermissionMatrixByResource, buildPermissionMatrixByRole } from './utils'; -const intl = { formatMessage: jest.fn((msg: any) => msg) }; +const intl = { formatMessage: jest.fn((msg: any) => msg) } as unknown as IntlShape; const permissions = [ { diff --git a/src/authz-module/roles-permissions/libraries/utils.ts b/src/authz-module/roles-permissions/libraries/utils.ts index 7dd5cf53..81c99f98 100644 --- a/src/authz-module/roles-permissions/libraries/utils.ts +++ b/src/authz-module/roles-permissions/libraries/utils.ts @@ -31,7 +31,7 @@ const getPermissionMetadata = (permission: PermissionMetadata, intl: IntlShape): messageKey = 'authz.permissions.actions.manage'; } - const messageDescriptor = actionMessages[messageKey]; + const messageDescriptor = actionMessages[messageKey as keyof typeof actionMessages]; const label = permission.label || (messageDescriptor ? intl.formatMessage(messageDescriptor, { resource: '' }) : permission.key); diff --git a/src/authz-module/team-members/TeamMembersTable.test.tsx b/src/authz-module/team-members/TeamMembersTable.test.tsx index 7619b88c..944c76c3 100644 --- a/src/authz-module/team-members/TeamMembersTable.test.tsx +++ b/src/authz-module/team-members/TeamMembersTable.test.tsx @@ -112,9 +112,9 @@ jest.mock('@src/authz-module/data/hooks', () => ({ })); const mockApiResponses = ( - allAsignmentsResponse = mockedAllRoleAssignments, - orgResponse = mockedOrgs, - scopesResponse = mockedScopes, + allAsignmentsResponse: Record = mockedAllRoleAssignments, + orgResponse: Record = mockedOrgs, + scopesResponse: Record = mockedScopes, ) => { (useAllRoleAssignments as jest.Mock).mockReturnValue(allAsignmentsResponse); (useOrgs as jest.Mock).mockReturnValue(orgResponse); @@ -150,9 +150,10 @@ describe('TeamMembersTable', () => { ...mockedAllRoleAssignments, isLoading: false, error: new Error('Failed to fetch'), - data: { results: [] }, + data: { + results: [], count: 0, next: null, previous: null, + }, }; - // @ts-ignore mockApiResponses(allAsignmentsResponse); renderWithAllProviders(); expect(screen.getByText(/Something went wrong on our end./)).toBeInTheDocument(); diff --git a/src/data/hooks.test.tsx b/src/data/hooks.test.tsx index a2e284cc..480e3a3c 100644 --- a/src/data/hooks.test.tsx +++ b/src/data/hooks.test.tsx @@ -88,7 +88,7 @@ describe('useValidateUserPermissions', () => { }); it('returns allowed true when permissions are valid', async () => { - getAuthenticatedHttpClient.mockReturnValue({ + (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ post: jest.fn().mockResolvedValueOnce({ data: mockValidPermissions }), }); @@ -103,7 +103,7 @@ describe('useValidateUserPermissions', () => { }); it('returns allowed false when permissions are invalid', async () => { - getAuthenticatedHttpClient.mockReturnValue({ + (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ post: jest.fn().mockResolvedValue({ data: mockInvalidPermissions }), }); @@ -120,7 +120,7 @@ describe('useValidateUserPermissions', () => { it('handles error when the API call fails', async () => { const mockError = new Error('API Error'); - getAuthenticatedHttpClient.mockReturnValue({ + (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ post: jest.fn().mockRejectedValue(new Error('API Error')), }); diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 00000000..a07597b0 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,10 @@ +declare module 'lodash.debounce' { + const debounce: any>( + fn: T, + wait?: number, + options?: { leading?: boolean; trailing?: boolean; maxWait?: number }, + ) => T & { cancel: () => void; flush: () => void }; + export default debounce; +} + +declare module '@edx/frontend-component-header'; diff --git a/src/index.tsx b/src/index.tsx index 7e5ee477..fb33467f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -30,12 +30,13 @@ subscribe(APP_READY, () => { ); }); -subscribe(APP_INIT_ERROR, (error) => { +subscribe(APP_INIT_ERROR, (_message, data) => { const root = createRoot(document.getElementById('root') as HTMLElement); + const error = data as { message?: string } | undefined; root.render( - + , ); }); @@ -47,7 +48,7 @@ initialize({ config: () => { mergeConfig({ COURSE_AUTHORING_MICROFRONTEND_URL: process.env.COURSE_AUTHORING_MICROFRONTEND_URL || null, - }, 'AdminConsoleAppConfig'); + }); }, }, }); diff --git a/src/paragon.d.ts b/src/paragon.d.ts new file mode 100644 index 00000000..164965be --- /dev/null +++ b/src/paragon.d.ts @@ -0,0 +1,35 @@ +export interface TableCellValue { + row: { + original: T; + }; +} + +export interface DataTableColumn { + id?: string; + accessor?: string; + Header?: string | React.ReactNode; + Filter?: React.ComponentType; + canFilter?: boolean; + filterValue?: any; + setFilter?: (value: any) => void; + filterChoices?: Array<{ name: string; number: number; value: string }>; + filterOrder?: number; + filterButtonText?: string; + [key: string]: any; +} + +export interface DataTableContextShape { + columns: DataTableColumn[]; + setAllFilters: (filters: any[]) => void; + toggleSortBy: (columnId: string, desc: boolean) => void; + gotoPage: (pageIndex: number) => void; + pageCount: number; + itemCount: number; + rows: Array<{ original: any }>; + state: { + sortBy?: Array<{ id: string; desc: boolean }>; + filters: Array<{ id: string; value: any }>; + pageIndex: number; + pageSize: number; + }; +} diff --git a/src/setupTest.tsx b/src/setupTest.tsx index a6375170..ad7ccedb 100644 --- a/src/setupTest.tsx +++ b/src/setupTest.tsx @@ -9,19 +9,21 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; const mockAppContext = { authenticatedUser: { + userId: 1, username: 'testuser', - email: 'testuser@example.com', + roles: [], + administrator: false, }, config: { ...process.env, }, -}; +} as unknown as React.ContextType; interface WrapperProps { children: ReactNode; } -export const renderWithAllProviders = (ui, options = {}) => { +export const renderWithAllProviders = (ui: ReactNode, options = {}) => { const queryClient = new QueryClient({ defaultOptions: { queries: { @@ -44,7 +46,7 @@ export const renderWithAllProviders = (ui, options = {}) => { return render(ui, { wrapper: Wrapper, ...options }); }; -export const renderWrapper = (ui, options = {}) => { +export const renderWrapper = (ui: ReactNode, options = {}) => { const Wrapper = ({ children }: WrapperProps) => ( diff --git a/src/types.ts b/src/types.ts index 1ce1c194..b44b3ff4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,7 +7,7 @@ export interface PermissionValidationResponse extends PermissionValidationReques allowed: boolean; } -// Libraries AuthZ types +// AuthZ types export interface TeamMember { username: string; fullName: string; @@ -93,20 +93,6 @@ export type PermissionsRoleGrouped = Role & { resources: RoleResourceGroup[]; }; -// Paragon table type -export interface TableCellValue { - row: { - original: T; - }; -} - -export type AppContextType = { - authenticatedUser: { - username: string; - email: string; - }; -}; - export interface UserRole { isSuperadmin?: boolean; role: string; diff --git a/tsconfig.json b/tsconfig.json index a45a627a..b28ec0ff 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,12 +3,12 @@ "compilerOptions": { "outDir": "dist", "rootDir": ".", - "baseUrl": "./src", - "paths": { - "*": ["*"], - "@src/*": ["*"] - } - }, - "include": ["*.js", ".eslintrc.js", "src/**/*", "plugins/**/*"], - "exclude": ["dist", "node_modules"] + // Ignore deprecation warnings for TypeScript 6.0, which will be used in the next major release of TypeScript. + "ignoreDeprecations": "6.0", + "paths": { + "@src/*": ["./src/*"] + } + }, + "include": ["*.js", ".eslintrc.js", "src/**/*", "plugins/**/*"], + "exclude": ["dist", "node_modules"] }