Skip to content

Commit 57b863a

Browse files
committed
Implementing extension links appearance on the sidebar and redirection handling.
1 parent 349c453 commit 57b863a

File tree

7 files changed

+123
-25
lines changed

7 files changed

+123
-25
lines changed

src/components/layout/Sidebar/Sidebar.js

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
33
import ImmutablePropTypes from 'react-immutable-proptypes';
4-
import { FormattedMessage } from 'react-intl';
4+
import { FormattedMessage, injectIntl } from 'react-intl';
55
import { lruMemoize } from 'reselect';
66
import { Link } from 'react-router-dom';
77

@@ -16,6 +16,7 @@ import { isSupervisorRole, isEmpoweredSupervisorRole, isSuperadminRole } from '.
1616
import withLinks from '../../../helpers/withLinks.js';
1717
import { getExternalIdForCAS } from '../../../helpers/cas.js';
1818
import { getConfigVar } from '../../../helpers/config.js';
19+
import { EMPTY_ARRAY } from '../../../helpers/common.js';
1920
import Admin from './Admin.js';
2021

2122
import './sidebar.css';
@@ -24,12 +25,45 @@ const URL_PREFIX = getConfigVar('URL_PATH_PREFIX');
2425

2526
const getUserData = lruMemoize(user => getJsData(user));
2627

28+
const processInstances = lruMemoize(instances =>
29+
instances && instances.size > 0 ? instances.toArray().filter(isReady).map(getJsData) : EMPTY_ARRAY
30+
);
31+
32+
const _getCaption = (caption, locale) =>
33+
typeof caption === 'string'
34+
? caption
35+
: caption && typeof caption === 'object'
36+
? caption[locale] || caption.en || caption[Object.keys(caption)[0]]
37+
: '??';
38+
39+
const getExtensions = lruMemoize((instances, locale) => {
40+
const exts = [];
41+
processInstances(instances).forEach(({ id, extensions = {} }) =>
42+
Object.keys(extensions).forEach(extension =>
43+
exts.push({ extension, caption: _getCaption(extensions[extension], locale), instance: id })
44+
)
45+
);
46+
exts.sort((a, b) => a.caption.localeCompare(b.caption));
47+
return exts;
48+
});
49+
50+
const extensionClickHandler = lruMemoize(fetchExtensionUrl => ev => {
51+
ev.preventDefault();
52+
if (window) {
53+
const extension = ev.currentTarget.dataset.extension;
54+
const instance = ev.currentTarget.dataset.instance;
55+
const locale = ev.currentTarget.dataset.locale;
56+
fetchExtensionUrl(extension, instance, locale).then(({ value: url }) => window.location.assign(url));
57+
}
58+
});
59+
2760
const Sidebar = ({
2861
pendingFetchOperations,
2962
loggedInUser,
3063
effectiveRole = null,
3164
currentUrl,
3265
instances,
66+
fetchExtensionUrl,
3367
links: {
3468
HOME_URI,
3569
FAQ_URL,
@@ -42,6 +76,7 @@ const Sidebar = ({
4276
ARCHIVE_URI,
4377
SIS_INTEGRATION_URI,
4478
},
79+
intl: { locale },
4580
}) => {
4681
const user = getUserData(loggedInUser);
4782

@@ -120,21 +155,15 @@ const Sidebar = ({
120155
link={DASHBOARD_URI}
121156
/>
122157

123-
{instances &&
124-
instances.size > 0 &&
125-
instances
126-
.toArray()
127-
.filter(isReady)
128-
.map(getJsData)
129-
.map(({ id, name }) => (
130-
<MenuItem
131-
key={id}
132-
title={name}
133-
icon="university"
134-
currentPath={currentUrl}
135-
link={INSTANCE_URI_FACTORY(id)}
136-
/>
137-
))}
158+
{processInstances(instances).map(({ id, name }) => (
159+
<MenuItem
160+
key={id}
161+
title={name}
162+
icon="university"
163+
currentPath={currentUrl}
164+
link={INSTANCE_URI_FACTORY(id)}
165+
/>
166+
))}
138167

139168
{isSupervisorRole(effectiveRole) && (
140169
<MenuItem
@@ -181,6 +210,26 @@ const Sidebar = ({
181210
)}
182211

183212
{isSuperadminRole(effectiveRole) && <Admin currentUrl={currentUrl} />}
213+
214+
{getExtensions(instances, locale).length > 0 && (
215+
<ul
216+
className="nav nav-pills sidebar-menu flex-column"
217+
data-lte-toggle="treeview"
218+
role="menu"
219+
data-accordion="false">
220+
<MenuTitle title={<FormattedMessage id="app.sidebar.menu.extensions" defaultMessage="Extensions" />} />
221+
222+
{getExtensions(instances, locale).map(({ extension, caption, instance }) => (
223+
<MenuItem
224+
key={extension}
225+
title={caption}
226+
icon="share-from-square"
227+
linkData={{ extension, instance, locale }}
228+
onClick={extensionClickHandler(fetchExtensionUrl)}
229+
/>
230+
))}
231+
</ul>
232+
)}
184233
</nav>
185234
</div>
186235
</div>
@@ -194,7 +243,9 @@ Sidebar.propTypes = {
194243
effectiveRole: PropTypes.string,
195244
currentUrl: PropTypes.string,
196245
instances: ImmutablePropTypes.list,
246+
fetchExtensionUrl: PropTypes.func.isRequired,
197247
links: PropTypes.object,
248+
intl: PropTypes.shape({ locale: PropTypes.string.isRequired }).isRequired,
198249
};
199250

200-
export default withLinks(Sidebar);
251+
export default withLinks(injectIntl(Sidebar));

src/components/widgets/Sidebar/MenuItem/MenuItem.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,31 @@ import { Link, useLocation } from 'react-router-dom';
55

66
import Icon from '../../../icons';
77
import { getConfigVar } from '../../../../helpers/config.js';
8+
import { EMPTY_OBJ, objectKeyMap } from '../../../../helpers/common.js';
89
import '../Sidebar.css';
910

1011
const SKIN = getConfigVar('SKIN') || 'success';
1112

12-
const MenuItem = ({ title, icon = 'circle', link, notificationsCount = 0, inNewTab = false, small = false }) => {
13+
const MenuItem = ({
14+
title,
15+
icon = 'circle',
16+
link,
17+
notificationsCount = 0,
18+
inNewTab = false,
19+
small = false,
20+
onClick,
21+
linkData = null,
22+
}) => {
1323
const { pathname, search } = useLocation();
1424
const active = link === pathname + search;
1525
return (
1626
<li className={classnames({ 'nav-item': true, small })}>
1727
<Link
1828
to={link}
1929
target={inNewTab ? '_blank' : undefined}
20-
className={classnames({ 'nav-link': true, 'align-items-center': true, active, [`bg-${SKIN}`]: active })}>
30+
className={classnames({ 'nav-link': true, 'align-items-center': true, active, [`bg-${SKIN}`]: active })}
31+
onClick={onClick}
32+
{...(linkData ? objectKeyMap(linkData, k => `data-${k}`) : EMPTY_OBJ)}>
2133
<Icon icon={icon} fixedWidth className="nav-icon" />
2234
<p className="sidebarMenuItem">{title}</p>
2335
{notificationsCount > 0 && <span className="right badge badge-warning">{notificationsCount}</span>}
@@ -33,6 +45,8 @@ MenuItem.propTypes = {
3345
link: PropTypes.string,
3446
inNewTab: PropTypes.bool,
3547
small: PropTypes.bool,
48+
onClick: PropTypes.func,
49+
linkData: PropTypes.object,
3650
};
3751

3852
export default MenuItem;
Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import { connect } from 'react-redux';
22
import Sidebar from '../../components/layout/Sidebar/Sidebar.js';
3+
import { fetchExtensionUrl } from '../../redux/modules/app.js';
34
import { loggedInUserMemberOfInstances } from '../../redux/selectors/instances.js';
45
import {
56
loggedInUserSelector,
67
notificationsSelector,
78
getLoggedInUserEffectiveRole,
89
} from '../../redux/selectors/users.js';
910

10-
export default connect(state => ({
11-
loggedInUser: loggedInUserSelector(state),
12-
effectiveRole: getLoggedInUserEffectiveRole(state),
13-
instances: loggedInUserMemberOfInstances(state),
14-
notifications: notificationsSelector(state),
15-
}))(Sidebar);
11+
export default connect(
12+
state => ({
13+
loggedInUser: loggedInUserSelector(state),
14+
effectiveRole: getLoggedInUserEffectiveRole(state),
15+
instances: loggedInUserMemberOfInstances(state),
16+
notifications: notificationsSelector(state),
17+
}),
18+
dispatch => ({
19+
fetchExtensionUrl: (extension, instance, locale) => dispatch(fetchExtensionUrl(extension, instance, locale)),
20+
})
21+
)(Sidebar);

src/helpers/common.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,20 @@ export const objectMap = (obj, fnc) => {
144144
return res;
145145
};
146146

147+
/**
148+
* Implementation of map() function for objects applied on keys only.
149+
* Create a copy of an object where each key is transformed using given function.
150+
* @param {object} obj Object being copied (used as template).
151+
* @param {function} fnc Mapping function for keys
152+
*/
153+
export const objectKeyMap = (obj, fnc) => {
154+
const res = {};
155+
for (const key in obj) {
156+
res[fnc(key)] = obj[key];
157+
}
158+
return res;
159+
};
160+
147161
/**
148162
* Search object by values and return corresponding key.
149163
* @param {Object} obj object to be searched

src/locales/cs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1749,6 +1749,7 @@
17491749
"app.sidebar.menu.createAccount": "Zaregistrovat se",
17501750
"app.sidebar.menu.dashboard": "Přehled",
17511751
"app.sidebar.menu.exercises": "Úlohy",
1752+
"app.sidebar.menu.extensions": "Rozšíření",
17521753
"app.sidebar.menu.faq": "FAQ",
17531754
"app.sidebar.menu.pipelines": "Pipeline",
17541755
"app.sidebar.menu.signIn": "Přihlásit se",

src/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1749,6 +1749,7 @@
17491749
"app.sidebar.menu.createAccount": "Create account",
17501750
"app.sidebar.menu.dashboard": "Dashboard",
17511751
"app.sidebar.menu.exercises": "Exercises",
1752+
"app.sidebar.menu.extensions": "Extensions",
17521753
"app.sidebar.menu.faq": "FAQ",
17531754
"app.sidebar.menu.pipelines": "Pipelines",
17541755
"app.sidebar.menu.signIn": "Sign in",

src/redux/modules/app.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { createAction, handleActions } from 'redux-actions';
22
import { fromJS } from 'immutable';
3+
import { createApiAction } from '../middleware/apiMiddleware.js';
4+
import { createActionsWithPostfixes } from '../helpers/resourceManager';
35

46
export const actionTypes = {
57
SET_LANG: 'recodex/app/SET_LANG',
68
NEW_PENDING_FETCH_OPERATION: 'recodex/app/NEW_PENDING_FETCH_OPERATION',
79
COMPLETED_FETCH_OPERATION: 'recodex/app/COMPLETED_FETCH_OPERATION',
10+
...createActionsWithPostfixes('EXTENSION_URL', 'recodex/app'),
811
};
912

1013
const createInitialState = lang =>
@@ -19,6 +22,14 @@ export const setLang = createAction(actionTypes.SET_LANG, lang => ({
1922
export const newPendingFetchOperation = createAction(actionTypes.NEW_PENDING_FETCH_OPERATION);
2023
export const completedFetchOperation = createAction(actionTypes.COMPLETED_FETCH_OPERATION);
2124

25+
export const fetchExtensionUrl = (extension, instance, locale) =>
26+
createApiAction({
27+
type: actionTypes.EXTENSION_URL,
28+
method: 'GET',
29+
endpoint: `/extensions/${extension}/${instance}?locale=${locale}`,
30+
meta: { extension, instance, locale },
31+
});
32+
2233
const app = (lang = 'en') =>
2334
handleActions(
2435
{

0 commit comments

Comments
 (0)