From 272e30f1b125017c33ebdaeb13ad9727b20c1cff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Mon, 31 Mar 2025 15:34:25 -0300 Subject: [PATCH] feat: library units tab (#1754) Implements the "Units" tab on the Library Authoring. --- .../LibraryAuthoringPage.test.tsx | 12 +++++- .../LibraryAuthoringPage.tsx | 18 +++++++-- src/library-authoring/LibraryLayout.tsx | 4 ++ .../__mocks__/library-search.json | 38 +++++++++++++++++++ src/library-authoring/messages.ts | 5 +++ src/library-authoring/routes.test.tsx | 29 +++++++++++++- src/library-authoring/routes.ts | 15 +++++++- 7 files changed, 114 insertions(+), 7 deletions(-) diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index 61b99d0a6..2df4e39a9 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -215,6 +215,10 @@ describe('', () => { fireEvent.click(screen.getByRole('tab', { name: 'Collections' })); expect(await screen.findByText('No matching collections found in this library.')).toBeInTheDocument(); + // Navigate to the units tab + fireEvent.click(screen.getByRole('tab', { name: 'Units' })); + expect(await screen.findByText('No matching components found in this library.')).toBeInTheDocument(); + // Go back to Home tab // This step is necessary to avoid the url change leak to other tests fireEvent.click(screen.getByRole('tab', { name: 'All Content' })); @@ -898,7 +902,7 @@ describe('', () => { }); }); - it('Disables Type filter on Collections tab', async () => { + it('Disables Type filter on Collections and Units tab', async () => { await renderLibraryPage(); expect(await screen.findByText('Content library')).toBeInTheDocument(); @@ -918,6 +922,12 @@ describe('', () => { // No Types filter shown expect(screen.queryByRole('button', { name: /type/i })).not.toBeInTheDocument(); + // Navigate to the units tab + fireEvent.click(await screen.findByRole('tab', { name: 'Units' })); + expect((await screen.findAllByText('Test Unit'))[0]).toBeInTheDocument(); + // No Types filter shown + expect(screen.queryByRole('button', { name: /type/i })).not.toBeInTheDocument(); + // Navigate to the components tab fireEvent.click(screen.getByRole('tab', { name: 'Components' })); // Text components should be shown diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index 7c4c54f38..e3321c6a8 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -146,7 +146,12 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage } = useLibraryContext(); const { openInfoSidebar, sidebarComponentInfo } = useSidebarContext(); - const { insideCollections, insideComponents, navigateTo } = useLibraryRoutes(); + const { + insideCollections, + insideComponents, + insideUnits, + navigateTo, + } = useLibraryRoutes(); // The activeKey determines the currently selected tab. const getActiveKey = () => { @@ -159,6 +164,9 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage if (insideComponents) { return ContentType.components; } + if (insideUnits) { + return ContentType.units; + } return ContentType.home; }; const [activeKey, setActiveKey] = useState(getActiveKey); @@ -217,13 +225,14 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage const activeTypeFilters = { components: 'type = "library_block"', collections: 'type = "collection"', + units: 'block_type = "unit"', }; if (activeKey !== ContentType.home) { extraFilter.push(activeTypeFilters[activeKey]); } // Disable filtering by block/problem type when viewing the Collections tab. - const overrideTypesFilter = insideCollections ? new TypesFilterData() : undefined; + const overrideTypesFilter = (insideCollections || insideUnits) ? new TypesFilterData() : undefined; return (
@@ -260,13 +269,14 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage className="my-3" > - + + - {!insideCollections && } + {!(insideCollections || insideUnits) && } diff --git a/src/library-authoring/LibraryLayout.tsx b/src/library-authoring/LibraryLayout.tsx index d0231cea9..844b366e4 100644 --- a/src/library-authoring/LibraryLayout.tsx +++ b/src/library-authoring/LibraryLayout.tsx @@ -58,6 +58,10 @@ const LibraryLayout = () => { path={ROUTES.COMPONENTS} element={context()} /> + )} + /> )} diff --git a/src/library-authoring/__mocks__/library-search.json b/src/library-authoring/__mocks__/library-search.json index d4456c655..9eee97031 100644 --- a/src/library-authoring/__mocks__/library-search.json +++ b/src/library-authoring/__mocks__/library-search.json @@ -481,6 +481,44 @@ "display_name": "String Response Problem", "description": "Problem" } + }, + { + "display_name": "Test Unit", + "block_id": "test-unit-9284e2", + "id": "lctAximTESTunittest-unit-9284e2-a9a4386e", + "type": "library_container", + "breadcrumbs": [ + { + "display_name": "Test Library" + } + ], + "created": 1742221203.895054, + "modified": 1742221203.895054, + "usage_key": "lct:Axim:TEST:unit:test-unit-9284e2", + "block_type": "unit", + "context_key": "lib:Axim:TEST", + "org": "Axim", + "access_id": 15, + "num_children": 0, + "_formatted": { + "display_name": "Test Unit", + "block_id": "test-unit-9284e2", + "id": "lctAximTESTunittest-unit-9284e2-a9a4386e", + "type": "library_container", + "breadcrumbs": [ + { + "display_name": "Test Library" + } + ], + "created": "1742221203.895054", + "modified": "1742221203.895054", + "usage_key": "lct:Axim:TEST:unit:test-unit-9284e2", + "block_type": "unit", + "context_key": "lib:Axim:TEST", + "org": "Axim", + "access_id": "15", + "num_children": "0" + } } ], "query": "", diff --git a/src/library-authoring/messages.ts b/src/library-authoring/messages.ts index 73ffe2066..428838645 100644 --- a/src/library-authoring/messages.ts +++ b/src/library-authoring/messages.ts @@ -46,6 +46,11 @@ const messages = defineMessages({ defaultMessage: 'Collections', description: 'Tab label for the collections tab', }, + unitsTab: { + id: 'course-authoring.library-authoring.units-tab', + defaultMessage: 'Units', + description: 'Tab label for the units tab', + }, componentsTempPlaceholder: { id: 'course-authoring.library-authoring.components-temp-placeholder', defaultMessage: 'There are {componentCount} components in this library', diff --git a/src/library-authoring/routes.test.tsx b/src/library-authoring/routes.test.tsx index 488b7889d..6f4b6c2f6 100644 --- a/src/library-authoring/routes.test.tsx +++ b/src/library-authoring/routes.test.tsx @@ -258,6 +258,33 @@ describe('Library Authoring routes', () => { path: '/collections/clctnId2', }, }, + // "Units" tab + { + label: 'navigate from All Content tab to Units', + origin: { + path: '', + params: {}, + }, + destination: { + path: '/units', + params: { + contentType: ContentType.units, + }, + }, + }, + { + label: 'navigate from Units tab to All Content tab', + origin: { + path: '/units', + params: {}, + }, + destination: { + path: '', + params: { + contentType: ContentType.home, + }, + }, + }, ])( '$label', async ({ origin, destination }) => { @@ -280,7 +307,7 @@ describe('Library Authoring routes', () => { }, }); - expect(mockNavigate).toBeCalledWith({ + expect(mockNavigate).toHaveBeenCalledWith({ pathname: `/library/${mockContentLibrary.libraryId}${destination.path}`, search: '', }); diff --git a/src/library-authoring/routes.ts b/src/library-authoring/routes.ts index fa96c7be0..69080cc00 100644 --- a/src/library-authoring/routes.ts +++ b/src/library-authoring/routes.ts @@ -20,6 +20,8 @@ export const ROUTES = { COMPONENTS: '/components/:componentId?', // * Collections tab, with an optionally selected collectionId in the sidebar. COLLECTIONS: '/collections/:collectionId?', + // * Units tab, with an optionally selected unitId in the sidebar. + UNITS: '/units/:unitId?', // * All Content tab, with an optionally selected componentId in the sidebar. COMPONENT: '/component/:componentId', // * All Content tab, with an optionally selected collectionId in the sidebar. @@ -31,8 +33,9 @@ export const ROUTES = { export enum ContentType { home = '', - components = 'components', collections = 'collections', + components = 'components', + units = 'units', } export type NavigateToData = { @@ -45,6 +48,7 @@ export type LibraryRoutesData = { insideCollection: PathMatch | null; insideCollections: PathMatch | null; insideComponents: PathMatch | null; + insideUnits: PathMatch | null; // Navigate using the best route from the current location for the given parameters. navigateTo: (dict?: NavigateToData) => void; @@ -59,6 +63,7 @@ export const useLibraryRoutes = (): LibraryRoutesData => { const insideCollection = matchPath(BASE_ROUTE + ROUTES.COLLECTION, pathname); const insideCollections = matchPath(BASE_ROUTE + ROUTES.COLLECTIONS, pathname); const insideComponents = matchPath(BASE_ROUTE + ROUTES.COMPONENTS, pathname); + const insideUnits = matchPath(BASE_ROUTE + ROUTES.UNITS, pathname); const navigateTo = useCallback(({ componentId, @@ -78,6 +83,8 @@ export const useLibraryRoutes = (): LibraryRoutesData => { route = ROUTES.COMPONENTS; } else if (contentType === ContentType.collections) { route = ROUTES.COLLECTIONS; + } else if (contentType === ContentType.units) { + route = ROUTES.UNITS; } else if (contentType === ContentType.home) { route = ROUTES.HOME; } else if (insideCollections) { @@ -97,6 +104,11 @@ export const useLibraryRoutes = (): LibraryRoutesData => { // We're inside the Components tab, so stay there, // optionally selecting a component. route = ROUTES.COMPONENTS; + } else if (insideUnits) { + // We're inside the Units tab, so stay there, + // optionally selecting a unit. + // istanbul ignore next: this will be covered when we add unit selection + route = ROUTES.UNITS; } else if (componentId) { // We're inside the All Content tab, so stay there, // and select a component. @@ -124,5 +136,6 @@ export const useLibraryRoutes = (): LibraryRoutesData => { insideCollection, insideCollections, insideComponents, + insideUnits, }; };