feat: add tags to collections [FC-0062] (#1379)

* feat: Add ContentTagsDrawer to collection

* test: Add test to show ContentTagsDrawer on CollectionInfo
This commit is contained in:
Chris Chávez
2024-10-16 21:50:17 -05:00
committed by GitHub
parent b81f611a0e
commit 4facf1cf5d
7 changed files with 73 additions and 6 deletions

View File

@@ -333,7 +333,7 @@ const useContentTagsDrawerContext = (contentId, canTagObject) => {
const closeToast = React.useCallback(() => setToastMessage(undefined), [setToastMessage]);
let contentName = '';
if (isContentDataLoaded) {
if (isContentDataLoaded && contentData) {
if ('displayName' in contentData) {
contentName = contentData.displayName;
} else {

View File

@@ -72,10 +72,16 @@ export async function getContentTaxonomyTagsCount(contentId) {
/**
* Fetch meta data (eg: display_name) about the content object (unit/compoenent)
* @param {string} contentId The id of the content object (unit/component)
* @returns {Promise<import("./types.mjs").ContentData>}
* @returns {Promise<import("./types.mjs").ContentData | null>}
*/
export async function getContentData(contentId) {
let url;
if (contentId.startsWith('lib-collection:')) {
// This type of usage_key is not used to obtain collections
// is only used in tagging.
return null;
}
if (contentId.startsWith('lb:')) {
url = getLibraryContentDataApiUrl(contentId);
} else if (contentId.startsWith('course-v1:')) {

View File

@@ -15,6 +15,7 @@ import {
getContentTaxonomyTagsCount,
} from './api';
import { libraryQueryPredicate, xblockQueryKeys } from '../../library-authoring/data/apiHooks';
import { getLibraryId } from '../../generic/key-utils';
/** @typedef {import("../../taxonomy/tag-list/data/types.mjs").TagListData} TagListData */
/** @typedef {import("../../taxonomy/tag-list/data/types.mjs").TagData} TagData */
@@ -147,9 +148,9 @@ export const useContentTaxonomyTagsUpdater = (contentId) => {
contentPattern = contentId.replace(/\+type@.*$/, '*');
}
queryClient.invalidateQueries({ queryKey: ['contentTagsCount', contentPattern] });
if (contentId.includes('lb:')) {
if (contentId.startsWith('lb:') || contentId.startsWith('lib-collection:')) {
// Obtain library id from contentId
const libraryId = ['lib', ...contentId.split(':').slice(1, 3)].join(':');
const libraryId = getLibraryId(contentId);
// Invalidate component metadata to update tags count
queryClient.invalidateQueries(xblockQueryKeys.componentMetadata(contentId));
// Invalidate content search to update tags count

View File

@@ -1,4 +1,5 @@
import {
buildCollectionUsageKey,
getBlockType,
getLibraryId,
isLibraryKey,
@@ -29,6 +30,8 @@ describe('component utils', () => {
['lb:org:lib:html:id', 'lib:org:lib'],
['lb:OpenCraftX:ALPHA:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd', 'lib:OpenCraftX:ALPHA'],
['lb:Axim:beta:problem:571fe018-f3ce-45c9-8f53-5dafcb422fdd', 'lib:Axim:beta'],
['lib-collection:org:lib:coll', 'lib:org:lib'],
['lib-collection:OpenCraftX:ALPHA:coll', 'lib:OpenCraftX:ALPHA'],
]) {
it(`returns '${expected}' for usage key '${input}'`, () => {
expect(getLibraryId(input)).toStrictEqual(expected);
@@ -75,4 +78,21 @@ describe('component utils', () => {
});
}
});
describe('buildCollectionUsageKey', () => {
for (const [libraryKey, collectionId, expected] of [
['lib:org:lib', 'coll', 'lib-collection:org:lib:coll'],
['lib:OpenCraftX:ALPHA', 'coll', 'lib-collection:OpenCraftX:ALPHA:coll'],
['lb:org:lib:html:id', 'coll', ''],
['lb:OpenCraftX:ALPHA:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd', 'coll', ''],
['library-v1:AximX+L1', 'coll', ''],
['course-v1:AximX+TS100+23', 'coll', ''],
['', 'coll', ''],
['', 'coll', ''],
] as const) {
it(`returns '${expected}' for learning context key '${libraryKey}' and collection Id '${collectionId}'`, () => {
expect(buildCollectionUsageKey(libraryKey, collectionId)).toStrictEqual(expected);
});
}
});
});

View File

@@ -19,7 +19,7 @@ export function getBlockType(usageKey: string): string {
* @returns The library key, e.g. `lib:org:lib`
*/
export function getLibraryId(usageKey: string): string {
if (usageKey && usageKey.startsWith('lb:')) {
if (usageKey && (usageKey.startsWith('lb:') || usageKey.startsWith('lib-collection:'))) {
const org = usageKey.split(':')[1];
const lib = usageKey.split(':')[2];
if (org && lib) {
@@ -38,3 +38,16 @@ export function isLibraryKey(learningContextKey: string | undefined): learningCo
export function isLibraryV1Key(learningContextKey: string | undefined): learningContextKey is string {
return typeof learningContextKey === 'string' && learningContextKey.startsWith('library-v1:');
}
/**
* Build a collection usage key from library V2 context key and collection Id.
* This Collection Usage Key is only used on tagging.
*/
export const buildCollectionUsageKey = (learningContextKey: string, collectionId: string) => {
if (!isLibraryKey(learningContextKey)) {
return '';
}
const orgLib = learningContextKey.replace('lib:', '');
return `lib-collection:${orgLib}:${collectionId}`;
};

View File

@@ -11,6 +11,8 @@ import { useNavigate, useMatch } from 'react-router-dom';
import { useLibraryContext } from '../common/context';
import CollectionDetails from './CollectionDetails';
import messages from './messages';
import { ContentTagsDrawer } from '../../content-tags-drawer';
import { buildCollectionUsageKey } from '../../generic/key-utils';
const CollectionInfo = () => {
const intl = useIntl();
@@ -34,6 +36,8 @@ const CollectionInfo = () => {
throw new Error('sidebarCollectionId is required');
}
const collectionUsageKey = buildCollectionUsageKey(libraryId, sidebarCollectionId);
const handleOpenCollection = useCallback(() => {
if (!componentPickerMode) {
navigate(url);
@@ -61,7 +65,10 @@ const CollectionInfo = () => {
defaultActiveKey="manage"
>
<Tab eventKey="manage" title={intl.formatMessage(messages.manageTabTitle)}>
Manage tab placeholder
<ContentTagsDrawer
id={collectionUsageKey}
variant="component"
/>
</Tab>
<Tab eventKey="details" title={intl.formatMessage(messages.detailsTabTitle)}>
<CollectionDetails />

View File

@@ -19,6 +19,7 @@ import {
import { mockContentSearchConfig, mockGetBlockTypes } from '../../search-manager/data/api.mock';
import { mockBroadcastChannel, mockClipboardEmpty } from '../../generic/data/api.mock';
import { LibraryLayout } from '..';
import { ContentTagsDrawer } from '../../content-tags-drawer';
import { getLibraryCollectionComponentApiUrl } from '../data/api';
let axiosMock: MockAdapter;
@@ -43,6 +44,7 @@ const mockCollection = {
};
const { title } = mockGetCollectionMetadata.collectionData;
jest.mock('../../content-tags-drawer/ContentTagsDrawer', () => jest.fn(() => <div>Mocked ContentTagsDrawer</div>));
describe('<LibraryCollectionPage />', () => {
beforeEach(() => {
@@ -200,6 +202,8 @@ describe('<LibraryCollectionPage />', () => {
});
it('should open collection Info by default', async () => {
const expectedCollectionUsageKey = 'lib-collection:Axim:TEST:my-first-collection';
await renderLibraryCollectionPage();
expect(await screen.findByText('All Collections')).toBeInTheDocument();
@@ -209,9 +213,18 @@ describe('<LibraryCollectionPage />', () => {
expect(screen.getByText('Manage')).toBeInTheDocument();
expect(screen.getByText('Details')).toBeInTheDocument();
expect(screen.getByText('Mocked ContentTagsDrawer')).toBeInTheDocument();
expect(ContentTagsDrawer).toHaveBeenCalledWith(
expect.objectContaining({
id: expectedCollectionUsageKey,
}),
{},
);
});
it('should close and open Collection Info', async () => {
const expectedCollectionUsageKey = 'lib-collection:Axim:TEST:my-first-collection';
await renderLibraryCollectionPage();
expect(await screen.findByText('All Collections')).toBeInTheDocument();
@@ -230,6 +243,13 @@ describe('<LibraryCollectionPage />', () => {
fireEvent.click(collectionInfoBtn);
expect(screen.getByText('Manage')).toBeInTheDocument();
expect(screen.getByText('Details')).toBeInTheDocument();
expect(screen.getByText('Mocked ContentTagsDrawer')).toBeInTheDocument();
expect(ContentTagsDrawer).toHaveBeenCalledWith(
expect.objectContaining({
id: expectedCollectionUsageKey,
}),
{},
);
});
it('sorts collection components', async () => {