From 4facf1cf5d91b259f8f324f562df4d0062548ae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Ch=C3=A1vez?= Date: Wed, 16 Oct 2024 21:50:17 -0500 Subject: [PATCH] feat: add tags to collections [FC-0062] (#1379) * feat: Add ContentTagsDrawer to collection * test: Add test to show ContentTagsDrawer on CollectionInfo --- .../ContentTagsDrawerHelper.jsx | 2 +- src/content-tags-drawer/data/api.js | 8 +++++++- src/content-tags-drawer/data/apiHooks.jsx | 5 +++-- src/generic/key-utils.test.ts | 20 +++++++++++++++++++ src/generic/key-utils.ts | 15 +++++++++++++- .../collections/CollectionInfo.tsx | 9 ++++++++- .../LibraryCollectionPage.test.tsx | 20 +++++++++++++++++++ 7 files changed, 73 insertions(+), 6 deletions(-) diff --git a/src/content-tags-drawer/ContentTagsDrawerHelper.jsx b/src/content-tags-drawer/ContentTagsDrawerHelper.jsx index 40898a877..2c9bec15b 100644 --- a/src/content-tags-drawer/ContentTagsDrawerHelper.jsx +++ b/src/content-tags-drawer/ContentTagsDrawerHelper.jsx @@ -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 { diff --git a/src/content-tags-drawer/data/api.js b/src/content-tags-drawer/data/api.js index 29b8d9e52..94841f7c9 100644 --- a/src/content-tags-drawer/data/api.js +++ b/src/content-tags-drawer/data/api.js @@ -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} + * @returns {Promise} */ 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:')) { diff --git a/src/content-tags-drawer/data/apiHooks.jsx b/src/content-tags-drawer/data/apiHooks.jsx index 34f70bb3f..5b90a6da8 100644 --- a/src/content-tags-drawer/data/apiHooks.jsx +++ b/src/content-tags-drawer/data/apiHooks.jsx @@ -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 diff --git a/src/generic/key-utils.test.ts b/src/generic/key-utils.test.ts index d3763d27c..224f4a963 100644 --- a/src/generic/key-utils.test.ts +++ b/src/generic/key-utils.test.ts @@ -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); + }); + } + }); }); diff --git a/src/generic/key-utils.ts b/src/generic/key-utils.ts index 103396fda..fa689e89f 100644 --- a/src/generic/key-utils.ts +++ b/src/generic/key-utils.ts @@ -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}`; +}; diff --git a/src/library-authoring/collections/CollectionInfo.tsx b/src/library-authoring/collections/CollectionInfo.tsx index 66987f099..b548c6b2a 100644 --- a/src/library-authoring/collections/CollectionInfo.tsx +++ b/src/library-authoring/collections/CollectionInfo.tsx @@ -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" > - Manage tab placeholder + diff --git a/src/library-authoring/collections/LibraryCollectionPage.test.tsx b/src/library-authoring/collections/LibraryCollectionPage.test.tsx index aa9083631..7c7d1afc6 100644 --- a/src/library-authoring/collections/LibraryCollectionPage.test.tsx +++ b/src/library-authoring/collections/LibraryCollectionPage.test.tsx @@ -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(() =>
Mocked ContentTagsDrawer
)); describe('', () => { beforeEach(() => { @@ -200,6 +202,8 @@ describe('', () => { }); 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('', () => { 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('', () => { 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 () => {