From a43b4c2681673bf779a053e6f9ef17cc62a077a5 Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Nov 24 2017 09:13:41 +0000 Subject: Make the left menu more dynamic When the user joins or subscribes to a hub, it is now automatically added to the left menu. --- diff --git a/hubs/static/client/app/components/HubHeader.js b/hubs/static/client/app/components/HubHeader.js index ed49c38..8edb1db 100644 --- a/hubs/static/client/app/components/HubHeader.js +++ b/hubs/static/client/app/components/HubHeader.js @@ -52,7 +52,7 @@ class HubHeader extends React.Component {
- { this.props.currentUser.perms.config_hub && + { this.props.hub.perms.config &&
diff --git a/hubs/static/client/app/components/HubMembership.js b/hubs/static/client/app/components/HubMembership.js index 9f18581..47b4014 100644 --- a/hubs/static/client/app/components/HubMembership.js +++ b/hubs/static/client/app/components/HubMembership.js @@ -53,7 +53,7 @@ class HubMembership extends React.Component { if (!this.props.hub.name) { return null; } - if (this.props.hub.user_hub && this.props.currentUser.perms.config_hub) { + if (this.props.hub.user_hub && this.props.hub.perms.config) { return null; // The user's own hub. } let commonProps = {disabled: false, title: ""} diff --git a/hubs/static/client/app/components/LeftMenu.js b/hubs/static/client/app/components/LeftMenu.js index 43315e0..26ff2f5 100644 --- a/hubs/static/client/app/components/LeftMenu.js +++ b/hubs/static/client/app/components/LeftMenu.js @@ -6,36 +6,62 @@ import "./LeftMenu.css"; class LeftMenu extends React.Component { - propTypes: { - menu: PropTypes.array, - } - defaultProps: { - menu: [], - } - render() { + let userEntries = []; + if (this.props.user.logged_in) { + userEntries = [ + ( + + ), ( + + ), + ...this.props.user.bookmarks.map((entry) => ( + + )) + ]; + } return (
    - {this.props.menu.map((entry) => ( - - )) - } + {userEntries} +
); } } +LeftMenu.propTypes = { + user: PropTypes.object, + allGroupsUrl: PropTypes.string, +} const mapStateToProps = (state) => { return { - menu: state.ui.leftMenu, + user: state.currentUser, + allGroupsUrl: state.urls.allGroups, } }; @@ -44,16 +70,6 @@ export default connect(mapStateToProps)(LeftMenu); class LeftMenuEntry extends React.Component { - propTypes: { - url: PropTypes.string.isRequired, - icon: PropTypes.string.isRequired, - text: PropTypes.string.isRequired, - cssClass: PropTypes.string, - } - defaultProps: { - cssClass: null, - } - render() { const isActive = window.location.pathname.indexOf(this.props.url) !== -1; let cssClass = "nav-link"; @@ -74,3 +90,13 @@ class LeftMenuEntry extends React.Component { ); } } + +LeftMenuEntry.propTypes = { + url: PropTypes.string.isRequired, + icon: PropTypes.string.isRequired, + text: PropTypes.string.isRequired, + cssClass: PropTypes.string, +} +LeftMenuEntry.defaultProps = { + cssClass: null, +} diff --git a/hubs/static/client/app/core/actions/hub.js b/hubs/static/client/app/core/actions/hub.js index e2ae971..7c31285 100644 --- a/hubs/static/client/app/core/actions/hub.js +++ b/hubs/static/client/app/core/actions/hub.js @@ -1,5 +1,6 @@ import { apiCall } from '../utils'; import { addFlashMessage } from './flashMessages'; +import { fetchCurrentUser } from './user'; /* GET */ @@ -113,6 +114,7 @@ function associateResponse(dispatch, role, result) { dispatch(addFlashMessage(result.message, "info")); } dispatch(receiveHubAssoc(role, result.users, result.perms)); + dispatch(fetchCurrentUser()); } function associateError(dispatch, role, error) { diff --git a/hubs/static/client/app/core/actions/user.js b/hubs/static/client/app/core/actions/user.js new file mode 100644 index 0000000..b15acbb --- /dev/null +++ b/hubs/static/client/app/core/actions/user.js @@ -0,0 +1,20 @@ +import { apiCall } from '../utils'; + +export const USER_FETCH_SUCCESS = 'USER_FETCH_SUCCESS'; + +function receiveUser(user) { + return { + type: USER_FETCH_SUCCESS, + user: user, + receivedAt: Date.now() + } +} + +export function fetchCurrentUser() { + return (dispatch, getState) => { + let url = getState().urls.user; + return apiCall(url).then( + user => dispatch(receiveUser(user)), + ); + } +} diff --git a/hubs/static/client/app/core/reducers/hub.js b/hubs/static/client/app/core/reducers/hub.js index e9669c3..9798ba1 100644 --- a/hubs/static/client/app/core/reducers/hub.js +++ b/hubs/static/client/app/core/reducers/hub.js @@ -59,6 +59,7 @@ export function hubReducer( return { ...state, users: action.users, + perms: action.perms, isLoading: false, }; default: diff --git a/hubs/static/client/app/core/reducers/index.js b/hubs/static/client/app/core/reducers/index.js index 65ae9d2..61bb90e 100644 --- a/hubs/static/client/app/core/reducers/index.js +++ b/hubs/static/client/app/core/reducers/index.js @@ -14,6 +14,11 @@ import { import { userReducer } from "./user"; +function noop(state=null, action) { + return state; +} + + const entities = combineReducers({ hub: hubReducer, widgets: widgetsReducer, @@ -26,15 +31,9 @@ const ui = combineReducers({ flashMessages, widgetsEditMode, widgetConfigDialogOpen, - leftMenu: noop, }); -function noop(state=null, action) { - return state; -} - - const rootReducer = combineReducers({ flashMessages, sse: sseReducer, diff --git a/hubs/static/client/app/core/reducers/user.js b/hubs/static/client/app/core/reducers/user.js index ded75b1..3537e9d 100644 --- a/hubs/static/client/app/core/reducers/user.js +++ b/hubs/static/client/app/core/reducers/user.js @@ -1,15 +1,15 @@ import { HUB_ASSOC_SUCCESS, } from '../actions/hub'; +import { + USER_FETCH_SUCCESS, + } from '../actions/user'; export function userReducer(state=null, action) { switch (action.type) { - case HUB_ASSOC_SUCCESS: - return { - ...state, - perms: action.perms, - }; + case USER_FETCH_SUCCESS: + return action.user; default: return state } diff --git a/hubs/static/client/app/widgets/irc/Widget.js b/hubs/static/client/app/widgets/irc/Widget.js index da75bab..56edd85 100644 --- a/hubs/static/client/app/widgets/irc/Widget.js +++ b/hubs/static/client/app/widgets/irc/Widget.js @@ -13,7 +13,7 @@ class IRCWidget extends React.Component { let content; if (!domain || !channel) { content = "No IRC channel configured."; - if (this.props.currentUser.perms.config_hub) { + if (this.props.currentUser.perms.config) { content += " Go to \"Hubs settings\" to configure it."; } content = ( diff --git a/hubs/tests/views/test_api_hub_config.py b/hubs/tests/views/test_api_hub_config.py index 09b030e..ef62acf 100644 --- a/hubs/tests/views/test_api_hub_config.py +++ b/hubs/tests/views/test_api_hub_config.py @@ -30,6 +30,7 @@ class TestAPIHubConfig(APPTest): "user_hub": True, "mtime": "Sun, 01 Jan 2017 00:00:00 GMT", "subscribed_to": [], + 'perms': {'config': True}, "config": { "avatar": avatar_url, 'chat_channel': None, diff --git a/hubs/tests/views/test_hub_view.py b/hubs/tests/views/test_hub_view.py index ea10ca2..52efd30 100644 --- a/hubs/tests/views/test_hub_view.py +++ b/hubs/tests/views/test_hub_view.py @@ -13,7 +13,6 @@ class HubViewTestCase(APPTest): state = response.context["initial_state"] self.assertEqual(state["urls"]["hubConfig"], "/api/hubs/ralph/config") self.assertFalse(state["currentUser"]["logged_in"]) - self.assertFalse(state["currentUser"]["perms"]["config_hub"]) def test_hub_logged_in(self): user = FakeAuthorization('ralph') @@ -23,7 +22,6 @@ class HubViewTestCase(APPTest): response.get_data(as_text=True)) state = response.context["initial_state"] self.assertTrue(state["currentUser"]["logged_in"]) - self.assertTrue(state["currentUser"]["perms"]["config_hub"]) def test_hub_preview(self): hub = Hub.by_name('ralph') diff --git a/hubs/utils/views.py b/hubs/utils/views.py index 073fe1d..648a884 100644 --- a/hubs/utils/views.py +++ b/hubs/utils/views.py @@ -49,6 +49,22 @@ def get_widget_instance(hub, idx, session=None): flask.abort(404) +def get_user_details(): + current_user = flask.g.auth.copy() + if flask.g.auth.logged_in: + user = flask.g.user + current_user["hub"] = flask.url_for("hub", name=user.username) + current_user["stream"] = flask.url_for("stream") + current_user["bookmarks"] = [] + for hub in user.bookmarks: + current_user["bookmarks"].append({ + "name": hub.name, + "url": flask.url_for("hub", name=hub.name), + "cssClass": "idle-{}".format(hub.activity_class), + }) + return current_user + + def get_menu_entries(): """Get the entries for the left menu.""" menu = [] @@ -243,7 +259,7 @@ def get_user_permissions(hub): hub (hubs.models.Hub): A Hub instance. """ return { - "config_hub": hub.allows(flask.g.user, "config"), + "config": hub.allows(flask.g.user, "config"), } diff --git a/hubs/views/api/__init__.py b/hubs/views/api/__init__.py index 3dc4ef2..2a77971 100644 --- a/hubs/views/api/__init__.py +++ b/hubs/views/api/__init__.py @@ -6,3 +6,4 @@ from .hub import * from .hub_association import * from .hub_config import * from .hub_widget import * +from .user import * diff --git a/hubs/views/api/hub.py b/hubs/views/api/hub.py index 5020bf3..6aa0c4f 100644 --- a/hubs/views/api/hub.py +++ b/hubs/views/api/hub.py @@ -3,12 +3,14 @@ from __future__ import absolute_import, unicode_literals import flask from hubs.app import app -from hubs.utils.views import get_hub, require_hub_access +from hubs.utils.views import get_hub, get_user_permissions, require_hub_access @app.route('/api/hubs//', methods=['GET']) @require_hub_access("view", json=True) def api_hub(name): hub = get_hub(name) - result = {"status": "OK", "data": hub.get_props()} + data = hub.get_props() + data["perms"] = get_user_permissions(hub) + result = {"status": "OK", "data": data} return flask.jsonify(result) diff --git a/hubs/views/api/hub_config.py b/hubs/views/api/hub_config.py index f4cc9ed..defff20 100644 --- a/hubs/views/api/hub_config.py +++ b/hubs/views/api/hub_config.py @@ -7,7 +7,8 @@ import flask import hubs.models from hubs.app import app from hubs.utils.views import ( - get_hub, check_hub_access, RequestValidator, require_hub_access, + get_hub, get_user_permissions, check_hub_access, RequestValidator, + require_hub_access, ) log = logging.getLogger(__name__) @@ -43,7 +44,9 @@ def api_hub_config(name): except Exception as err: result = {"status": "ERROR", "message": str(err)} return flask.jsonify(result) - result = {"status": "OK", "data": hub.get_props()} + data = hub.get_props() + data["perms"] = get_user_permissions(hub) + result = {"status": "OK", "data": data} return flask.jsonify(result) diff --git a/hubs/views/api/user.py b/hubs/views/api/user.py new file mode 100644 index 0000000..e05b3cf --- /dev/null +++ b/hubs/views/api/user.py @@ -0,0 +1,13 @@ +from __future__ import absolute_import, unicode_literals + +import flask + +from hubs.app import app +from hubs.utils.views import get_user_details, login_required + + +@app.route('/api/user/', methods=['GET']) +@login_required +def api_user(): + result = {"status": "OK", "data": get_user_details()} + return flask.jsonify(result) diff --git a/hubs/views/hub.py b/hubs/views/hub.py index f253d0d..c1e0a07 100644 --- a/hubs/views/hub.py +++ b/hubs/views/hub.py @@ -5,8 +5,7 @@ import hubs.models from hubs.app import app from hubs.utils.views import ( - get_hub, get_sse_url, get_menu_entries, require_hub_access, - get_user_permissions + get_hub, get_sse_url, require_hub_access, get_user_details, ) @@ -27,10 +26,10 @@ def hub(name): "hubConfig": flask.url_for("api_hub_config", name=hub.name), "hubConfigSuggestUsers": flask.url_for( "api_hub_config_suggest_users", name=hub.name), + "user": flask.url_for("api_user"), + "allGroups": flask.url_for("groups"), } - current_user = flask.g.auth.copy() - current_user["perms"] = get_user_permissions(hub) - menu = get_menu_entries() + current_user = get_user_details() flash_messages = [ {"msg": msg[1], "type": msg[0]} for msg in flask.get_flashed_messages(with_categories=True) @@ -42,7 +41,6 @@ def hub(name): ui=dict( page="Hub", flashMessages=flash_messages, - leftMenu=menu, ), globalConfig=global_config, urls=urls, diff --git a/hubs/views/user.py b/hubs/views/user.py index 86905a0..819dd3a 100644 --- a/hubs/views/user.py +++ b/hubs/views/user.py @@ -6,20 +6,20 @@ import hubs.feed from hubs.app import app from hubs.utils.views import ( - login_required, get_sse_url, get_menu_entries) + login_required, get_sse_url, get_user_details) @app.route('/stream') @app.route('/stream/') @login_required def stream(): - current_user = flask.g.auth.copy() + current_user = get_user_details() urls = { "sse": get_sse_url("user/{}".format(current_user["nickname"])), "notifications": flask.url_for("stream_existing"), "saved": flask.url_for("saved_notifs"), + "allGroups": flask.url_for("groups"), } - menu = get_menu_entries() flash_messages = [ {"msg": msg[1], "type": msg[0]} for msg in flask.get_flashed_messages(with_categories=True) @@ -31,7 +31,6 @@ def stream(): ui=dict( page="Streams", flashMessages=flash_messages, - leftMenu=menu, ), urls=urls, currentUser=current_user,