diff --git a/src/taxonomy/index.scss b/src/taxonomy/index.scss index 13642488e..873c0f549 100644 --- a/src/taxonomy/index.scss +++ b/src/taxonomy/index.scss @@ -3,3 +3,4 @@ @import "taxonomy/delete-dialog/DeleteDialog"; @import "taxonomy/system-defined-badge/SystemDefinedBadge"; @import "taxonomy/export-modal/ExportModal"; +@import "taxonomy/tag-list/TagListTable"; diff --git a/src/taxonomy/tag-list/TagListTable.jsx b/src/taxonomy/tag-list/TagListTable.jsx index 09ec2b990..617787334 100644 --- a/src/taxonomy/tag-list/TagListTable.jsx +++ b/src/taxonomy/tag-list/TagListTable.jsx @@ -39,7 +39,9 @@ SubTagsExpanded.propTypes = { /** * An "Expand" toggle to show/hide subtags, but one which is hidden if the given tag row has no subtags. */ -const OptionalExpandLink = ({ row }) => (row.original.childCount > 0 ? : null); +const OptionalExpandLink = ({ row }) => ( + row.original.childCount > 0 ?
: null +); OptionalExpandLink.propTypes = DataTable.ExpandRow.propTypes; /** @@ -65,6 +67,7 @@ const TagListTable = ({ taxonomyId }) => { const intl = useIntl(); const [options, setOptions] = useState({ pageIndex: 0, + pageSize: 100, }); const { isLoading } = useTagListDataStatus(taxonomyId, options); const tagList = useTagListDataResponse(taxonomyId, options); @@ -76,38 +79,40 @@ const TagListTable = ({ taxonomyId }) => { }; return ( - ( - - )} - columns={[ - { - Header: intl.formatMessage(messages.tagListColumnValueHeader), - Cell: TagValue, - }, - { - id: 'expander', - Header: DataTable.ExpandAll, - Cell: OptionalExpandLink, - }, - ]} - > - - - - - +
+ ( + + )} + columns={[ + { + Header: intl.formatMessage(messages.tagListColumnValueHeader), + Cell: TagValue, + }, + { + id: 'expander', + Header: DataTable.ExpandAll, + Cell: OptionalExpandLink, + }, + ]} + > + + + {tagList?.numPages !== undefined && tagList?.numPages > 1 + && } + +
); }; diff --git a/src/taxonomy/tag-list/TagListTable.scss b/src/taxonomy/tag-list/TagListTable.scss new file mode 100644 index 000000000..ad5c23467 --- /dev/null +++ b/src/taxonomy/tag-list/TagListTable.scss @@ -0,0 +1,12 @@ +.tag-list-table { + table tr:first-child > th:nth-child(2) > span { + // Used to move "Expand all" button to the right. + // Find the first of the second of the first of the . + // + // The approach of the expand buttons cannot be applied here since the + // table headers are rendered differently and at the component level + // there is no control of this style. + display: flex; + justify-content: flex-end; + } +} diff --git a/src/taxonomy/tag-list/TagListTable.test.jsx b/src/taxonomy/tag-list/TagListTable.test.jsx index 2d3837e16..41c5b11f5 100644 --- a/src/taxonomy/tag-list/TagListTable.test.jsx +++ b/src/taxonomy/tag-list/TagListTable.test.jsx @@ -3,7 +3,9 @@ import { IntlProvider } from '@edx/frontend-platform/i18n'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { initializeMockApp } from '@edx/frontend-platform'; import { AppProvider } from '@edx/frontend-platform/react'; -import { render, waitFor, within } from '@testing-library/react'; +import { + render, waitFor, screen, within, +} from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import MockAdapter from 'axios-mock-adapter'; @@ -59,7 +61,16 @@ const mockTagsResponse = { }, ], }; -const rootTagsListUrl = 'http://localhost:18010/api/content_tagging/v1/taxonomies/1/tags/?page=1'; +const mockTagsPaginationResponse = { + next: null, + previous: null, + count: 103, + num_pages: 2, + current_page: 1, + start: 0, + results: [], +}; +const rootTagsListUrl = 'http://localhost:18010/api/content_tagging/v1/taxonomies/1/tags/?page=1&page_size=100'; const subTagsResponse = { next: null, previous: null, @@ -102,22 +113,21 @@ describe('', () => { let resolveResponse; const promise = new Promise(resolve => { resolveResponse = resolve; }); axiosMock.onGet(rootTagsListUrl).reply(() => promise); - const result = render(); - const spinner = result.getByRole('status'); + render(); + const spinner = screen.getByRole('status'); expect(spinner.textContent).toEqual('loading'); resolveResponse([200, {}]); - await waitFor(() => { - expect(result.getByText('No results found')).toBeInTheDocument(); - }); + const noFoundComponent = await screen.findByText('No results found'); + expect(noFoundComponent).toBeInTheDocument(); }); it('should render page correctly', async () => { axiosMock.onGet(rootTagsListUrl).reply(200, mockTagsResponse); - const result = render(); - await waitFor(() => { - expect(result.getByText('root tag 1')).toBeInTheDocument(); - }); - const rows = result.getAllByRole('row'); + render(); + const tag = await screen.findByText('root tag 1'); + expect(tag).toBeInTheDocument(); + + const rows = screen.getAllByRole('row'); expect(rows.length).toBe(3 + 1); // 3 items plus header expect(within(rows[0]).getAllByRole('columnheader')[0].textContent).toEqual('Tag name'); expect(within(rows[1]).getAllByRole('cell')[0].textContent).toEqual('root tag 1 (14)'); @@ -126,11 +136,29 @@ describe('', () => { it('should render page correctly with subtags', async () => { axiosMock.onGet(rootTagsListUrl).reply(200, mockTagsResponse); axiosMock.onGet(subTagsUrl).reply(200, subTagsResponse); - const result = render(); - const expandButton = result.getAllByLabelText('Expand row')[0]; + render(); + const expandButton = screen.getAllByLabelText('Expand row')[0]; expandButton.click(); + const childTag = await screen.findByText('the child tag'); + expect(childTag).toBeInTheDocument(); + }); + + it('should not render pagination footer', async () => { + axiosMock.onGet(rootTagsListUrl).reply(200, mockTagsResponse); + render(); await waitFor(() => { - expect(result.getByText('the child tag')).toBeInTheDocument(); + expect(screen.queryByRole('navigation', { + name: /table pagination/i, + })).not.toBeInTheDocument(); }); }); + + it('should render pagination footer', async () => { + axiosMock.onGet(rootTagsListUrl).reply(200, mockTagsPaginationResponse); + render(); + const tableFooter = await screen.findByRole('navigation', { + name: /table pagination/i, + }); + expect(tableFooter).toBeInTheDocument(); + }); }); diff --git a/src/taxonomy/tag-list/data/api.js b/src/taxonomy/tag-list/data/api.js index 4fcb5e89f..2b6845e34 100644 --- a/src/taxonomy/tag-list/data/api.js +++ b/src/taxonomy/tag-list/data/api.js @@ -9,10 +9,12 @@ import { camelCaseObject, getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; -const getTagListApiUrl = (taxonomyId, page) => new URL( - `api/content_tagging/v1/taxonomies/${taxonomyId}/tags/?page=${page + 1}`, - getApiBaseUrl(), -).href; +const getTagListApiUrl = (taxonomyId, page, pageSize) => { + const url = new URL(`api/content_tagging/v1/taxonomies/${taxonomyId}/tags/`, getApiBaseUrl()); + url.searchParams.append('page', page + 1); + url.searchParams.append('page_size', pageSize); + return url.href; +}; /** * @param {number} taxonomyId @@ -20,11 +22,11 @@ const getTagListApiUrl = (taxonomyId, page) => new URL( * @returns {import('@tanstack/react-query').UseQueryResult} */ export const useTagListData = (taxonomyId, options) => { - const { pageIndex } = options; + const { pageIndex, pageSize } = options; return useQuery({ queryKey: ['tagList', taxonomyId, pageIndex], queryFn: async () => { - const { data } = await getAuthenticatedHttpClient().get(getTagListApiUrl(taxonomyId, pageIndex)); + const { data } = await getAuthenticatedHttpClient().get(getTagListApiUrl(taxonomyId, pageIndex, pageSize)); return camelCaseObject(data); }, }); diff --git a/src/taxonomy/tag-list/data/types.mjs b/src/taxonomy/tag-list/data/types.mjs index fcb4a38fe..8998e609e 100644 --- a/src/taxonomy/tag-list/data/types.mjs +++ b/src/taxonomy/tag-list/data/types.mjs @@ -7,6 +7,7 @@ /** * @typedef {Object} QueryOptions * @property {number} pageIndex + * @property {number} pageSize */ /**