Skip to content

Commit cffc4d7

Browse files
authored
feat: migration filter and search bar in legacy libraries tab [FC-0097] (#2421)
Adds search bar and migration filter in legacy libraries tab
1 parent 2516034 commit cffc4d7

8 files changed

Lines changed: 286 additions & 48 deletions

File tree

src/generic/key-utils.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
isLibraryKey,
77
isLibraryV1Key,
88
normalizeContainerType,
9+
parseLibraryKey,
910
} from './key-utils';
1011

1112
describe('component utils', () => {
@@ -69,6 +70,30 @@ describe('component utils', () => {
6970
}
7071
});
7172

73+
describe('parseLibraryKey', () => {
74+
for (const [input, expected] of [
75+
['lib:org:lib', { org: 'org', lib: 'lib' }],
76+
['lib:OpenCraftX:ALPHA', { org: 'OpenCraftX', lib: 'ALPHA' }],
77+
] as const) {
78+
it(`returns '${JSON.stringify(expected)}' for learning context key '${input}'`, () => {
79+
expect(parseLibraryKey(input)).toStrictEqual(expected);
80+
});
81+
}
82+
83+
for (const input of [
84+
'',
85+
undefined,
86+
null,
87+
'not a key',
88+
'lb:foo',
89+
'lb:org:lib:html:id',
90+
]) {
91+
it(`throws an exception for library key '${input}'`, () => {
92+
expect(() => parseLibraryKey(input as any)).toThrow(`Invalid libraryKey: ${input}`);
93+
});
94+
}
95+
});
96+
7297
describe('isLibraryV1Key', () => {
7398
for (const [input, expected] of [
7499
['library-v1:AximX+L1', true],

src/generic/key-utils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ export function getBlockType(usageKey: string): string {
1717
* Parses a library key and returns the organization and library name as an object.
1818
*/
1919
export function parseLibraryKey(libraryKey: string): { org: string, lib: string } {
20-
const [, org, lib] = libraryKey?.split(':') || [];
20+
const splitKey = libraryKey?.split(':') || [];
21+
if (splitKey.length !== 3) {
22+
throw new Error(`Invalid libraryKey: ${libraryKey}`);
23+
}
24+
const [, org, lib] = splitKey;
2125
if (org && lib) {
2226
return { org, lib };
2327
}

src/search-manager/SearchFilterWidget.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import messages from './messages';
1212

1313
/**
1414
* A button that represents a filter on the search.
15-
* If the filter is active, the button displays the currently applied values.
15+
* If the filter is active and skipLabelUpdate is not true, the button displays the currently applied values.
1616
* So when no filter is active it may look like:
1717
* [ Type ▼ ]
1818
* Or when a filter is active and limited to two values, it may look like:
@@ -27,6 +27,7 @@ const SearchFilterWidget: React.FC<{
2727
children: React.ReactNode;
2828
clearFilter: () => void,
2929
icon: React.ComponentType;
30+
skipLabelUpdate?: boolean;
3031
}> = ({ appliedFilters, ...props }) => {
3132
const intl = useIntl();
3233
const [isOpen, open, close] = useToggle(false);
@@ -49,8 +50,10 @@ const SearchFilterWidget: React.FC<{
4950
iconAfter={ArrowDropDown}
5051
>
5152
{props.label}
52-
{appliedFilters.length >= 1 ? <>: {appliedFilters[0].label}</> : null}
53-
{appliedFilters.length > 1 ? <>,&nbsp;<Badge variant="secondary">+{appliedFilters.length - 1}</Badge></> : null}
53+
{!props.skipLabelUpdate && appliedFilters.length >= 1 ? <>: {appliedFilters[0].label}</> : null}
54+
{!props.skipLabelUpdate && appliedFilters.length > 1 ? (
55+
<>,&nbsp;<Badge variant="secondary">+{appliedFilters.length - 1}</Badge></>
56+
) : null}
5457
</Button>
5558
</div>
5659
<ModalPopup

src/studio-home/card-item/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ interface BaseProps {
3030
isMigrated?: boolean;
3131
migratedToKey?: string;
3232
migratedToTitle?: string;
33-
migratedToCollectionKey?: string;
33+
migratedToCollectionKey?: string | null;
3434
}
3535

3636
type Props = BaseProps & (

src/studio-home/factories/mockApiResponses.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,15 @@ export const generateGetStudioHomeLibrariesApiResponse = () => ({
103103
migratedToCollectionKey: 'imported-content',
104104
migratedToCollectionTitle: 'Imported content',
105105
},
106+
{
107+
displayName: 'MBA 1',
108+
libraryKey: 'library-v1:MBA+1234',
109+
url: '/library/library-v1:MBA+1234',
110+
org: 'Cambridge',
111+
number: '1234',
112+
canEdit: true,
113+
isMigrated: false,
114+
},
106115
],
107116
});
108117

src/studio-home/tabs-section/TabsSection.test.tsx

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
fireEvent,
1313
screen,
1414
act,
15+
within,
1516
} from '@src/testUtils';
1617
import messages from '../messages';
1718
import tabMessages from './messages';
@@ -269,7 +270,7 @@ describe('<TabsSection />', () => {
269270
beforeEach(async () => {
270271
await axiosMock.onGet(courseApiLinkV2).reply(200, generateGetStudioCoursesApiResponseV2());
271272
});
272-
it('should switch to Legacy Libraries tab and render specific v1 library details', async () => {
273+
it('should switch to Legacy Libraries tab and render - search and filter should work as expected', async () => {
273274
await axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
274275
await axiosMock.onGet(libraryApiLink).reply(200, generateGetStudioHomeLibrariesApiResponse());
275276
render();
@@ -280,12 +281,70 @@ describe('<TabsSection />', () => {
280281
await user.click(librariesTab);
281282

282283
expect(librariesTab).toHaveClass('active');
284+
const panel = await screen.findByRole('tabpanel', { hidden: false });
283285

284286
expect(await screen.findByText(studioHomeMock.libraries[0].displayName)).toBeVisible();
285287

286288
expect(
287289
await screen.findByText(`${studioHomeMock.libraries[0].org} / ${studioHomeMock.libraries[0].number}`),
288290
).toBeVisible();
291+
292+
// Migration info should be displayed
293+
const migratedContent = generateGetStudioHomeLibrariesApiResponse().libraries[1];
294+
expect(await screen.findByText(migratedContent.displayName)).toBeVisible();
295+
const newTitleElement = await screen.findAllByText(migratedContent.migratedToTitle!);
296+
expect(newTitleElement[0]).toBeVisible();
297+
expect(newTitleElement[0]).toHaveAttribute('href', `/library/${migratedContent.migratedToKey}`);
298+
expect(newTitleElement[1]).toHaveAttribute(
299+
'href',
300+
`/library/${migratedContent.migratedToKey}/collection/${migratedContent.migratedToCollectionKey}`,
301+
);
302+
303+
// Check total count display
304+
expect(await within(panel).findByText('Showing 3 of 3')).toBeInTheDocument();
305+
306+
// Test search functionality
307+
const searchField = await within(panel).findByPlaceholderText('Search');
308+
309+
fireEvent.change(searchField, { target: { value: 'Legacy' } });
310+
// Should only show 1 result i.e. migratedContent.displayName
311+
expect(await within(panel).findByText('Showing 1 of 3')).toBeInTheDocument();
312+
expect(await within(panel).findByText(migratedContent.displayName)).toBeVisible();
313+
// Should not show other items.
314+
expect(within(panel).queryByText(
315+
generateGetStudioHomeLibrariesApiResponse().libraries[0].displayName,
316+
)).not.toBeInTheDocument();
317+
// reset search
318+
fireEvent.change(searchField, { target: { value: '' } });
319+
320+
// Test migration filter
321+
const filter = await within(panel).findByRole('button', { name: 'Any Migration Status' });
322+
await user.click(filter);
323+
let migratedOption = await within(panel).findByRole('checkbox', { name: 'Migrated' });
324+
// This should uncheck Migrated option as all options are selected by default
325+
await user.click(migratedOption);
326+
// Should only show 2 result i.e. unmigrated libraries
327+
expect(await within(panel).findByText('Showing 2 of 3')).toBeInTheDocument();
328+
// test clearing filter
329+
const clearFilter = await within(panel).findByRole('button', { name: 'Clear Filter' });
330+
await user.click(clearFilter);
331+
// Should show all 3 results
332+
expect(await within(panel).findByText('Showing 3 of 3')).toBeInTheDocument();
333+
// Open the filter again
334+
await user.click(filter);
335+
// Reload migratedOption as clearing and opening the filter again creates a new modal
336+
migratedOption = await within(panel).findByRole('checkbox', { name: 'Migrated' });
337+
const unmigratedOption = await within(panel).findByRole('checkbox', { name: 'Unmigrated' });
338+
// both options should be selected by default - even after clearing
339+
expect(migratedOption).toBeChecked();
340+
expect(unmigratedOption).toBeChecked();
341+
// Un-checking both options should reset the state to both checked.
342+
await user.click(unmigratedOption);
343+
await user.click(migratedOption);
344+
expect(migratedOption).toBeChecked();
345+
expect(unmigratedOption).toBeChecked();
346+
// Should show all 3 results
347+
expect(await within(panel).findByText('Showing 3 of 3')).toBeInTheDocument();
289348
});
290349

291350
it('should switch to Libraries tab and render specific v2 library details', async () => {
@@ -331,17 +390,6 @@ describe('<TabsSection />', () => {
331390
expect(
332391
await screen.findByText(`${studioHomeMock.libraries[0].org} / ${studioHomeMock.libraries[0].number}`),
333392
).toBeVisible();
334-
335-
// Migration info should be displayed
336-
const migratedContent = generateGetStudioHomeLibrariesApiResponse().libraries[1];
337-
expect(await screen.findByText(migratedContent.displayName)).toBeVisible();
338-
const newTitleElement = await screen.findAllByText(migratedContent.migratedToTitle!);
339-
expect(newTitleElement[0]).toBeVisible();
340-
expect(newTitleElement[0]).toHaveAttribute('href', `/library/${migratedContent.migratedToKey}`);
341-
expect(newTitleElement[1]).toHaveAttribute(
342-
'href',
343-
`/library/${migratedContent.migratedToKey}/collection/${migratedContent.migratedToCollectionKey}`,
344-
);
345393
});
346394

347395
it('should switch to Libraries tab and render specific v2 library details ("v2 only" mode)', async () => {
@@ -402,10 +450,11 @@ describe('<TabsSection />', () => {
402450
await axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
403451
await axiosMock.onGet(libraryApiLink).reply(404);
404452
render();
453+
const user = userEvent.setup();
405454
await executeThunk(fetchStudioHomeData(), store.dispatch);
406455

407456
const librariesTab = await screen.findByText(tabMessages.legacyLibrariesTabTitle.defaultMessage);
408-
fireEvent.click(librariesTab);
457+
await user.click(librariesTab);
409458

410459
expect(librariesTab).toHaveClass('active');
411460

0 commit comments

Comments
 (0)