Unverified Commit 41484b64 authored by Paul Kaplan's avatar Paul Kaplan Committed by GitHub

Merge pull request #5176 from paulkaplan/studio-selectors

Add initial selectors for studio permissions
parents f4e95aba 8267dcf1
......@@ -14282,6 +14282,11 @@
"integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=",
"dev": true
},
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
},
"lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
const keyMirror = require('keymirror');
const defaults = require('lodash.defaults');
const get = require('lodash.get');
const {requestSession, requestSessionWithRetry} = require('../lib/session');
const messageCountActions = require('./message-count.js');
......@@ -118,3 +119,13 @@ module.exports.refreshSessionWithRetry = () => (dispatch => {
dispatch(module.exports.setSessionError(err));
});
});
// Selectors
module.exports.selectIsLoggedIn = state => get(state, ['session', 'session', 'user'], false);
module.exports.selectUsername = state => get(state, ['session', 'session', 'user', 'username'], null);
module.exports.selectToken = state => get(state, ['session', 'session', 'user', 'token'], null);
module.exports.selectIsAdmin = state => get(state, ['session', 'session', 'permissions', 'admin'], false);
module.exports.selectIsSocial = state => get(state, ['session', 'session', 'permissions', 'social'], false);
// NB logged out user id as NaN so that it can never be used in equality testing since NaN !== NaN
module.exports.selectUserId = state => get(state, ['session', 'session', 'user', 'id'], NaN);
......@@ -3,6 +3,8 @@ const keyMirror = require('keymirror');
const api = require('../lib/api');
const log = require('../lib/log');
const {selectUserId, selectIsAdmin, selectIsSocial} = require('./session');
const Status = keyMirror({
FETCHED: null,
NOT_FETCHED: null,
......@@ -18,6 +20,7 @@ const getInitialState = () => ({
commentingAllowed: false,
thumbnail: '',
followers: 0,
owner: null,
rolesStatus: Status.NOT_FETCHED,
manager: false,
......@@ -55,6 +58,8 @@ const studioReducer = (state, action) => {
}
};
// Action Creators
const setFetchStatus = (fetchType, fetchStatus, error) => ({
type: 'SET_FETCH_STATUS',
fetchType,
......@@ -72,6 +77,8 @@ const setRoles = roles => ({
roles: roles
});
// Thunks
const getInfo = studioId => (dispatch => {
dispatch(setFetchStatus('infoStatus', Status.FETCHING));
api({uri: `/studios/${studioId}`}, (err, body, res) => {
......@@ -86,7 +93,8 @@ const getInfo = studioId => (dispatch => {
openToAll: body.open_to_all,
commentingAllowed: body.commenting_allowed,
updated: new Date(body.history.modified),
followers: body.stats.followers
followers: body.stats.followers,
owner: body.owner
}));
});
});
......@@ -111,10 +119,34 @@ const getRoles = (studioId, username, token) => (dispatch => {
});
});
// Selectors
// Fine-grain selector helpers - not exported, use the higher level selectors below
const isCreator = state => selectUserId(state) === state.studio.owner;
const isCurator = state => state.studio.curator;
const isManager = state => state.studio.manager || isCreator(state);
// Action-based permissions selectors
const selectCanEditInfo = state => selectIsAdmin(state) || isManager(state);
const selectCanAddProjects = state =>
isManager(state) ||
isCurator(state) ||
(selectIsSocial(state) && state.studio.openToAll);
// This isn't "canComment" since they could be muted, but comment composer handles that
const selectShowCommentComposer = state => selectIsSocial(state);
module.exports = {
getInitialState,
studioReducer,
Status,
// Thunks
getInfo,
getRoles
getRoles,
// Selectors
selectCanEditInfo,
selectCanAddProjects,
selectShowCommentComposer
};
......@@ -9,6 +9,8 @@ import ComposeComment from '../preview/comment/compose-comment.jsx';
import TopLevelComment from '../preview/comment/top-level-comment.jsx';
import studioCommentActions from '../../redux/studio-comment-actions.js';
import {selectShowCommentComposer} from '../../redux/studio.js';
const StudioComments = ({
comments,
getTopLevelComments,
......@@ -81,9 +83,7 @@ export default connect(
comments: state.comments.comments,
moreCommentsToLoad: state.comments.moreCommentsToLoad,
replies: state.comments.replies,
// TODO permissions like this to a selector for testing
shouldShowCommentComposer: !!state.session.session.user // is logged in
shouldShowCommentComposer: selectShowCommentComposer(state)
}),
{
getTopLevelComments: studioCommentActions.getTopLevelComments,
......
......@@ -3,9 +3,11 @@ import PropTypes from 'prop-types';
import {useParams} from 'react-router-dom';
import {connect} from 'react-redux';
import Debug from './debug.jsx';
import {getInfo, getRoles} from '../../redux/studio';
const StudioInfo = ({username, studio, token, onLoadInfo, onLoadRoles}) => {
import {selectUsername, selectToken} from '../../redux/session';
import {getInfo, getRoles, selectCanEditInfo} from '../../redux/studio';
const StudioInfo = ({username, studio, token, canEditInfo, onLoadInfo, onLoadRoles}) => {
const {studioId} = useParams();
useEffect(() => { // Load studio info after first render
......@@ -23,11 +25,16 @@ const StudioInfo = ({username, studio, token, onLoadInfo, onLoadRoles}) => {
label="Studio Info"
data={studio}
/>
<Debug
label="Studio Info Permissions"
data={{canEditInfo}}
/>
</div>
);
};
StudioInfo.propTypes = {
canEditInfo: PropTypes.bool,
username: PropTypes.string,
token: PropTypes.string,
studio: PropTypes.shape({
......@@ -38,14 +45,12 @@ StudioInfo.propTypes = {
};
export default connect(
state => {
const user = state.session.session.user;
return {
studio: state.studio,
username: user && user.username,
token: user && user.token
};
},
state => ({
studio: state.studio,
username: selectUsername(state),
token: selectToken(state),
canEditInfo: selectCanEditInfo(state)
}),
dispatch => ({
onLoadInfo: studioId => dispatch(getInfo(studioId)),
onLoadRoles: (studioId, username, token) => dispatch(
......
......@@ -7,10 +7,11 @@ import {projectFetcher} from './lib/fetchers';
import {projects} from './lib/redux-modules';
import Debug from './debug.jsx';
const {actions, selector} = projects;
const {actions, selector: projectsSelector} = projects;
import {selectCanAddProjects} from '../../redux/studio';
const StudioProjects = ({
items, error, loading, moreToLoad, onLoadMore
canAddProjects, items, error, loading, moreToLoad, onLoadMore
}) => {
const {studioId} = useParams();
......@@ -27,6 +28,10 @@ const StudioProjects = ({
label="Error"
data={error}
/>}
<Debug
label="Project Permissions"
data={{canAddProjects}}
/>
<div>
{items.map((item, index) =>
(<Debug
......@@ -48,6 +53,7 @@ const StudioProjects = ({
};
StudioProjects.propTypes = {
canAddProjects: PropTypes.bool,
items: PropTypes.array, // eslint-disable-line react/forbid-prop-types
loading: PropTypes.bool,
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
......@@ -55,7 +61,10 @@ StudioProjects.propTypes = {
onLoadMore: PropTypes.func
};
const mapStateToProps = state => selector(state);
const mapStateToProps = state => ({
...projectsSelector(state),
canAddProjects: selectCanAddProjects(state)
});
const mapDispatchToProps = dispatch => ({
onLoadMore: (studioId, offset) => dispatch(
......
{
"studios": {
"isManager": {
"manager": true
},
"isCurator": {
"curator": true
},
"creator1": {
"owner": 1
},
"openToAll": {
"openToAll": true
}
},
"sessions": {
"user1Admin": {
"session": {
"user": {
"id": 1
},
"permissions": {
"admin": true
}
}
},
"user1Social": {
"session": {
"user": {
"id": 1
},
"permissions": {
"social": true
}
}
},
"user1": {
"session": {
"user": {
"id": 1,
"username": "user1-username",
"token": "user1-token"
},
"permissions": {
"admin": false,
"social": false
}
}
}
}
}
\ No newline at end of file
import {
getInitialState, selectIsAdmin, selectIsSocial, selectUserId,
selectUsername, selectToken, sessionReducer, setSession
} from '../../../src/redux/session';
import {sessions} from '../../helpers/state-fixtures.json';
describe('session selectors', () => {
test('logged out', () => {
const state = {session: getInitialState()};
expect(selectIsAdmin(state)).toBe(false);
expect(selectIsSocial(state)).toBe(false);
expect(selectUserId(state)).toBeNaN();
expect(selectToken(state)).toBeNull();
expect(selectUsername(state)).toBeNull();
});
test('user data', () => {
let state = {session: getInitialState()};
const newSession = sessions.user1.session;
state.session = sessionReducer(state.session, setSession(newSession));
expect(selectUserId(state)).toBe(1);
expect(selectUsername(state)).toBe('user1-username');
expect(selectToken(state)).toBe('user1-token');
});
describe('permissions', () => {
test('selectIsAdmin', () => {
let state = {session: getInitialState()};
const newSession = sessions.user1Admin.session;
state.session = sessionReducer(state.session, setSession(newSession));
expect(selectIsAdmin(state)).toBe(true);
// Confirm that admin/social are totally separate and just read directly from the state
expect(selectIsSocial(state)).toBe(false);
});
test('selectIsSocial', () => {
let state = {session: getInitialState()};
const newSession = sessions.user1Social.session;
state.session = sessionReducer(state.session, setSession(newSession));
expect(selectIsSocial(state)).toBe(true);
// Confirm that admin/social are totally separate and just read directly from the state
expect(selectIsAdmin(state)).toBe(false);
});
});
});
import {
getInitialState as getInitialStudioState,
selectCanEditInfo,
selectCanAddProjects,
selectShowCommentComposer
} from '../../../src/redux/studio';
import {
getInitialState as getInitialSessionState
} from '../../../src/redux/session';
import {sessions, studios} from '../../helpers/state-fixtures.json';
describe('studio selectors', () => {
let state;
beforeEach(() => {
state = {
session: getInitialSessionState(),
studio: getInitialStudioState()
};
});
describe('studio info', () => {
test('is editable by admin', () => {
state.session = sessions.user1Admin;
expect(selectCanEditInfo(state)).toBe(true);
});
test('is editable by managers and studio creator', () => {
state.studio = studios.isManager;
expect(selectCanEditInfo(state)).toBe(true);
state.studio = studios.creator1;
state.session = sessions.user1;
expect(selectCanEditInfo(state)).toBe(true);
});
test('is not editable by curators', () => {
state.studio = studios.isCurator;
state.session = sessions.user1;
expect(selectCanEditInfo(state)).toBe(false);
});
test('is not editable by other logged in users', () => {
state.session = sessions.user1;
expect(selectCanEditInfo(state)).toBe(false);
});
test('is not editable by logged out users', () => {
expect(selectCanEditInfo(state)).toBe(false);
});
});
describe('studio projects', () => {
test('cannot be added by admin', () => {
state.session = sessions.user1Admin;
expect(selectCanAddProjects(state)).toBe(false);
});
test('can be added by managers and studio creator', () => {
state.studio = studios.isManager;
expect(selectCanAddProjects(state)).toBe(true);
state.studio = studios.creator1;
state.session = sessions.user1;
expect(selectCanAddProjects(state)).toBe(true);
});
test('can be added by curators', () => {
state.studio = studios.isCurator;
state.session = sessions.user1;
expect(selectCanAddProjects(state)).toBe(true);
});
test('can be added by social users if studio is openToAll', () => {
state.studio = studios.openToAll;
state.session = sessions.user1Social;
expect(selectCanAddProjects(state)).toBe(true);
});
test('cannot be added by social users if not openToAll', () => {
state.session = sessions.user1Social;
expect(selectCanAddProjects(state)).toBe(false);
});
});
describe('studio comments', () => {
test('show comment composer only for social users', () => {
expect(selectShowCommentComposer(state)).toBe(false);
state.session = sessions.user1;
expect(selectShowCommentComposer(state)).toBe(false);
state.session = sessions.user1Social;
expect(selectShowCommentComposer(state)).toBe(true);
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment