feat: library units tab (#1754)

Implements the "Units" tab on the Library Authoring.
This commit is contained in:
Rômulo Penido
2025-03-31 15:34:25 -03:00
committed by GitHub
parent 98ae74e78c
commit 272e30f1b1
7 changed files with 114 additions and 7 deletions

View File

@@ -215,6 +215,10 @@ describe('<LibraryAuthoringPage />', () => {
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('<LibraryAuthoringPage />', () => {
});
});
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('<LibraryAuthoringPage />', () => {
// 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

View File

@@ -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<ContentType>(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 (
<div className="d-flex">
@@ -260,13 +269,14 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage
className="my-3"
>
<Tab eventKey={ContentType.home} title={intl.formatMessage(messages.homeTab)} />
<Tab eventKey={ContentType.components} title={intl.formatMessage(messages.componentsTab)} />
<Tab eventKey={ContentType.collections} title={intl.formatMessage(messages.collectionsTab)} />
<Tab eventKey={ContentType.components} title={intl.formatMessage(messages.componentsTab)} />
<Tab eventKey={ContentType.units} title={intl.formatMessage(messages.unitsTab)} />
</Tabs>
<ActionRow className="my-3">
<SearchKeywordsField className="mr-3" />
<FilterByTags />
{!insideCollections && <FilterByBlockType />}
{!(insideCollections || insideUnits) && <FilterByBlockType />}
<FilterByPublished />
<ClearFiltersButton />
<ActionRow.Spacer />

View File

@@ -58,6 +58,10 @@ const LibraryLayout = () => {
path={ROUTES.COMPONENTS}
element={context(<LibraryAuthoringPage />)}
/>
<Route
path={ROUTES.UNITS}
element={context(<LibraryAuthoringPage />)}
/>
<Route
path={ROUTES.COLLECTIONS}
element={context(<LibraryAuthoringPage />)}

View File

@@ -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": "",

View File

@@ -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',

View File

@@ -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: '',
});

View File

@@ -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<string> | null;
insideCollections: PathMatch<string> | null;
insideComponents: PathMatch<string> | null;
insideUnits: PathMatch<string> | 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,
};
};