fix unsafe roles being usable, begin work on role/server editor

This commit is contained in:
Katalina / stardust 2017-12-24 03:22:41 -06:00
parent d1f556b0f0
commit bd15a812e5
20 changed files with 343 additions and 92 deletions

View file

@ -1,10 +1,14 @@
module.exports = (R, $) => {
R.get('/api/servers', async (ctx) => {
const { userId } = ctx.session
const srv = $.discord.getRelevantServers(userId)
const presentable = await $.P.oldPresentableServers(srv, userId)
try {
const { userId } = ctx.session
const srv = $.discord.getRelevantServers(userId)
const presentable = await $.P.oldPresentableServers(srv, userId)
ctx.body = presentable
ctx.body = presentable
} catch (e) {
console.error(e.trace)
}
})
R.get('/api/server/:id', async (ctx) => {
@ -33,15 +37,13 @@ module.exports = (R, $) => {
const { added, removed } = ctx.request.body
if (added.length > 0) {
gm = await gm.addRoles(added)
gm = await gm.addRoles(added.filter(r => $.discord.safeRole(server, r)))
}
if (removed.length > 0) {
gm = await gm.removeRoles(removed)
gm = await gm.removeRoles(removed.filter(r => $.discord.safeRole(server, r)))
}
console.log(gm.roles)
ctx.body = { ok: true }
})
}

View file

@ -15,7 +15,10 @@ class DiscordService extends Service {
this.client = new discord.Client()
this.startBot()
}
ownGm (server) {
return this.gm(server, this.client.user.id)
}
async startBot () {
@ -45,6 +48,11 @@ class DiscordService extends Service {
}
}
safeRole (server, role) {
const r = this.getRoles(server).get(role)
return r.editable && !r.hasPermission('MANAGE_ROLES', false, true)
}
// oauth step 2 flow, grab the auth token via code
async getAuthToken (code) {
const url = 'https://discordapp.com/api/oauth2/token'

View file

@ -42,13 +42,14 @@ class PresentationService extends Service {
async rolesByServer (server) {
return server.roles
.filter(r => r.id !== server.id) // get rid of @everyone
.map(r => ({
id: r.id,
color: r.color,
name: r.name,
position: r.position
}))
.filter(r => r.id !== server.id) // get rid of @everyone
.map(r => ({
id: r.id,
color: r.color,
name: r.name,
position: r.position,
safe: this.discord.safeRole(server.id, r.id)
}))
}
}

View file

@ -11,12 +11,15 @@
"prop-types": "^15.6.0",
"react": "^16.2.0",
"react-custom-scrollbars": "^4.2.1",
"react-dnd": "^2.5.4",
"react-dnd-html5-backend": "^2.5.4",
"react-dom": "^16.2.0",
"react-immutable-proptypes": "^2.1.0",
"react-redux": "^5.0.6",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",
"react-router-redux": "^5.0.0-alpha.8",
"react-transition-group": "^2.2.1",
"redux": "^3.7.2",
"redux-devtools": "^3.4.1",
"redux-devtools-dock-monitor": "^1.1.2",

13
UI/src/actions/ui.js Normal file
View file

@ -0,0 +1,13 @@
export const fadeOut = cb => dispatch => {
dispatch({
type: Symbol.for('app fade'),
data: true
})
setTimeout(cb, 300)
}
export const fadeIn = {
type: Symbol.for('app fade'),
data: false
}

View file

@ -0,0 +1,21 @@
import React, { Component } from 'react'
import Role from '../role'
class Category extends Component {
render () {
const { category, name } = this.props
return <div key={name} className="role-picker__category">
<h4>{ name }</h4>
{
category.get('roles_map')
.sortBy(r => r.get('position'))
.reverse()
.map((r, k) => <Role key={k} role={r} type='drag' />)
.toArray()
}
</div>
}
}
export default Category

View file

@ -0,0 +1,6 @@
.role-editor
&__grid
display: grid
grid-template-areas: 'left right'
grid-template-columns: 1fr
grid-template-rows: 1fr 1fr

View file

View file

@ -0,0 +1,62 @@
import React, { Component } from 'react'
import { Set } from 'immutable'
import { connect } from 'react-redux'
import * as Actions from './actions'
import * as PickerActions from '../role-picker/actions'
import * as UIActions from '../../actions/ui'
import './RoleEditor.sass'
import Category from './Category'
import Role from '../role'
import { Scrollbars } from 'react-custom-scrollbars';
const mapState = ({ rolePicker, roleEditor, servers }, ownProps) => ({
rp: rolePicker,
editor: roleEditor,
server: servers.get(ownProps.match.params.server)
})
@connect(mapState)
class RoleEditor extends Component {
componentWillMount () {
const { dispatch, match: { params: { server } } } = this.props
dispatch(PickerActions.setup(server))
}
componentWillReceiveProps (nextProps) {
if (this.props.match.params.server !== nextProps.match.params.server) {
const { dispatch } = this.props
dispatch(UIActions.fadeOut(() => dispatch(PickerActions.setup(nextProps.match.params.server))))
}
}
render () {
const vm = this.props.rp.get('viewMap')
console.log(vm.toJS())
return <div className="inner role-editor">
<div className="role-editor__grid">
<div className="role-editor__grid__left">
<Scrollbars autoHeight autoHeightMax='calc(100vh - 110px)'>
{
vm
.filter((_, k) => k !== 'Unassigned')
.map((c, name) => <Category key={name} name={name} category={c} />)
.toArray()
}
</Scrollbars>
</div>
<div className="role-editor__grid__right">
{
(vm.getIn(['Uncategorized', 'roles_map']) || Set())
.sortBy(r => r.get('position'))
.reverse()
.map((r, k) => <Role key={k} role={r} type='drag' />)
.toArray()
}
</div>
</div>
</div>
}
}
export default RoleEditor

View file

@ -32,10 +32,14 @@ class Category extends Component {
return <div key={name} className="role-picker__category">
<h4>{ name }</h4>
{
category.get('roles_map').map((r, k) => {
category.get('roles_map')
.sortBy(r => r.get('position'))
.reverse()
.map((r, k) => {
const id = r.get('id')
return <Role key={k} role={r} selected={isSelected(id)} onToggle={this.onRoleToggle(id)}/>
}).toArray()
return <Role key={k} role={r} disabled={!r.get('safe')} selected={isSelected(id)} onToggle={this.onRoleToggle(id)}/>
})
.toArray()
}
</div>
}

View file

@ -17,10 +17,25 @@
background-color: var(--c-1)
padding: 15px
margin: 10px
width: 220px - 30px
min-width: 220px - 30px
&__roles-header
&__header
display: flex
align-items: center
justify-content: left
color: var(--c-7)
svg
transition: transform 0.05s ease-in-out
transform: translateZ(0)
&:hover
transform: translateZ(0) scale(1.1)
color: var(--c-9)
h3
margin: 0
margin-right: 10px
&__spacer
flex: 1

View file

@ -1,6 +1,6 @@
import { Map, Set, fromJS } from 'immutable'
import superagent from 'superagent'
import * as UIActions from '../../actions/ui'
export const setup = id => async dispatch => {
// const rsp = await superagent.get(`/api/server/${id}`)
@ -22,7 +22,7 @@ export const constructView = id => (dispatch, getState) => {
const categories = server.get('categories')
const allRoles = server.get('roles').map(r => r.get('id')).toSet()
const allRoles = server.get('roles').filter(v => v.get('safe')).map(r => r.get('id')).toSet()
const accountedRoles = categories.map(c => c.get('roles')).toSet().flatten()
const unaccountedRoles = allRoles.subtract(accountedRoles)
@ -53,6 +53,8 @@ export const constructView = id => (dispatch, getState) => {
hidden: false
}
})
dispatch(UIActions.fadeIn)
}
export const resetSelected = (dispatch) => {

View file

@ -2,10 +2,12 @@ import React, { Component, Fragment } from 'react'
import { connect } from 'react-redux'
import superagent from 'superagent'
import * as Actions from './actions'
import * as UIActions from '../../actions/ui'
import './RolePicker.sass'
import Category from './Category'
import { Scrollbars } from 'react-custom-scrollbars';
import { Link } from 'react-router-dom';
const mapState = ({ rolePicker, servers }, ownProps) => {
return {
@ -24,7 +26,7 @@ class RolePicker extends Component {
componentWillReceiveProps (nextProps) {
if (this.props.match.params.server !== nextProps.match.params.server) {
const { dispatch } = this.props
dispatch(Actions.setup(nextProps.match.params.server))
dispatch(UIActions.fadeOut(() => dispatch(Actions.setup(nextProps.match.params.server))))
}
}
@ -37,6 +39,29 @@ class RolePicker extends Component {
return !data.get('rolesSelected').equals(data.get('originalRolesSelected'))
}
renderServerMessage (server) {
const roleManager = server.getIn(['perms', 'canManageRoles'])
const msg = server.get('message')
if (!roleManager && msg !== '') {
return <section>
<h3>Server Message</h3>
<p>{msg}</p>
</section>
}
if (roleManager) {
return <section>
<div className="role-picker__header">
<h3>Server Message</h3>
<Link to={`/s/${server.get('id')}/edit`} uk-tooltip='' title='Edit' uk-icon="icon: file-edit"></Link>
</div>
<p>{msg || <i>no server message</i>}</p>
</section>
}
return null
}
render () {
const { data, server, dispatch } = this.props
const vm = data.get('viewMap')
@ -46,16 +71,9 @@ class RolePicker extends Component {
}
return <div className={`inner role-picker ${(data.get('hidden')) ? 'hidden' : ''}`}>
{/* <Scrollbars> */}
{ (server.get('message') !== '')
? <section>
<h3>Server Message</h3>
<p>{server.get('message')}</p>
</section>
: null
}
{ this.renderServerMessage(server) }
<section>
<div className="role-picker__roles-header">
<div className="role-picker__header">
<h3>Roles</h3>
<div className="role-picker__spacer"></div>
<div className={`role-picker__actions ${(!this.rolesHaveChanged) ? 'hidden' : ''}`}>
@ -69,12 +87,10 @@ class RolePicker extends Component {
</div>
<div className="role-picker__categories">
{
vm.map((c, name) => <Category name={name} category={c} isSelected={this.isSelected} onChange={(roles) => dispatch(Actions.updateRoles(roles))} />).toArray()
vm.map((c, name) => <Category key={name} name={name} category={c} isSelected={this.isSelected} onChange={(roles) => dispatch(Actions.updateRoles(roles))} />).toArray()
}
</div>
</section>
{/* </Scrollbars> */}
</div>
}
}

View file

@ -11,42 +11,32 @@
justify-content: center
font-size: 16px
vertical-align: baseline
transition: border-color 0.2s ease-in-out
transition: background-color 0.15s ease-in-out, transform 0.15s ease-in-out, box-shadow 0.15s ease-in-out
transform: translateZ(0)
&:hover
&.role__button
.role__option
transform: translateY(-1px) translateZ(0px)
box-shadow: 0 1px 1px var(--c-dark)
border-color: var(--role-color-hover)
background-color: transparent
&:active
&:hover
cursor: inherit
&:hover:not(.disabled)
.role__option
transform: translateY(-1px) translateZ(0px)
box-shadow: 0 1px 1px var(--c-dark)
border-color: var(--role-color-hover)
background-color: transparent
&:active
box-shadow: none
&:active .role__option
box-shadow: none
&:active .role__option
box-shadow: none
&__option
border-radius: 50%
height: 22px
margin-right: 6px
width: 22px
box-sizing: border-box
/* display: inline-block */
background-color: transparent
overflow: hidden
transform: translateZ(0px)
border: 1px solid var(--role-color-hex)
transition: background-color 0.1s ease-in-out, border-left-width 0.3s ease-in-out, border-color 0.1s ease-in-out, transform 0.1s ease-in-out, box-shadow 0.1s ease-out
&.selected
// background-color: var(--role-color-hex)
// This **must** be width-1, otherwise blink adds width to the boundaries.
border-left-width: 21px
&:hover
cursor: pointer
&.disabled
.role__option
border-color: var(--c-7)
transition: none
transform: translateZ(0)
&:hover
cursor: default
&__name
@ -57,5 +47,33 @@
white-space: nowrap
user-select: none
&__option
border-radius: 50%
height: 22px
margin-right: 6px
width: 22px
box-sizing: border-box
/* display: inline-block */
background-color: transparent
overflow: hidden
transform: translateZ(0)
border: 1px solid var(--role-color-hex)
transition: background-color 0.1s ease-in-out, border-left-width 0.3s ease-in-out, border-color 0.1s ease-in-out, transform 0.1s ease-in-out, box-shadow 0.1s ease-out
&.selected
// This **must** be width-1, otherwise blink adds width to the boundaries.
border-left-width: 21px
&.role__drag
box-shadow: none
&:hover
transform: translateZ(0) translateY(-1px)
box-shadow: 0 1px 2px rgba(0,0,0,0.3)
background-color: rgba(#ccc,0.03)
cursor: grab
&:active
cursor: grabbing
.role__option:active, .role:active .role__option:not(:active)
transform: translateY(0) translateZ(0px) !important

View file

@ -10,11 +10,13 @@ class Role extends Component {
role: PropTypes.object.isRequired,
onToggle: PropTypes.func,
type: PropTypes.string,
selected: PropTypes.bool.isRequired
selected: PropTypes.bool,
disabled: PropTypes.bool
}
render () {
let { role, selected } = this.props
let { role, selected, disabled, type, provided } = this.props
type = type || 'button'
let color = Color(role.get('color'))
@ -26,8 +28,13 @@ class Role extends Component {
let hc = color.lighten(0.1)
return <div
onClick={this.props.onToggle.bind(null, !selected, selected)}
className='role font-sans-serif'
onClick={() => {
if (!disabled && this.props.onToggle != null) {
this.props.onToggle(!selected, selected) }
}
}
{...((disabled) ? { 'uk-tooltip': '', title: "I don't have permissions to grant this." } : {})}
className={`role font-sans-serif ${(disabled) ? 'disabled' : ''} role__${type}`}
style={{
'--role-color-hex': c.string(),
'--role-color-hover': hc.string(),

View file

@ -25,8 +25,8 @@
&__icon img
border-radius: 100%
width: 50px
height: 50px
max-width: 50px
max-height: 50px
border: 2px solid transparent
&__info

View file

@ -1,19 +1,22 @@
import React, { Component } from 'react'
import { Route } from 'react-router-dom'
import { CSSTransition, TransitionGroup } from 'react-transition-group'
import { Scrollbars } from 'react-custom-scrollbars'
import { connect } from 'react-redux'
import { withRouter } from 'react-router'
import './index.sass'
import Navigation from './Navigation'
import RolePicker from '../role-picker'
import { withRouter } from 'react-router';
import RoleEditor from '../role-editor'
// import mockData from './mockData'
const mapState = ({ servers, user }) => {
const mapState = ({ servers, user, appState }) => {
return {
servers,
user
user,
fade: appState.fade
}
}
@ -22,12 +25,12 @@ class Servers extends Component {
render () {
return <div className="servers">
<Navigation className="servers__nav" servers={this.props.servers} user={this.props.user} />
<div className="servers__content">
<Scrollbars autoHeight autoHeightMax='calc(100vh - 80px)'>
<Route path='/s/:server' component={RolePicker} />
<Route path='/s/:server/edit' component={RolePicker} />
</Scrollbars>
</div>
<div className='servers__content'>
<Scrollbars className={`fade-element ${(this.props.fade) ? 'fade' : ''}`} autoHeight autoHeightMax='calc(100vh - 80px)'>
<Route path='/s/:server' component={RolePicker} exact />
</Scrollbars>
<Route path='/s/:server/edit' component={RoleEditor} />
</div>
</div>
}
}

View file

@ -51,3 +51,12 @@ h1,h2,h3,h4,h5,h6 {
.uk-navbar-nav>li>a {
color: var(--c-7);
}
.fade-element {
opacity: 1;
transition: opacity 0.3s ease-in-out;
}
.fade {
opacity: 0;
}

View file

@ -3,18 +3,26 @@ import { combineReducers } from 'redux'
import servers from './servers'
import user from './user'
import rolePicker from './role-picker'
import { routerMiddleware } from 'react-router-redux';
import { routerMiddleware } from 'react-router-redux'
// import roles from './roles'
const initialState = {
ready: false
ready: false,
fade: true
}
const appState = (state = initialState, { type }) => {
const appState = (state = initialState, { type, data }) => {
switch (type) {
case Symbol.for('app ready'):
return {
ready: true
ready: true,
fade: false
}
case Symbol.for('app fade'):
return {
...state,
fade: data
}
default:

View file

@ -263,7 +263,7 @@ arrify@^1.0.0, arrify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
asap@~2.0.3:
asap@^2.0.6, asap@~2.0.3:
version "2.0.6"
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
@ -1542,6 +1542,10 @@ center-align@^0.1.1:
align-text "^0.1.3"
lazy-cache "^1.0.3"
chain-function@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.0.tgz#0d4ab37e7e18ead0bdc47b920764118ce58733dc"
chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
@ -1600,6 +1604,10 @@ clap@^1.0.9:
dependencies:
chalk "^1.1.3"
classnames@^2.2.5:
version "2.2.5"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
clean-css@4.1.x:
version "4.1.9"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.1.9.tgz#35cee8ae7687a49b98034f70de00c4edd3826301"
@ -2272,6 +2280,19 @@ diffie-hellman@^5.0.0:
miller-rabin "^4.0.0"
randombytes "^2.0.0"
disposables@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/disposables/-/disposables-1.0.1.tgz#064727a25b54f502bd82b89aa2dfb8df9f1b39e3"
dnd-core@^2.5.4:
version "2.5.4"
resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-2.5.4.tgz#0c70a8dcbb609c0b222e275fcae9fa83e5897397"
dependencies:
asap "^2.0.6"
invariant "^2.0.0"
lodash "^4.2.0"
redux "^3.7.1"
dns-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d"
@ -2316,6 +2337,10 @@ dom-css@^2.0.0:
prefix-style "2.0.1"
to-camel-case "1.0.0"
dom-helpers@^3.2.0:
version "3.3.1"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6"
dom-serializer@0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
@ -3574,7 +3599,7 @@ hoek@4.x.x:
version "4.2.0"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d"
hoist-non-react-statics@^2.2.1, hoist-non-react-statics@^2.3.0:
hoist-non-react-statics@^2.1.0, hoist-non-react-statics@^2.2.1, hoist-non-react-statics@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0"
@ -3843,7 +3868,7 @@ interpret@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
invariant@^2.0.0, invariant@^2.2.1, invariant@^2.2.2:
invariant@^2.0.0, invariant@^2.1.0, invariant@^2.2.1, invariant@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
dependencies:
@ -6059,6 +6084,23 @@ react-dev-utils@^3.1.0:
strip-ansi "3.0.1"
text-table "0.2.0"
react-dnd-html5-backend@^2.5.4:
version "2.5.4"
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-2.5.4.tgz#974ad083f67b12d56977a5b171f5ffeb29d78352"
dependencies:
lodash "^4.2.0"
react-dnd@^2.5.4:
version "2.5.4"
resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-2.5.4.tgz#0b6dc5e9d0dfc2909f4f4fe736e5534f3afd1bd9"
dependencies:
disposables "^1.0.1"
dnd-core "^2.5.4"
hoist-non-react-statics "^2.1.0"
invariant "^2.1.0"
lodash "^4.2.0"
prop-types "^15.5.10"
react-dock@^0.2.4:
version "0.2.4"
resolved "https://registry.yarnpkg.com/react-dock/-/react-dock-0.2.4.tgz#e727dc7550b3b73116635dcb9c0e04d0b7afe17c"
@ -6144,6 +6186,17 @@ react-router@^4.2.0:
prop-types "^15.5.4"
warning "^3.0.0"
react-transition-group@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.2.1.tgz#e9fb677b79e6455fd391b03823afe84849df4a10"
dependencies:
chain-function "^1.0.0"
classnames "^2.2.5"
dom-helpers "^3.2.0"
loose-envify "^1.3.1"
prop-types "^15.5.8"
warning "^3.0.0"
react@^16.2.0:
version "16.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba"
@ -6302,7 +6355,7 @@ redux-thunk@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.2.0.tgz#e615a16e16b47a19a515766133d1e3e99b7852e5"
redux@^3.7.2:
redux@^3.7.1, redux@^3.7.2:
version "3.7.2"
resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b"
dependencies: