Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: collections tab [FC-0062] #1257

Merged
merged 16 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/generic/block-type-utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const STRUCTURAL_TYPE_ICONS: Record<string, React.ComponentType> = {
vertical: UNIT_TYPE_ICONS_MAP.vertical,
sequential: Folder,
chapter: Folder,
collection: Folder,
};

export const COMPONENT_TYPE_STYLE_COLOR_MAP = {
Expand Down
37 changes: 0 additions & 37 deletions src/hooks.js

This file was deleted.

68 changes: 68 additions & 0 deletions src/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useEffect, useState } from 'react';
import { history } from '@edx/frontend-platform';

export const useScrollToHashElement = ({ isLoading }: { isLoading: boolean }) => {
const [elementWithHash, setElementWithHash] = useState<string | null>(null);

useEffect(() => {
const currentHash = window.location.hash.substring(1);

if (currentHash) {
const element = document.getElementById(currentHash);

Check warning on line 11 in src/hooks.ts

View check run for this annotation

Codecov / codecov/patch

src/hooks.ts#L11

Added line #L11 was not covered by tests
if (element) {
element.scrollIntoView();
history.replace({ hash: '' });

Check warning on line 14 in src/hooks.ts

View check run for this annotation

Codecov / codecov/patch

src/hooks.ts#L13-L14

Added lines #L13 - L14 were not covered by tests
}
setElementWithHash(currentHash);

Check warning on line 16 in src/hooks.ts

View check run for this annotation

Codecov / codecov/patch

src/hooks.ts#L16

Added line #L16 was not covered by tests
}
}, [isLoading]);

return { elementWithHash };
};

export const useEscapeClick = ({ onEscape, dependency }: { onEscape: () => void, dependency: any }) => {
useEffect(() => {
const handleEscapeClick = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onEscape();

Check warning on line 27 in src/hooks.ts

View check run for this annotation

Codecov / codecov/patch

src/hooks.ts#L27

Added line #L27 was not covered by tests
}
};

window.addEventListener('keydown', handleEscapeClick);

return () => {
window.removeEventListener('keydown', handleEscapeClick);
};
}, [dependency]);
};

/**
* Hook which loads next page of items on scroll
*/
export const useLoadOnScroll = (
hasNextPage: boolean | undefined,
isFetchingNextPage: boolean,
fetchNextPage: () => void,
enabled: boolean,
) => {
useEffect(() => {
if (enabled) {
const onscroll = () => {
// Verify the position of the scroll to implement an infinite scroll.
// Used `loadLimit` to fetch next page before reach the end of the screen.
const loadLimit = 300;
const scrolledTo = window.scrollY + window.innerHeight;
const scrollDiff = document.body.scrollHeight - scrolledTo;
const isNearToBottom = scrollDiff <= loadLimit;
if (isNearToBottom && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
};
window.addEventListener('scroll', onscroll);
return () => {
window.removeEventListener('scroll', onscroll);
};
}
return () => { };
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
};
22 changes: 16 additions & 6 deletions src/library-authoring/EmptyStates.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,37 @@ import messages from './messages';
import { LibraryContext } from './common/context';
import { useContentLibrary } from './data/apiHooks';

export const NoComponents = () => {
type NoSearchResultsProps = {
searchType?: 'collection' | 'component',
};

export const NoComponents = ({ searchType = 'component' }: NoSearchResultsProps) => {
const { openAddContentSidebar } = useContext(LibraryContext);
const { libraryId } = useParams();
const { data: libraryData } = useContentLibrary(libraryId);
const canEditLibrary = libraryData?.canEditLibrary ?? false;

return (
<Stack direction="horizontal" gap={3} className="mt-6 justify-content-center">
<FormattedMessage {...messages.noComponents} />
{searchType === 'collection'
? <FormattedMessage {...messages.noCollections} />
: <FormattedMessage {...messages.noComponents} />}
{canEditLibrary && (
<Button iconBefore={Add} onClick={() => openAddContentSidebar()}>
<FormattedMessage {...messages.addComponent} />
{searchType === 'collection'
? <FormattedMessage {...messages.addCollection} />
: <FormattedMessage {...messages.addComponent} />}
</Button>
)}
</Stack>
);
};

export const NoSearchResults = () => (
<Stack direction="horizontal" gap={3} className="mt-6 justify-content-center">
<FormattedMessage {...messages.noSearchResults} />
export const NoSearchResults = ({ searchType = 'component' }: NoSearchResultsProps) => (
<Stack direction="horizontal" gap={3} className="my-6 justify-content-center">
{searchType === 'collection'
? <FormattedMessage {...messages.noSearchResultsCollections} />
: <FormattedMessage {...messages.noSearchResults} />}
<ClearFiltersButton variant="primary" size="md" />
</Stack>
);
76 changes: 60 additions & 16 deletions src/library-authoring/LibraryAuthoringPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,12 @@ const returnEmptyResult = (_url, req) => {
// We have to replace the query (search keywords) in the mock results with the actual query,
// because otherwise we may have an inconsistent state that causes more queries and unexpected results.
mockEmptyResult.results[0].query = query;
mockEmptyResult.results[2].query = query;
// And fake the required '_formatted' fields; it contains the highlighting <mark>...</mark> around matched words
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
mockEmptyResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
mockEmptyResult.results[2]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
return mockEmptyResult;
};

Expand All @@ -48,10 +51,14 @@ const returnLowNumberResults = (_url, req) => {
newMockResult.results[0].query = query;
// Limit number of results to just 2
newMockResult.results[0].hits = mockResult.results[0]?.hits.slice(0, 2);
newMockResult.results[2].hits = mockResult.results[2]?.hits.slice(0, 2);
newMockResult.results[0].estimatedTotalHits = 2;
newMockResult.results[2].estimatedTotalHits = 2;
// And fake the required '_formatted' fields; it contains the highlighting <mark>...</mark> around matched words
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
newMockResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
newMockResult.results[2]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
return newMockResult;
};

Expand Down Expand Up @@ -129,42 +136,46 @@ describe('<LibraryAuthoringPage />', () => {

// "Recently Modified" header + sort shown
expect(screen.getAllByText('Recently Modified').length).toEqual(2);
expect(screen.getByText('Collections (0)')).toBeInTheDocument();
expect(screen.getByText('Collections (6)')).toBeInTheDocument();
expect(screen.getByText('Components (10)')).toBeInTheDocument();
expect((await screen.findAllByText('Introduction to Testing'))[0]).toBeInTheDocument();

// Navigate to the components tab
fireEvent.click(screen.getByRole('tab', { name: 'Components' }));
// "Recently Modified" default sort shown
expect(screen.getAllByText('Recently Modified').length).toEqual(1);
expect(screen.queryByText('Collections (0)')).not.toBeInTheDocument();
expect(screen.queryByText('Collections (6)')).not.toBeInTheDocument();
expect(screen.queryByText('Components (10)')).not.toBeInTheDocument();

// Navigate to the collections tab
fireEvent.click(screen.getByRole('tab', { name: 'Collections' }));
// "Recently Modified" default sort shown
expect(screen.getAllByText('Recently Modified').length).toEqual(1);
expect(screen.queryByText('Collections (0)')).not.toBeInTheDocument();
expect(screen.queryByText('Collections (6)')).not.toBeInTheDocument();
expect(screen.queryByText('Components (10)')).not.toBeInTheDocument();
expect(screen.queryByText('There are 10 components in this library')).not.toBeInTheDocument();
expect(screen.getByText('Coming soon!')).toBeInTheDocument();
expect((await screen.findAllByText('Collection 1'))[0]).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: 'Home' }));
// "Recently Modified" header + sort shown
expect(screen.getAllByText('Recently Modified').length).toEqual(2);
expect(screen.getByText('Collections (0)')).toBeInTheDocument();
expect(screen.getByText('Collections (6)')).toBeInTheDocument();
expect(screen.getByText('Components (10)')).toBeInTheDocument();
});

it('shows a library without components', async () => {
it('shows a library without components and collections', async () => {
fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true });
await renderLibraryPage();

expect(await screen.findByText('Content library')).toBeInTheDocument();
expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument();

fireEvent.click(screen.getByRole('tab', { name: 'Collections' }));
expect(screen.getByText('You have not added any collection to this library yet.')).toBeInTheDocument();

fireEvent.click(screen.getByRole('tab', { name: 'Home' }));
expect(screen.getByText('You have not added any content to this library yet.')).toBeInTheDocument();
});

Expand Down Expand Up @@ -211,6 +222,14 @@ describe('<LibraryAuthoringPage />', () => {
// Navigate to the components tab
fireEvent.click(screen.getByRole('tab', { name: 'Components' }));
expect(screen.getByText('No matching components found in this library.')).toBeInTheDocument();

// Navigate to the collections tab
fireEvent.click(screen.getByRole('tab', { name: 'Collections' }));
expect(screen.getByText('No matching collections 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: 'Home' }));
});

it('should open and close new content sidebar', async () => {
Expand Down Expand Up @@ -282,20 +301,29 @@ describe('<LibraryAuthoringPage />', () => {

// "Recently Modified" header + sort shown
await waitFor(() => { expect(screen.getAllByText('Recently Modified').length).toEqual(2); });
expect(screen.getByText('Collections (0)')).toBeInTheDocument();
expect(screen.getByText('Collections (6)')).toBeInTheDocument();
expect(screen.getByText('Components (10)')).toBeInTheDocument();
expect(screen.getAllByText('Introduction to Testing')[0]).toBeInTheDocument();
expect(screen.queryByText('You have not added any content to this library yet.')).not.toBeInTheDocument();

// There should only be one "View All" button, since the Components count
// There should be two "View All" button, since the Components and Collections count
// are above the preview limit (4)
expect(screen.getByText('View All')).toBeInTheDocument();
expect(screen.getAllByText('View All').length).toEqual(2);

// Clicking on "View All" button should navigate to the Components tab
fireEvent.click(screen.getByText('View All'));
// Clicking on first "View All" button should navigate to the Collections tab
fireEvent.click(screen.getAllByText('View All')[0]);
// "Recently Modified" default sort shown
expect(screen.getAllByText('Recently Modified').length).toEqual(1);
expect(screen.queryByText('Collections (0)')).not.toBeInTheDocument();
expect(screen.queryByText('Collections (6)')).not.toBeInTheDocument();
expect(screen.queryByText('Components (10)')).not.toBeInTheDocument();
expect(screen.getByText('Collection 1')).toBeInTheDocument();

fireEvent.click(screen.getByRole('tab', { name: 'Home' }));
// Clicking on second "View All" button should navigate to the Components tab
fireEvent.click(screen.getAllByText('View All')[1]);
// "Recently Modified" default sort shown
expect(screen.getAllByText('Recently Modified').length).toEqual(1);
expect(screen.queryByText('Collections (6)')).not.toBeInTheDocument();
expect(screen.queryByText('Components (10)')).not.toBeInTheDocument();
expect(screen.getAllByText('Introduction to Testing')[0]).toBeInTheDocument();

Expand All @@ -304,7 +332,7 @@ describe('<LibraryAuthoringPage />', () => {
fireEvent.click(screen.getByRole('tab', { name: 'Home' }));
// "Recently Modified" header + sort shown
expect(screen.getAllByText('Recently Modified').length).toEqual(2);
expect(screen.getByText('Collections (0)')).toBeInTheDocument();
expect(screen.getByText('Collections (6)')).toBeInTheDocument();
expect(screen.getByText('Components (10)')).toBeInTheDocument();
});

Expand All @@ -317,7 +345,7 @@ describe('<LibraryAuthoringPage />', () => {

// "Recently Modified" header + sort shown
await waitFor(() => { expect(screen.getAllByText('Recently Modified').length).toEqual(2); });
expect(screen.getByText('Collections (0)')).toBeInTheDocument();
expect(screen.getByText('Collections (2)')).toBeInTheDocument();
expect(screen.getByText('Components (2)')).toBeInTheDocument();
expect(screen.getAllByText('Introduction to Testing')[0]).toBeInTheDocument();
expect(screen.queryByText('You have not added any content to this library yet.')).not.toBeInTheDocument();
Expand Down Expand Up @@ -405,8 +433,8 @@ describe('<LibraryAuthoringPage />', () => {
await renderLibraryPage();

// Click on the first component
waitFor(() => expect(screen.queryByText(displayName)).toBeInTheDocument());
fireEvent.click(screen.getAllByText(displayName)[0]);
expect((await screen.findAllByText(displayName))[0]).toBeInTheDocument();
fireEvent.click((await screen.findAllByText(displayName))[0]);

const sidebar = screen.getByTestId('library-sidebar');

Expand Down Expand Up @@ -518,4 +546,20 @@ describe('<LibraryAuthoringPage />', () => {

expect(screen.getByText(/no matching components/i)).toBeInTheDocument();
});

it('shows both components and collections in recently modified section', async () => {
await renderLibraryPage();

expect(await screen.findByText('Content library')).toBeInTheDocument();
expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument();

// "Recently Modified" header + sort shown
expect(screen.getAllByText('Recently Modified').length).toEqual(2);
const recentModifiedContainer = (await screen.findAllByText('Recently Modified'))[1].parentElement?.parentElement?.parentElement;
expect(recentModifiedContainer).toBeTruthy();

const container = within(recentModifiedContainer!);
expect(container.queryAllByText('Text').length).toBeGreaterThan(0);
expect(container.queryAllByText('Collection').length).toBeGreaterThan(0);
});
});
2 changes: 1 addition & 1 deletion src/library-authoring/LibraryAuthoringPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ const LibraryAuthoringPage = () => {
/>
<Route
path={TabList.collections}
element={<LibraryCollections />}
element={<LibraryCollections variant="full" />}
/>
<Route
path="*"
Expand Down
Loading