Skip to content

Commit fff40f5

Browse files
navinkarkerarpenido
authored andcommitted
feat: Collections page (in libraries) (openedx#1281)
1 parent 0d472ae commit fff40f5

32 files changed

+1250
-107
lines changed

src/editors/EditorContainer.test.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ jest.mock('react-router', () => ({
88
blockId: 'company-id1',
99
blockType: 'html',
1010
}),
11+
useLocation: () => {},
1112
}));
1213

1314
const props = { learningContextId: 'cOuRsEId' };

src/editors/EditorContainer.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { useParams } from 'react-router-dom';
2+
import { useLocation, useParams } from 'react-router-dom';
33
import { getConfig } from '@edx/frontend-platform';
44

55
import EditorPage from './EditorPage';
@@ -8,7 +8,7 @@ interface Props {
88
/** Course ID or Library ID */
99
learningContextId: string;
1010
/** Event handler sometimes called when user cancels out of the editor page */
11-
onClose?: () => void;
11+
onClose?: (prevPath?: string) => void;
1212
/**
1313
* Event handler called after when user saves their changes using an editor
1414
* and sometimes called when user cancels the editor, instead of onClose.
@@ -17,7 +17,7 @@ interface Props {
1717
* TODO: clean this up so there are separate onCancel and onSave callbacks,
1818
* and they are used consistently instead of this mess.
1919
*/
20-
returnFunction?: () => (newData: Record<string, any> | undefined) => void;
20+
returnFunction?: (prevPath?: string) => (newData: Record<string, any> | undefined) => void;
2121
}
2222

2323
const EditorContainer: React.FC<Props> = ({
@@ -26,6 +26,8 @@ const EditorContainer: React.FC<Props> = ({
2626
returnFunction,
2727
}) => {
2828
const { blockType, blockId } = useParams();
29+
const location = useLocation();
30+
2931
if (blockType === undefined || blockId === undefined) {
3032
// istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker.
3133
return <div>Error: missing URL parameters</div>;
@@ -38,8 +40,8 @@ const EditorContainer: React.FC<Props> = ({
3840
blockId={blockId}
3941
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
4042
lmsEndpointUrl={getConfig().LMS_BASE_URL}
41-
onClose={onClose}
42-
returnFunction={returnFunction}
43+
onClose={onClose ? () => onClose(location.state?.from) : null}
44+
returnFunction={returnFunction ? () => returnFunction(location.state?.from) : null}
4345
/>
4446
</div>
4547
);

src/generic/block-type-utils/index.scss

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
.pgn__icon {
55
color: white;
66
}
7+
8+
.btn-icon {
9+
&:hover, &:active, &:focus {
10+
background-color: darken(#005C9E, 15%);
11+
}
12+
}
713
}
814

915
.component-style-html {
@@ -12,6 +18,12 @@
1218
.pgn__icon {
1319
color: white;
1420
}
21+
22+
.btn-icon {
23+
&:hover, &:active, &:focus {
24+
background-color: darken(#9747FF, 15%);
25+
}
26+
}
1527
}
1628

1729
.component-style-collection {
@@ -20,6 +32,12 @@
2032
.pgn__icon {
2133
color: black;
2234
}
35+
36+
.btn-icon {
37+
&:hover, &:active, &:focus {
38+
background-color: darken(#FFCD29, 15%);
39+
}
40+
}
2341
}
2442

2543
.component-style-video {
@@ -28,6 +46,12 @@
2846
.pgn__icon {
2947
color: white;
3048
}
49+
50+
.btn-icon {
51+
&:hover, &:active, &:focus {
52+
background-color: darken(#358F0A, 15%);
53+
}
54+
}
3155
}
3256

3357
.component-style-vertical {
@@ -36,6 +60,12 @@
3660
.pgn__icon {
3761
color: white;
3862
}
63+
64+
.btn-icon {
65+
&:hover, &:active, &:focus {
66+
background-color: darken(#0B8E77, 15%);
67+
}
68+
}
3969
}
4070

4171
.component-style-other {
@@ -44,4 +74,10 @@
4474
.pgn__icon {
4575
color: white;
4676
}
77+
78+
.btn-icon {
79+
&:hover, &:active, &:focus {
80+
background-color: darken(#646464, 15%);
81+
}
82+
}
4783
}

src/library-authoring/EmptyStates.tsx

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,46 @@
1-
import React, { useContext, useCallback } from 'react';
21
import { useParams } from 'react-router';
32
import { FormattedMessage } from '@edx/frontend-platform/i18n';
3+
import type { MessageDescriptor } from 'react-intl';
44
import {
55
Button, Stack,
66
} from '@openedx/paragon';
77
import { Add } from '@openedx/paragon/icons';
88
import { ClearFiltersButton } from '../search-manager';
99
import messages from './messages';
10-
import { LibraryContext } from './common/context';
1110
import { useContentLibrary } from './data/apiHooks';
1211

13-
type NoSearchResultsProps = {
14-
searchType?: 'collection' | 'component',
15-
};
16-
17-
export const NoComponents = ({ searchType = 'component' }: NoSearchResultsProps) => {
18-
const { openAddContentSidebar, openCreateCollectionModal } = useContext(LibraryContext);
12+
export const NoComponents = ({
13+
infoText = messages.noComponents,
14+
addBtnText = messages.addComponent,
15+
handleBtnClick,
16+
}: {
17+
infoText?: MessageDescriptor;
18+
addBtnText?: MessageDescriptor;
19+
handleBtnClick: () => void;
20+
}) => {
1921
const { libraryId } = useParams();
2022
const { data: libraryData } = useContentLibrary(libraryId);
2123
const canEditLibrary = libraryData?.canEditLibrary ?? false;
2224

23-
const handleOnClickButton = useCallback(() => {
24-
if (searchType === 'collection') {
25-
openCreateCollectionModal();
26-
} else {
27-
openAddContentSidebar();
28-
}
29-
}, [searchType]);
30-
3125
return (
3226
<Stack direction="horizontal" gap={3} className="mt-6 justify-content-center">
33-
{searchType === 'collection'
34-
? <FormattedMessage {...messages.noCollections} />
35-
: <FormattedMessage {...messages.noComponents} />}
27+
<FormattedMessage {...infoText} />
3628
{canEditLibrary && (
37-
<Button iconBefore={Add} onClick={handleOnClickButton}>
38-
{searchType === 'collection'
39-
? <FormattedMessage {...messages.addCollection} />
40-
: <FormattedMessage {...messages.addComponent} />}
29+
<Button iconBefore={Add} onClick={handleBtnClick}>
30+
<FormattedMessage {...addBtnText} />
4131
</Button>
4232
)}
4333
</Stack>
4434
);
4535
};
4636

47-
export const NoSearchResults = ({ searchType = 'component' }: NoSearchResultsProps) => (
37+
export const NoSearchResults = ({
38+
infoText = messages.noSearchResults,
39+
}: {
40+
infoText?: MessageDescriptor;
41+
}) => (
4842
<Stack direction="horizontal" gap={3} className="my-6 justify-content-center">
49-
{searchType === 'collection'
50-
? <FormattedMessage {...messages.noSearchResultsCollections} />
51-
: <FormattedMessage {...messages.noSearchResults} />}
43+
<FormattedMessage {...infoText} />
5244
<ClearFiltersButton variant="primary" size="md" />
5345
</Stack>
5446
);

src/library-authoring/LibraryAuthoringPage.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,8 @@
2020
height: 100vh;
2121
overflow-y: auto;
2222
}
23+
24+
// Reduce breadcrumb bottom margin
25+
ol.list-inline {
26+
margin-bottom: 0;
27+
}

src/library-authoring/LibraryAuthoringPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
SearchSortWidget,
3030
} from '../search-manager';
3131
import LibraryComponents from './components/LibraryComponents';
32-
import LibraryCollections from './LibraryCollections';
32+
import LibraryCollections from './collections/LibraryCollections';
3333
import LibraryHome from './LibraryHome';
3434
import { useContentLibrary } from './data/apiHooks';
3535
import { LibrarySidebar } from './library-sidebar';

src/library-authoring/LibraryHome.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
import React from 'react';
1+
import React, { useContext } from 'react';
22
import { Stack } from '@openedx/paragon';
33
import { useIntl } from '@edx/frontend-platform/i18n';
44

55
import { useSearchContext } from '../search-manager';
66
import { NoComponents, NoSearchResults } from './EmptyStates';
7-
import LibraryCollections from './LibraryCollections';
7+
import LibraryCollections from './collections/LibraryCollections';
88
import { LibraryComponents } from './components';
99
import LibrarySection from './components/LibrarySection';
1010
import LibraryRecentlyModified from './LibraryRecentlyModified';
1111
import messages from './messages';
12+
import { LibraryContext } from './common/context';
1213

1314
type LibraryHomeProps = {
1415
libraryId: string,
@@ -23,10 +24,11 @@ const LibraryHome = ({ libraryId, tabList, handleTabChange } : LibraryHomeProps)
2324
totalCollectionHits: collectionCount,
2425
isFiltered,
2526
} = useSearchContext();
27+
const { openAddContentSidebar } = useContext(LibraryContext);
2628

2729
const renderEmptyState = () => {
2830
if (componentCount === 0 && collectionCount === 0) {
29-
return isFiltered ? <NoSearchResults /> : <NoComponents />;
31+
return isFiltered ? <NoSearchResults /> : <NoComponents handleBtnClick={openAddContentSidebar} />;
3032
}
3133
return null;
3234
};

src/library-authoring/LibraryLayout.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import LibraryAuthoringPage from './LibraryAuthoringPage';
1313
import { LibraryProvider } from './common/context';
1414
import { CreateCollectionModal } from './create-collection';
1515
import { invalidateComponentData } from './data/apiHooks';
16+
import LibraryCollectionPage from './collections/LibraryCollectionPage';
1617

1718
const LibraryLayout = () => {
1819
const { libraryId } = useParams();
@@ -24,14 +25,20 @@ const LibraryLayout = () => {
2425
}
2526

2627
const navigate = useNavigate();
27-
const goBack = React.useCallback(() => {
28-
// Go back to the library
29-
navigate(`/library/${libraryId}`);
28+
const goBack = React.useCallback((prevPath?: string) => {
29+
if (prevPath) {
30+
// Redirects back to the previous route like collection page or library page
31+
navigate(prevPath);
32+
} else {
33+
// Go back to the library
34+
navigate(`/library/${libraryId}`);
35+
}
3036
}, []);
31-
const returnFunction = React.useCallback(() => {
37+
38+
const returnFunction = React.useCallback((prevPath?: string) => {
3239
// When changes are cancelled, either onClose (goBack) or this returnFunction will be called.
3340
// When changes are saved, this returnFunction is called.
34-
goBack();
41+
goBack(prevPath);
3542
return (args) => {
3643
if (args === undefined) {
3744
return; // Do nothing - the user cancelled the changes
@@ -58,6 +65,10 @@ const LibraryLayout = () => {
5865
</PageWrap>
5966
)}
6067
/>
68+
<Route
69+
path="collection/:collectionId"
70+
element={<LibraryCollectionPage />}
71+
/>
6172
<Route
6273
path="*"
6374
element={<LibraryAuthoringPage />}

0 commit comments

Comments
 (0)