diff --git a/src/library-authoring/library-info/LibraryInfo.test.tsx b/src/library-authoring/library-info/LibraryInfo.test.tsx
index 1ef5ed6f12..4b620677db 100644
--- a/src/library-authoring/library-info/LibraryInfo.test.tsx
+++ b/src/library-authoring/library-info/LibraryInfo.test.tsx
@@ -291,7 +291,7 @@ describe('', () => {
expect(screen.getByText('Settings')).toBeInTheDocument();
});
- it('renders PublicReadToggle when user can manage team', async () => {
+ it('renders PublicReadToggle when user can manage team', async () => {
render();
const allowSwitch = await screen.findByRole('switch', { name: /allow public read/i });
expect(allowSwitch).toBeInTheDocument();
diff --git a/src/taxonomy/data/api.ts b/src/taxonomy/data/api.ts
index 063971ee08..eb50241106 100644
--- a/src/taxonomy/data/api.ts
+++ b/src/taxonomy/data/api.ts
@@ -67,12 +67,13 @@ export const apiUrls = {
pageIndex, pageSize, fullDepth, disablePagination,
}: { pageIndex: number | null; pageSize: number | null; fullDepth?: boolean; disablePagination?: boolean }) => {
if (disablePagination) {
- return makeUrl(`${taxonomyId}/tags/`, { full_depth_threshold: fullDepth ? MAX_TAXONOMY_ITEMS : 0 });
+ return makeUrl(`${taxonomyId}/tags/`, { full_depth_threshold: fullDepth ? MAX_TAXONOMY_ITEMS : 0, include_counts: 'true' });
}
return makeUrl(`${taxonomyId}/tags/`, {
page: (pageIndex ?? 0) + 1,
page_size: pageSize ?? 10,
full_depth_threshold: fullDepth ? MAX_TAXONOMY_ITEMS : 0,
+ include_counts: 'true',
});
},
/**
diff --git a/src/taxonomy/data/apiHooks.ts b/src/taxonomy/data/apiHooks.ts
index 753503cf3f..3207f5665a 100644
--- a/src/taxonomy/data/apiHooks.ts
+++ b/src/taxonomy/data/apiHooks.ts
@@ -13,10 +13,10 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { camelCaseObject } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+import { useIntl } from '@edx/frontend-platform/i18n';
import { apiUrls, ALL_TAXONOMIES, getApiErrorMessage } from './api';
import * as api from './api';
import type { QueryOptions, TagListData } from './types';
-import { useIntl } from '@edx/frontend-platform/i18n';
// Query key patterns. Allows an easy way to clear all data related to a given taxonomy.
// https://github.com/openedx/frontend-app-admin-portal/blob/2ba315d/docs/decisions/0006-tanstack-react-query.rst
@@ -97,6 +97,7 @@ export const useDeleteTaxonomy = () => {
export const useTaxonomyDetails = (taxonomyId: number) => useQuery({
queryKey: taxonomyQueryKeys.taxonomyMetadata(taxonomyId),
queryFn: () => api.getTaxonomy(taxonomyId),
+ refetchOnMount: 'always',
});
/**
@@ -194,6 +195,7 @@ export const useTagListData = (taxonomyId: number, options: QueryOptions) => {
return camelCaseObject(data) as TagListData;
},
enabled,
+ refetchOnMount: 'always',
});
};
@@ -228,9 +230,13 @@ export const useCreateTag = (taxonomyId: number) => {
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: taxonomyQueryKeys.taxonomyTagList(taxonomyId),
+ refetchType: 'none',
});
// In the metadata, 'tagsCount' (and possibly other fields) will have changed:
- queryClient.invalidateQueries({ queryKey: taxonomyQueryKeys.taxonomyMetadata(taxonomyId) });
+ queryClient.invalidateQueries({
+ queryKey: taxonomyQueryKeys.taxonomyMetadata(taxonomyId),
+ refetchType: 'none',
+ });
},
});
};
diff --git a/src/taxonomy/tag-list/TagListTable.test.jsx b/src/taxonomy/tag-list/TagListTable.test.jsx
index a7616a2ab7..6074dc1e23 100644
--- a/src/taxonomy/tag-list/TagListTable.test.jsx
+++ b/src/taxonomy/tag-list/TagListTable.test.jsx
@@ -61,6 +61,7 @@ const mockTagsResponse = {
descendant_count: 14,
_id: 1001,
sub_tags_url: '/request/to/load/subtags/1',
+ usage_count: 1,
},
{
...tagDefaults,
@@ -69,6 +70,7 @@ const mockTagsResponse = {
descendant_count: 10,
_id: 1002,
sub_tags_url: '/request/to/load/subtags/2',
+ usage_count: 0,
},
{
...tagDefaults,
@@ -77,6 +79,7 @@ const mockTagsResponse = {
descendant_count: 5,
_id: 1003,
sub_tags_url: '/request/to/load/subtags/3',
+ usage_count: 3,
},
{
...tagDefaults,
@@ -86,6 +89,7 @@ const mockTagsResponse = {
_id: 1111,
sub_tags_url: null,
parent_value: 'root tag 1',
+ usage_count: 1,
},
{
...tagDefaults,
@@ -95,6 +99,7 @@ const mockTagsResponse = {
_id: 1111,
sub_tags_url: null,
parent_value: 'the child tag',
+ usage_count: null,
},
],
};
@@ -107,7 +112,7 @@ const mockTagsPaginationResponse = {
start: 0,
results: [],
};
-const rootTagsListUrl = 'http://localhost:18010/api/content_tagging/v1/taxonomies/1/tags/?full_depth_threshold=10000';
+const rootTagsListUrl = 'http://localhost:18010/api/content_tagging/v1/taxonomies/1/tags/?full_depth_threshold=10000&include_counts=true';
const subTagsResponse = {
next: null,
previous: null,
@@ -217,6 +222,13 @@ describe('', () => {
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');
+ expect(within(rows[0]).getAllByRole('columnheader')[1].textContent).toEqual('Usage Count');
+ });
+
+ it('should render usage count correctly for root tag', async () => {
+ const rows = screen.getAllByRole('row');
+ expect(rows.length).toBe(3 + 1); // 3 items plus header
+ expect(within(rows[1]).getAllByRole('cell')[1].textContent).toEqual('1');
});
it('should render page correctly with subtags', async () => {
@@ -226,6 +238,36 @@ describe('', () => {
expect(childTag).toBeInTheDocument();
});
+ it('should render usage count correctly for sub tag', async () => {
+ // Expand all tags and await for child tag to render
+ const expandButton = screen.getAllByText('Expand All')[0];
+ fireEvent.click(expandButton);
+ const childTag = await screen.findByText('the child tag');
+ expect(childTag).toBeInTheDocument();
+
+ const rows = screen.getAllByRole('row');
+ expect(rows.length).toBe(5 + 1); // 5 items plus header
+ expect(within(rows[2]).getAllByRole('cell')[1].textContent).toEqual('1');
+ });
+
+ it('should render usage count as empty/no content when usage count is "0"', async () => {
+ const rows = screen.getAllByRole('row');
+ expect(rows.length).toBe(3 + 1); // 3 items plus header
+ expect(within(rows[2]).getAllByRole('cell')[1].textContent).toEqual('');
+ });
+
+ it('should render usage count as empty/no when usage count is "null"', async () => {
+ // Expand all tags and await for child tag to render
+ const expandButton = screen.getAllByText('Expand All')[0];
+ fireEvent.click(expandButton);
+ const childTag = await screen.findByText('the child tag');
+ expect(childTag).toBeInTheDocument();
+
+ const rows = screen.getAllByRole('row');
+ expect(rows.length).toBe(5 + 1); // 5 items plus header
+ expect(within(rows[4]).getAllByRole('cell')[1].textContent).toEqual('');
+ });
+
it('should not render pagination footer if too few results', async () => {
axiosMock.onGet(rootTagsListUrl).reply(200, mockTagsResponse);
renderTagListTable();
diff --git a/src/taxonomy/tag-list/TagListTable.tsx b/src/taxonomy/tag-list/TagListTable.tsx
index 01795b8917..01bfdbfe11 100644
--- a/src/taxonomy/tag-list/TagListTable.tsx
+++ b/src/taxonomy/tag-list/TagListTable.tsx
@@ -3,7 +3,6 @@ import React, {
useMemo,
useEffect,
} from 'react';
-import { useIntl } from '@edx/frontend-platform/i18n';
import type { PaginationState } from '@tanstack/react-table';
import { useTagListData, useCreateTag } from '../data/apiHooks';
import { TagTree } from './tagTree';
@@ -40,7 +39,6 @@ const TagListTable = ({ taxonomyId, maxDepth }: TagListTableProps) => {
// TODO: Simpler approaches have been suggested. Two options are to just use simple React state:
// `isCurrentlyEditingTag` and `lastCreatedTag`, or to use optimistic updates.
// For reference, see https://github.com/openedx/frontend-app-authoring/pull/2872#discussion_r2880965005.
- const intl = useIntl();
const [creatingParentId, setCreatingParentId] = useState(null);
const [editingRowId, setEditingRowId] = useState(null);
diff --git a/src/taxonomy/tag-list/messages.ts b/src/taxonomy/tag-list/messages.ts
index 2e9b7ecadb..b85c8a4696 100644
--- a/src/taxonomy/tag-list/messages.ts
+++ b/src/taxonomy/tag-list/messages.ts
@@ -5,6 +5,10 @@ const messages = defineMessages({
id: 'course-authoring.tag-list.column.value.header',
defaultMessage: 'Tag name',
},
+ tagListColumnCountHeader: {
+ id: 'course-authoring.tag-list.column.count.header',
+ defaultMessage: 'Usage Count',
+ },
tagListError: {
id: 'course-authoring.tag-list.error',
defaultMessage: 'Error: unable to load child tags',
diff --git a/src/taxonomy/tag-list/tagColumns.tsx b/src/taxonomy/tag-list/tagColumns.tsx
index c70b415a69..2fad2e25c6 100644
--- a/src/taxonomy/tag-list/tagColumns.tsx
+++ b/src/taxonomy/tag-list/tagColumns.tsx
@@ -1,4 +1,5 @@
import {
+ Bubble,
Button,
Icon,
IconButton,
@@ -24,6 +25,7 @@ interface TagListRowData extends TreeRowData {
depth: number;
childCount: number;
descendantCount: number;
+ usageCount?: number;
isNew?: boolean;
isEditing?: boolean;
}
@@ -47,6 +49,17 @@ interface GetColumnsArgs {
creatingParentId: RowId | null;
}
+const UsageCountDisplay = ({ row }: { row: Row }) => {
+ const count = asTagListRowData(row).usageCount ?? 0;
+ return (
+ count > 0 && (
+
+ {count}
+
+ )
+ );
+};
+
interface ActionsHeaderProps {
onStartDraft: () => void;
setDraftError: (error: string) => void;
@@ -120,7 +133,7 @@ const ActionsMenu = ({ rowData, startSubtagDraft, disableAddSubtag }: ActionsMen
);
-}
+};
function getColumns({
setIsCreatingTopTag,
@@ -135,6 +148,7 @@ function getColumns({
}: GetColumnsArgs): TreeColumnDef[] {
const canAddSubtag = (row: Row) => row.depth < maxDepth;
const draftInProgressHintId = 'tag-list-draft-in-progress-hint';
+ const intl = useIntl();
return [
{
@@ -153,6 +167,11 @@ function getColumns({
);
},
},
+ {
+ id: 'count',
+ header: intl.formatMessage(messages.tagListColumnCountHeader),
+ cell: UsageCountDisplay,
+ },
{
id: 'actions',
header: () => (