Skip to content

Commit 16d1903

Browse files
authored
Tbain/253 add tags count (#2957)
This implements openedx/modular-learning#253 , the task to add tag usage counts to the tags table under the taxonomies table. The corresponding backend part is openedx/openedx-core#506, which updates the count aggregations to ensure the correct count numbers are sent to the frontend. This frontend PR does not depend on the backend part.
1 parent ec4cd64 commit 16d1903

7 files changed

Lines changed: 78 additions & 8 deletions

File tree

src/library-authoring/library-info/LibraryInfo.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ describe('<LibraryInfo />', () => {
291291
expect(screen.getByText('Settings')).toBeInTheDocument();
292292
});
293293

294-
it('renders PublicReadToggle when user can manage team', async () => {
294+
it('renders PublicReadToggle when user can manage team', async () => {
295295
render();
296296
const allowSwitch = await screen.findByRole('switch', { name: /allow public read/i });
297297
expect(allowSwitch).toBeInTheDocument();

src/taxonomy/data/api.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,13 @@ export const apiUrls = {
6767
pageIndex, pageSize, fullDepth, disablePagination,
6868
}: { pageIndex: number | null; pageSize: number | null; fullDepth?: boolean; disablePagination?: boolean }) => {
6969
if (disablePagination) {
70-
return makeUrl(`${taxonomyId}/tags/`, { full_depth_threshold: fullDepth ? MAX_TAXONOMY_ITEMS : 0 });
70+
return makeUrl(`${taxonomyId}/tags/`, { full_depth_threshold: fullDepth ? MAX_TAXONOMY_ITEMS : 0, include_counts: 'true' });
7171
}
7272
return makeUrl(`${taxonomyId}/tags/`, {
7373
page: (pageIndex ?? 0) + 1,
7474
page_size: pageSize ?? 10,
7575
full_depth_threshold: fullDepth ? MAX_TAXONOMY_ITEMS : 0,
76+
include_counts: 'true',
7677
});
7778
},
7879
/**

src/taxonomy/data/apiHooks.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
1414
import { camelCaseObject } from '@edx/frontend-platform';
1515
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
16+
import { useIntl } from '@edx/frontend-platform/i18n';
1617
import { apiUrls, ALL_TAXONOMIES, getApiErrorMessage } from './api';
1718
import * as api from './api';
1819
import type { QueryOptions, TagListData } from './types';
19-
import { useIntl } from '@edx/frontend-platform/i18n';
2020

2121
// Query key patterns. Allows an easy way to clear all data related to a given taxonomy.
2222
// https://github.com/openedx/frontend-app-admin-portal/blob/2ba315d/docs/decisions/0006-tanstack-react-query.rst
@@ -97,6 +97,7 @@ export const useDeleteTaxonomy = () => {
9797
export const useTaxonomyDetails = (taxonomyId: number) => useQuery({
9898
queryKey: taxonomyQueryKeys.taxonomyMetadata(taxonomyId),
9999
queryFn: () => api.getTaxonomy(taxonomyId),
100+
refetchOnMount: 'always',
100101
});
101102

102103
/**
@@ -194,6 +195,7 @@ export const useTagListData = (taxonomyId: number, options: QueryOptions) => {
194195
return camelCaseObject(data) as TagListData;
195196
},
196197
enabled,
198+
refetchOnMount: 'always',
197199
});
198200
};
199201

@@ -228,9 +230,13 @@ export const useCreateTag = (taxonomyId: number) => {
228230
onSuccess: () => {
229231
queryClient.invalidateQueries({
230232
queryKey: taxonomyQueryKeys.taxonomyTagList(taxonomyId),
233+
refetchType: 'none',
231234
});
232235
// In the metadata, 'tagsCount' (and possibly other fields) will have changed:
233-
queryClient.invalidateQueries({ queryKey: taxonomyQueryKeys.taxonomyMetadata(taxonomyId) });
236+
queryClient.invalidateQueries({
237+
queryKey: taxonomyQueryKeys.taxonomyMetadata(taxonomyId),
238+
refetchType: 'none',
239+
});
234240
},
235241
});
236242
};

src/taxonomy/tag-list/TagListTable.test.jsx

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const mockTagsResponse = {
6161
descendant_count: 14,
6262
_id: 1001,
6363
sub_tags_url: '/request/to/load/subtags/1',
64+
usage_count: 1,
6465
},
6566
{
6667
...tagDefaults,
@@ -69,6 +70,7 @@ const mockTagsResponse = {
6970
descendant_count: 10,
7071
_id: 1002,
7172
sub_tags_url: '/request/to/load/subtags/2',
73+
usage_count: 0,
7274
},
7375
{
7476
...tagDefaults,
@@ -77,6 +79,7 @@ const mockTagsResponse = {
7779
descendant_count: 5,
7880
_id: 1003,
7981
sub_tags_url: '/request/to/load/subtags/3',
82+
usage_count: 3,
8083
},
8184
{
8285
...tagDefaults,
@@ -86,6 +89,7 @@ const mockTagsResponse = {
8689
_id: 1111,
8790
sub_tags_url: null,
8891
parent_value: 'root tag 1',
92+
usage_count: 1,
8993
},
9094
{
9195
...tagDefaults,
@@ -95,6 +99,7 @@ const mockTagsResponse = {
9599
_id: 1111,
96100
sub_tags_url: null,
97101
parent_value: 'the child tag',
102+
usage_count: null,
98103
},
99104
],
100105
};
@@ -107,7 +112,7 @@ const mockTagsPaginationResponse = {
107112
start: 0,
108113
results: [],
109114
};
110-
const rootTagsListUrl = 'http://localhost:18010/api/content_tagging/v1/taxonomies/1/tags/?full_depth_threshold=10000';
115+
const rootTagsListUrl = 'http://localhost:18010/api/content_tagging/v1/taxonomies/1/tags/?full_depth_threshold=10000&include_counts=true';
111116
const subTagsResponse = {
112117
next: null,
113118
previous: null,
@@ -217,6 +222,13 @@ describe('<TagListTable />', () => {
217222
expect(rows.length).toBe(3 + 1); // 3 items plus header
218223
expect(within(rows[0]).getAllByRole('columnheader')[0].textContent).toEqual('Tag name');
219224
expect(within(rows[1]).getAllByRole('cell')[0].textContent).toEqual('root tag 1');
225+
expect(within(rows[0]).getAllByRole('columnheader')[1].textContent).toEqual('Usage Count');
226+
});
227+
228+
it('should render usage count correctly for root tag', async () => {
229+
const rows = screen.getAllByRole('row');
230+
expect(rows.length).toBe(3 + 1); // 3 items plus header
231+
expect(within(rows[1]).getAllByRole('cell')[1].textContent).toEqual('1');
220232
});
221233

222234
it('should render page correctly with subtags', async () => {
@@ -226,6 +238,36 @@ describe('<TagListTable />', () => {
226238
expect(childTag).toBeInTheDocument();
227239
});
228240

241+
it('should render usage count correctly for sub tag', async () => {
242+
// Expand all tags and await for child tag to render
243+
const expandButton = screen.getAllByText('Expand All')[0];
244+
fireEvent.click(expandButton);
245+
const childTag = await screen.findByText('the child tag');
246+
expect(childTag).toBeInTheDocument();
247+
248+
const rows = screen.getAllByRole('row');
249+
expect(rows.length).toBe(5 + 1); // 5 items plus header
250+
expect(within(rows[2]).getAllByRole('cell')[1].textContent).toEqual('1');
251+
});
252+
253+
it('should render usage count as empty/no content when usage count is "0"', async () => {
254+
const rows = screen.getAllByRole('row');
255+
expect(rows.length).toBe(3 + 1); // 3 items plus header
256+
expect(within(rows[2]).getAllByRole('cell')[1].textContent).toEqual('');
257+
});
258+
259+
it('should render usage count as empty/no when usage count is "null"', async () => {
260+
// Expand all tags and await for child tag to render
261+
const expandButton = screen.getAllByText('Expand All')[0];
262+
fireEvent.click(expandButton);
263+
const childTag = await screen.findByText('the child tag');
264+
expect(childTag).toBeInTheDocument();
265+
266+
const rows = screen.getAllByRole('row');
267+
expect(rows.length).toBe(5 + 1); // 5 items plus header
268+
expect(within(rows[4]).getAllByRole('cell')[1].textContent).toEqual('');
269+
});
270+
229271
it('should not render pagination footer if too few results', async () => {
230272
axiosMock.onGet(rootTagsListUrl).reply(200, mockTagsResponse);
231273
renderTagListTable();

src/taxonomy/tag-list/TagListTable.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import React, {
33
useMemo,
44
useEffect,
55
} from 'react';
6-
import { useIntl } from '@edx/frontend-platform/i18n';
76
import type { PaginationState } from '@tanstack/react-table';
87
import { useTagListData, useCreateTag } from '../data/apiHooks';
98
import { TagTree } from './tagTree';
@@ -40,7 +39,6 @@ const TagListTable = ({ taxonomyId, maxDepth }: TagListTableProps) => {
4039
// TODO: Simpler approaches have been suggested. Two options are to just use simple React state:
4140
// `isCurrentlyEditingTag` and `lastCreatedTag`, or to use optimistic updates.
4241
// For reference, see https://github.com/openedx/frontend-app-authoring/pull/2872#discussion_r2880965005.
43-
const intl = useIntl();
4442

4543
const [creatingParentId, setCreatingParentId] = useState<RowId | null>(null);
4644
const [editingRowId, setEditingRowId] = useState<RowId | null>(null);

src/taxonomy/tag-list/messages.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ const messages = defineMessages({
55
id: 'course-authoring.tag-list.column.value.header',
66
defaultMessage: 'Tag name',
77
},
8+
tagListColumnCountHeader: {
9+
id: 'course-authoring.tag-list.column.count.header',
10+
defaultMessage: 'Usage Count',
11+
},
812
tagListError: {
913
id: 'course-authoring.tag-list.error',
1014
defaultMessage: 'Error: unable to load child tags',

src/taxonomy/tag-list/tagColumns.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
Bubble,
23
Button,
34
Icon,
45
IconButton,
@@ -24,6 +25,7 @@ interface TagListRowData extends TreeRowData {
2425
depth: number;
2526
childCount: number;
2627
descendantCount: number;
28+
usageCount?: number;
2729
isNew?: boolean;
2830
isEditing?: boolean;
2931
}
@@ -47,6 +49,17 @@ interface GetColumnsArgs {
4749
creatingParentId: RowId | null;
4850
}
4951

52+
const UsageCountDisplay = ({ row }: { row: Row<TreeRowData> }) => {
53+
const count = asTagListRowData(row).usageCount ?? 0;
54+
return (
55+
count > 0 && (
56+
<Bubble expandable>
57+
{count}
58+
</Bubble>
59+
)
60+
);
61+
};
62+
5063
interface ActionsHeaderProps {
5164
onStartDraft: () => void;
5265
setDraftError: (error: string) => void;
@@ -120,7 +133,7 @@ const ActionsMenu = ({ rowData, startSubtagDraft, disableAddSubtag }: ActionsMen
120133
</Dropdown.Menu>
121134
</Dropdown>
122135
);
123-
}
136+
};
124137

125138
function getColumns({
126139
setIsCreatingTopTag,
@@ -135,6 +148,7 @@ function getColumns({
135148
}: GetColumnsArgs): TreeColumnDef[] {
136149
const canAddSubtag = (row: Row<TreeRowData>) => row.depth < maxDepth;
137150
const draftInProgressHintId = 'tag-list-draft-in-progress-hint';
151+
const intl = useIntl();
138152

139153
return [
140154
{
@@ -153,6 +167,11 @@ function getColumns({
153167
);
154168
},
155169
},
170+
{
171+
id: 'count',
172+
header: intl.formatMessage(messages.tagListColumnCountHeader),
173+
cell: UsageCountDisplay,
174+
},
156175
{
157176
id: 'actions',
158177
header: () => (

0 commit comments

Comments
 (0)