Skip to content

Commit 04faf54

Browse files
authored
feat: add container (e.g. unit) tag support (#1782)
1 parent d688cf5 commit 04faf54

12 files changed

Lines changed: 191 additions & 18 deletions

File tree

src/content-tags-drawer/ContentTagsDrawer.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,6 @@ interface ContentTagsDrawerProps {
227227
* It is used both in interfaces of this MFE and in edx-platform interfaces such as iframe.
228228
* - If you want to use it as an iframe, the component obtains the `contentId` from the url parameters.
229229
* Functions to close the drawer are handled internally.
230-
* TODO: We can delete this method when is no longer used on edx-platform.
231230
* - If you want to use it as react component, you need to pass the content id and the close functions
232231
* through the component parameters.
233232
*/
@@ -246,7 +245,7 @@ const ContentTagsDrawer = ({
246245
throw new Error('Error: contentId cannot be null.');
247246
}
248247

249-
const context = useCreateContentTagsDrawerContext(contentId, !readOnly);
248+
const context = useCreateContentTagsDrawerContext(contentId, !readOnly, variant === 'drawer');
250249
const { blockingSheet } = useContext(ContentTagsDrawerSheetContext);
251250

252251
const {

src/content-tags-drawer/ContentTagsDrawerHelper.jsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ import { ContentTagsDrawerSheetContext } from './common/context';
2020
* To *use* the context, just use `useContext(ContentTagsDrawerContext)`
2121
* @param {string} contentId
2222
* @param {boolean} canTagObject
23+
* @param {boolean} fetchMetadata=false If true, fetches metadata for the contentId. This is used on `edx-platform`
24+
* and the Course/Unit Outline to show the content name as the drawer title.
2325
* @returns {ContentTagsDrawerContextData}
2426
*/
25-
export const useCreateContentTagsDrawerContext = (contentId, canTagObject) => {
27+
export const useCreateContentTagsDrawerContext = (contentId, canTagObject, fetchMetadata = false) => {
2628
const intl = useIntl();
2729
const org = extractOrgFromContentId(contentId);
2830

@@ -48,7 +50,7 @@ export const useCreateContentTagsDrawerContext = (contentId, canTagObject) => {
4850
const updateTags = useContentTaxonomyTagsUpdater(contentId);
4951

5052
// Fetch from database
51-
const { data: contentData, isSuccess: isContentDataLoaded } = useContentData(contentId);
53+
const { data: contentData, isSuccess: isContentDataLoaded } = useContentData(contentId, fetchMetadata);
5254
const {
5355
data: contentTaxonomyTagsData,
5456
isSuccess: isContentTaxonomyTagsLoaded,

src/content-tags-drawer/data/api.js

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,12 @@ export async function getContentTaxonomyTagsCount(contentId) {
7070
}
7171

7272
/**
73-
* Fetch meta data (eg: display_name) about the content object (unit/compoenent)
73+
* Fetch meta data (eg: display_name) about the content object (unit/component)
7474
* @param {string} contentId The id of the content object (unit/component)
75-
* @returns {Promise<import("./types.js").ContentData | null>}
75+
* @returns {Promise<import("./types.js").ContentData>}
7676
*/
7777
export async function getContentData(contentId) {
7878
let url;
79-
if (contentId.startsWith('lib-collection:')) {
80-
// This type of usage_key is not used to obtain collections
81-
// is only used in tagging.
82-
return null;
83-
}
8479

8580
if (contentId.startsWith('lb:')) {
8681
url = getLibraryContentDataApiUrl(contentId);

src/content-tags-drawer/data/apiHooks.jsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,13 @@ export const useContentTaxonomyTagsData = (contentId) => (
112112
/**
113113
* Builds the query to get meta data about the content object
114114
* @param {string} contentId The id of the content object (unit/component)
115+
* @param {boolean} enabled Flag to enable/disable the query
115116
*/
116-
export const useContentData = (contentId) => (
117+
export const useContentData = (contentId, enabled) => (
117118
useQuery({
118119
queryKey: ['contentData', contentId],
119-
queryFn: () => getContentData(contentId),
120+
queryFn: enabled ? () => getContentData(contentId) : undefined,
121+
enabled,
120122
})
121123
);
122124

@@ -149,7 +151,7 @@ export const useContentTaxonomyTagsUpdater = (contentId) => {
149151
contentPattern = contentId.replace(/\+type@.*$/, '*');
150152
}
151153
queryClient.invalidateQueries({ queryKey: ['contentTagsCount', contentPattern] });
152-
if (contentId.startsWith('lb:') || contentId.startsWith('lib-collection:')) {
154+
if (contentId.startsWith('lb:') || contentId.startsWith('lib-collection:') || contentId.startsWith('lct:')) {
153155
// Obtain library id from contentId
154156
const libraryId = getLibraryId(contentId);
155157
// Invalidate component metadata to update tags count

src/content-tags-drawer/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { default as ContentTagsDrawer } from './ContentTagsDrawer';
22
export { default as ContentTagsDrawerSheet } from './ContentTagsDrawerSheet';
3+
export { useContentTaxonomyTagsData } from './data/apiHooks';

src/library-authoring/component-info/ComponentManagement.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { mockContentLibrary, mockLibraryBlockMetadata } from '../data/api.mocks'
1313
import ComponentManagement from './ComponentManagement';
1414

1515
jest.mock('../../content-tags-drawer', () => ({
16+
...jest.requireActual('../../content-tags-drawer'),
1617
ContentTagsDrawer: ({ readOnly }: { readOnly: boolean }) => (
1718
<div>Mocked {readOnly ? 'read-only' : 'editable'} ContentTagsDrawer</div>
1819
),

src/library-authoring/component-info/ComponentManagement.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ import { SidebarActions, useSidebarContext } from '../common/context/SidebarCont
1111
import { useLibraryBlockMetadata } from '../data/apiHooks';
1212
import StatusWidget from '../generic/status-widget';
1313
import messages from './messages';
14-
import { ContentTagsDrawer } from '../../content-tags-drawer';
15-
import { useContentTaxonomyTagsData } from '../../content-tags-drawer/data/apiHooks';
14+
import { ContentTagsDrawer, useContentTaxonomyTagsData } from '../../content-tags-drawer';
1615
import ManageCollections from './ManageCollections';
1716

1817
const ComponentManagement = () => {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { setConfig, getConfig } from '@edx/frontend-platform';
2+
3+
import { mockContentTaxonomyTagsData } from '../../content-tags-drawer/data/api.mocks';
4+
import {
5+
initializeMocks,
6+
render as baseRender,
7+
screen,
8+
waitFor,
9+
} from '../../testUtils';
10+
import { LibraryProvider } from '../common/context/LibraryContext';
11+
import { SidebarBodyComponentId, SidebarProvider } from '../common/context/SidebarContext';
12+
import { mockContentLibrary, mockGetContainerMetadata } from '../data/api.mocks';
13+
import ContainerOrganize from './ContainerOrganize';
14+
15+
jest.mock('../../content-tags-drawer', () => ({
16+
...jest.requireActual('../../content-tags-drawer'),
17+
ContentTagsDrawer: ({ readOnly }: { readOnly: boolean }) => (
18+
<div>Mocked {readOnly ? 'read-only' : 'editable'} ContentTagsDrawer</div>
19+
),
20+
}));
21+
22+
mockGetContainerMetadata.applyMock();
23+
mockContentLibrary.applyMock();
24+
mockContentTaxonomyTagsData.applyMock();
25+
26+
const { containerIdForTags } = mockGetContainerMetadata;
27+
28+
const render = (libraryId?: string) => baseRender(<ContainerOrganize />, {
29+
extraWrapper: ({ children }) => (
30+
<LibraryProvider libraryId={libraryId || mockContentLibrary.libraryId}>
31+
<SidebarProvider
32+
initialSidebarComponentInfo={{
33+
id: containerIdForTags,
34+
type: SidebarBodyComponentId.ComponentInfo,
35+
}}
36+
>
37+
{children}
38+
</SidebarProvider>
39+
</LibraryProvider>
40+
),
41+
});
42+
43+
describe('<ContainerOrganize />', () => {
44+
beforeEach(() => {
45+
initializeMocks();
46+
});
47+
48+
test.each([
49+
{
50+
libraryId: mockContentLibrary.libraryId,
51+
expected: 'editable',
52+
},
53+
{
54+
libraryId: mockContentLibrary.libraryIdReadOnly,
55+
expected: 'read-only',
56+
},
57+
])(
58+
'should render the tagging info as $expected',
59+
async ({ libraryId, expected }) => {
60+
setConfig({
61+
...getConfig(),
62+
ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
63+
});
64+
render(libraryId);
65+
await waitFor(() => {
66+
expect(screen.getByText(`Mocked ${expected} ContentTagsDrawer`)).toBeInTheDocument();
67+
});
68+
},
69+
);
70+
71+
it('should render tag count in tagging info', async () => {
72+
setConfig({
73+
...getConfig(),
74+
ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
75+
});
76+
render();
77+
expect(await screen.findByText('Tags (6)')).toBeInTheDocument();
78+
});
79+
});
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { useMemo } from 'react';
2+
import { useIntl } from '@edx/frontend-platform/i18n';
3+
import { getConfig } from '@edx/frontend-platform';
4+
import {
5+
Collapsible,
6+
Icon,
7+
Stack,
8+
useToggle,
9+
} from '@openedx/paragon';
10+
import {
11+
ExpandLess, ExpandMore, Tag,
12+
} from '@openedx/paragon/icons';
13+
14+
import { ContentTagsDrawer, useContentTaxonomyTagsData } from '../../content-tags-drawer';
15+
import { useLibraryContext } from '../common/context/LibraryContext';
16+
import { useSidebarContext } from '../common/context/SidebarContext';
17+
import messages from './messages';
18+
19+
const ContainerOrganize = () => {
20+
const intl = useIntl();
21+
const [tagsCollapseIsOpen, , , toggleTags] = useToggle(true);
22+
23+
const { readOnly } = useLibraryContext();
24+
const { sidebarComponentInfo } = useSidebarContext();
25+
26+
const containerId = sidebarComponentInfo?.id;
27+
// istanbul ignore if: this should never happen
28+
if (!containerId) {
29+
throw new Error('containerId is required');
30+
}
31+
32+
const { data: componentTags } = useContentTaxonomyTagsData(containerId);
33+
34+
const tagsCount = useMemo(() => {
35+
if (!componentTags) {
36+
return 0;
37+
}
38+
let result = 0;
39+
componentTags.taxonomies.forEach((taxonomy) => {
40+
const countedTags : string[] = [];
41+
taxonomy.tags.forEach((tagData) => {
42+
tagData.lineage.forEach((tag) => {
43+
if (!countedTags.includes(tag)) {
44+
result += 1;
45+
countedTags.push(tag);
46+
}
47+
});
48+
});
49+
});
50+
return result;
51+
}, [componentTags]);
52+
53+
return (
54+
<Stack gap={3}>
55+
{[true, 'true'].includes(getConfig().ENABLE_TAGGING_TAXONOMY_PAGES)
56+
&& (
57+
<Collapsible.Advanced
58+
open={tagsCollapseIsOpen}
59+
className="collapsible-card border-0"
60+
>
61+
<Collapsible.Trigger
62+
onClick={toggleTags}
63+
className="collapsible-trigger d-flex justify-content-between p-2"
64+
>
65+
<Stack gap={1} direction="horizontal">
66+
<Icon src={Tag} />
67+
{intl.formatMessage(messages.organizeTabTagsTitle, { count: tagsCount })}
68+
</Stack>
69+
<Collapsible.Visible whenClosed>
70+
<Icon src={ExpandMore} />
71+
</Collapsible.Visible>
72+
<Collapsible.Visible whenOpen>
73+
<Icon src={ExpandLess} />
74+
</Collapsible.Visible>
75+
</Collapsible.Trigger>
76+
<Collapsible.Body className="collapsible-body">
77+
<ContentTagsDrawer
78+
id={containerId}
79+
variant="component"
80+
readOnly={readOnly}
81+
/>
82+
</Collapsible.Body>
83+
</Collapsible.Advanced>
84+
)}
85+
</Stack>
86+
);
87+
};
88+
89+
export default ContainerOrganize;

src/library-authoring/containers/UnitInfo.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ import {
55
Tab,
66
Tabs,
77
} from '@openedx/paragon';
8-
98
import { useComponentPickerContext } from '../common/context/ComponentPickerContext';
109
import {
1110
type UnitInfoTab,
1211
UNIT_INFO_TABS,
1312
isUnitInfoTab,
1413
useSidebarContext,
1514
} from '../common/context/SidebarContext';
15+
import ContainerOrganize from './ContainerOrganize';
1616
import messages from './messages';
1717

1818
const UnitInfo = () => {
@@ -57,7 +57,7 @@ const UnitInfo = () => {
5757
Unit Preview
5858
</Tab>
5959
<Tab eventKey={UNIT_INFO_TABS.Organize} title={intl.formatMessage(messages.organizeTabTitle)}>
60-
Organize Unit
60+
<ContainerOrganize />
6161
</Tab>
6262
<Tab eventKey={UNIT_INFO_TABS.Settings} title={intl.formatMessage(messages.settingsTabTitle)}>
6363
Unit Settings

0 commit comments

Comments
 (0)