add role diff calculations, add action buttons, fix a few regressions

This commit is contained in:
Katalina / stardust 2017-12-23 02:21:31 -06:00
parent 3d541ac480
commit 3c545bdeaa
13 changed files with 203 additions and 89 deletions

View file

@ -8,7 +8,13 @@ class ServerService extends Service {
} }
async ensure (server) { async ensure (server) {
const srv = await this.get(server.id) let srv
try {
srv = await this.get(server.id)
} catch (e) {
}
if (srv == null) { if (srv == null) {
return this.create({ return this.create({
id: server.id, id: server.id,

View file

@ -10,6 +10,7 @@
"immutable": "^3.8.2", "immutable": "^3.8.2",
"prop-types": "^15.6.0", "prop-types": "^15.6.0",
"react": "^16.2.0", "react": "^16.2.0",
"react-custom-scrollbars": "^4.2.1",
"react-dom": "^16.2.0", "react-dom": "^16.2.0",
"react-immutable-proptypes": "^2.1.0", "react-immutable-proptypes": "^2.1.0",
"react-redux": "^5.0.6", "react-redux": "^5.0.6",

View file

@ -4,8 +4,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" sizes="any" href="%PUBLIC_URL%/favicon.png"/> <link rel="icon" type="image/png" sizes="any" href="%PUBLIC_URL%/favicon.png"/>
<title>Roleypoly</title> <title>Roleypoly</title>
<script src="https://use.typekit.net/bck0pci.js"></script> <!-- <script src="https://use.typekit.net/bck0pci.js"></script>
<script>try{Typekit.load({ async: true });}catch(e){}</script> <script>try{Typekit.load({ async: true });}catch(e){}</script> -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.35/css/uikit.min.css" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.35/css/uikit.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.35/js/uikit.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.35/js/uikit.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.35/js/uikit-icons.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.35/js/uikit-icons.min.js"></script>

View file

@ -11,7 +11,6 @@
flex-wrap: wrap flex-wrap: wrap
flex-direction: row flex-direction: row
&__category &__category
// flex: 1 3 33% // flex: 1 3 33%
box-sizing: border-box box-sizing: border-box
@ -19,3 +18,56 @@
padding: 15px padding: 15px
margin: 10px margin: 10px
width: 220px - 30px width: 220px - 30px
&__roles-header
display: flex
&__spacer
flex: 1
&__actions
opacity: 1
transition: opacity 0.3s ease-in-out
button
margin-left: 5px
&.hidden
opacity: 0
// display: none
.action__button
border: 0
border-radius: 5px
transition: transform 0.2s ease-out, box-shadow 0.2s ease-out
position: relative
&::after
content: ""
position: absolute
top: 0
bottom: 0
right: 0
left: 0
background-color: rgba(0,0,0,0.1)
border-radius: 5px
opacity: 0
transition: opacity 0.15s ease-in-out
&:hover
transform: translateY(-1px)
box-shadow: 0 1px 2px rgba(0,0,0,0.3)
&::after
opacity: 0.7
&:active
transform: translateY(0px)
box-shadow: none
&::after
opacity: 1
&.primary
background-color: var(--c-5)
&.secondary
background-color: var(--c-3)

View file

@ -42,7 +42,11 @@ export const constructView = id => (dispatch, getState) => {
hidden: false, hidden: false,
type: 'multi' type: 'multi'
})).map(c => { })).map(c => {
const roles = c.get('roles').map(r => server.get('roles').find(sr => sr.get('id') === r)) const roles = c.get('roles')
.map(r =>
server.get('roles').find(sr => sr.get('id') === r)
)
.sort((a, b) => a.position > b.position)
return c.set('roles_map', roles) return c.set('roles_map', roles)
}) })
@ -59,3 +63,30 @@ export const constructView = id => (dispatch, getState) => {
} }
}) })
} }
export const resetSelected = (dispatch) => {
dispatch({
type: Symbol.for('reset selected')
})
}
export const submitSelected = serverId => async (dispatch, getState) => {
const { rolePicker } = getState()
const original = rolePicker.get('originalRolesSelected')
const current = rolePicker.get('rolesSelected')
const diff = original.reduce((acc, v, k) => {
if (current.get(k) !== v) {
// if original value is false, then we know we're adding, otherwise removing.
if (v !== true) {
return acc.set('added', acc.get('added').add(k))
} else {
return acc.set('removed', acc.get('removed').add(k))
}
}
return acc
}, Map({ added: Set(), removed: Set() }))
await superagent.patch(`/api/servers/${serverId}/roles`).send(diff.toJS())
}

View file

@ -1,10 +1,11 @@
import React, { Component } from 'react' import React, { Component, Fragment } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import superagent from 'superagent' import superagent from 'superagent'
import * as Actions from './actions' import * as Actions from './actions'
import './RolePicker.sass' import './RolePicker.sass'
import Role from '../role' import Role from '../role'
import { Scrollbars } from 'react-custom-scrollbars';
const mapState = ({ rolePicker, servers }, ownProps) => { const mapState = ({ rolePicker, servers }, ownProps) => {
return { return {
@ -31,15 +32,21 @@ class RolePicker extends Component {
return this.props.data.getIn([ 'rolesSelected', id ]) return this.props.data.getIn([ 'rolesSelected', id ])
} }
get rolesHaveChanged () {
const { data } = this.props
return !data.get('rolesSelected').equals(data.get('originalRolesSelected'))
}
render () { render () {
const { data, server } = this.props const { data, server, dispatch } = this.props
const vm = data.get('viewMap') const vm = data.get('viewMap')
if (server === undefined) { if (server === undefined) {
return null return null
} }
return <div className={`role-picker ${(data.get('hidden')) ? 'hidden' : ''}`}> return <div className={`inner role-picker ${(data.get('hidden')) ? 'hidden' : ''}`}>
{/* <Scrollbars> */}
{ (server.get('message') !== '') { (server.get('message') !== '')
? <section> ? <section>
<h3>Server Message</h3> <h3>Server Message</h3>
@ -48,7 +55,18 @@ class RolePicker extends Component {
: null : null
} }
<section> <section>
<h3>Roles</h3> <div className="role-picker__roles-header">
<h3>Roles</h3>
<div className="role-picker__spacer"></div>
<div className={`role-picker__actions ${(!this.rolesHaveChanged) ? 'hidden' : ''}`}>
<button disabled={!this.rolesHaveChanged} onClick={() => dispatch(Actions.resetSelected)} className="uk-button action__button secondary">
Reset
</button>
<button disabled={!this.rolesHaveChanged} onClick={() => dispatch(Actions.submitSelected(server.id))} className="uk-button action__button primary">
Save Changes
</button>
</div>
</div>
<div className="role-picker__categories"> <div className="role-picker__categories">
{ {
vm.map((c, name) => { vm.map((c, name) => {
@ -68,6 +86,8 @@ class RolePicker extends Component {
} }
</div> </div>
</section> </section>
{/* </Scrollbars> */}
</div> </div>
} }
} }

View file

@ -15,7 +15,7 @@
&:hover &:hover
.role__option .role__option
transform: translateY(-1px) transform: translateY(-1px) translateZ(0px)
box-shadow: 0 1px 1px var(--c-dark) box-shadow: 0 1px 1px var(--c-dark)
border-color: var(--role-color-hover) border-color: var(--role-color-hover)
background-color: transparent background-color: transparent
@ -35,11 +35,12 @@
/* display: inline-block */ /* display: inline-block */
background-color: transparent background-color: transparent
overflow: hidden overflow: hidden
transform: translateZ(0px)
border: 1px solid var(--role-color-hex) border: 1px solid var(--role-color-hex)
transition: background-color 0.1s ease-in-out, border-left-width 0.3s ease-in-out, border-right-width 0.5s ease-in-out, border-color 0.1s ease-in-out, transform 0.1s ease-in-out, box-shadow 0.1s ease-out 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 &.selected
background-color: var(--role-color-hex) // background-color: var(--role-color-hex)
// This **must** be width-1, otherwise blink adds width to the boundaries. // This **must** be width-1, otherwise blink adds width to the boundaries.
border-left-width: 21px border-left-width: 21px
@ -57,4 +58,4 @@
user-select: none user-select: none
.role__option:active, .role:active .role__option:not(:active) .role__option:active, .role:active .role__option:not(:active)
transform: translateY(0) !important transform: translateY(0) translateZ(0px) !important

View file

@ -3,6 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import ServerCard from './ServerCard' import ServerCard from './ServerCard'
import UserCard from './UserCard' import UserCard from './UserCard'
import { Scrollbars } from 'react-custom-scrollbars';
class ServersNavigation extends Component { class ServersNavigation extends Component {
static propTypes = { static propTypes = {
@ -16,12 +17,14 @@ class ServersNavigation extends Component {
return <Fragment> return <Fragment>
<UserCard user={this.props.user} /> <UserCard user={this.props.user} />
<div className={this.props.className}> <div className={this.props.className}>
{ <Scrollbars autoHeight autoHeightMax='calc(100vh - 180px)'>
this.props.servers.reduce((acc, s, i) => { {
acc.push(<ServerCard server={s} user={this.props.user} key={i} />) this.props.servers.reduce((acc, s, i) => {
return acc acc.push(<ServerCard server={s} user={this.props.user} key={i} />)
}, []) return acc
} }, [])
}
</Scrollbars>
</div> </div>
</Fragment> </Fragment>
} }

View file

@ -1,5 +1,6 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import { Route } from 'react-router-dom' import { Route } from 'react-router-dom'
import { Scrollbars } from 'react-custom-scrollbars'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import './index.sass' import './index.sass'
@ -21,10 +22,12 @@ class Servers extends Component {
render () { render () {
return <div className="servers"> return <div className="servers">
<Navigation className="servers__nav" servers={this.props.servers} user={this.props.user} /> <Navigation className="servers__nav" servers={this.props.servers} user={this.props.user} />
<div className="servers__content"> <div className="servers__content">
<Route path='/s/:server' component={RolePicker} /> <Scrollbars autoHeight autoHeightMax='calc(100vh - 80px)'>
<Route path='/s/:server/edit' component={RolePicker} /> <Route path='/s/:server' component={RolePicker} />
</div> <Route path='/s/:server/edit' component={RolePicker} />
</Scrollbars>
</div>
</div> </div>
} }
} }

View file

@ -10,13 +10,17 @@
&__nav &__nav
grid-area: listing grid-area: listing
overflow-y: scroll overflow: hidden
height: $fullH // height: $fullH
&__content &__content
grid-area: content
background-color: var(--c-3) background-color: var(--c-3)
padding: 15px // padding: 15px
overflow-y: scroll grid-area: content
position: relative
// height: $fullH
overflow: hidden
box-sizing: border-box box-sizing: border-box
.inner
padding: 15px

View file

@ -14,20 +14,17 @@ export default (state = initialState, { type, data }) => {
return Map(data) return Map(data)
case Symbol.for('hide role picker ui'): case Symbol.for('hide role picker ui'):
return { return state.set('hidden', data)
...state,
hidden: data
}
case Symbol.for('reset role picker ui'): case Symbol.for('reset role picker ui'):
return { return state.set('emptyRoles', data)
...state,
emptyRoles: data
}
case Symbol.for('update selected roles'): case Symbol.for('update selected roles'):
return state.setIn(['rolesSelected', data.id], data.state) return state.setIn(['rolesSelected', data.id], data.state)
case Symbol.for('reset selected'):
return state.set('rolesSelected', state.get('originalRolesSelected'))
// case Symbol.for('zero role picker'): // case Symbol.for('zero role picker'):
// return initialState // return initialState

View file

@ -1,50 +0,0 @@
import { observable, computed } from 'mobx'
class Store {
@observable servers = [
{
"id": "203493697696956418",
"gm": {
"nickname": "sexkittenhime",
"color": "#ff5c00"
},
"server": {
"id": "203493697696956418",
"name": "Genudine Medkit Manufacturing",
"ownerID": "62601275618889728",
"icon": "ff08d36f5aee1ff48f8377b65d031ab0"
},
"perms": {
"isAdmin": true,
"canManageRoles": true
}
},
{
"id": "386659935687147521",
"gm": {
"nickname": null,
"color": "#cca1a1"
},
"server": {
"id": "386659935687147521",
"name": "Roleypoly",
"ownerID": "62601275618889728",
"icon": "4fa0c1063649a739f3fe1a0589aa2c03"
},
"perms": {
"isAdmin": true,
"canManageRoles": true
}
}
]
@observable user = {
username: 'あたし',
discriminator: '0001',
id: '',
avatar: null
}
}
export default Store

View file

@ -64,6 +64,10 @@ acorn@^5.0.0, acorn@^5.2.1:
version "5.2.1" version "5.2.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.2.1.tgz#317ac7821826c22c702d66189ab8359675f135d7" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.2.1.tgz#317ac7821826c22c702d66189ab8359675f135d7"
add-px-to-style@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/add-px-to-style/-/add-px-to-style-1.0.0.tgz#d0c135441fa8014a8137904531096f67f28f263a"
address@1.0.2: address@1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/address/-/address-1.0.2.tgz#480081e82b587ba319459fef512f516fe03d58af" resolved "https://registry.yarnpkg.com/address/-/address-1.0.2.tgz#480081e82b587ba319459fef512f516fe03d58af"
@ -2304,6 +2308,14 @@ dom-converter@~0.1:
dependencies: dependencies:
utila "~0.3" utila "~0.3"
dom-css@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/dom-css/-/dom-css-2.1.0.tgz#fdbc2d5a015d0a3e1872e11472bbd0e7b9e6a202"
dependencies:
add-px-to-style "1.0.0"
prefix-style "2.0.1"
to-camel-case "1.0.0"
dom-serializer@0: dom-serializer@0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
@ -5812,6 +5824,10 @@ postcss@^6.0.1, postcss@^6.0.2, postcss@^6.0.6:
source-map "^0.6.1" source-map "^0.6.1"
supports-color "^4.4.0" supports-color "^4.4.0"
prefix-style@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/prefix-style/-/prefix-style-2.0.1.tgz#66bba9a870cfda308a5dc20e85e9120932c95a06"
prelude-ls@~1.1.2: prelude-ls@~1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@ -5954,6 +5970,12 @@ querystringify@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-1.0.0.tgz#6286242112c5b712fa654e526652bf6a13ff05cb" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-1.0.0.tgz#6286242112c5b712fa654e526652bf6a13ff05cb"
raf@^3.1.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575"
dependencies:
performance-now "^2.1.0"
randomatic@^1.1.3: randomatic@^1.1.3:
version "1.1.7" version "1.1.7"
resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c"
@ -6005,6 +6027,14 @@ react-base16-styling@^0.5.1:
lodash.flow "^3.3.0" lodash.flow "^3.3.0"
pure-color "^1.2.0" pure-color "^1.2.0"
react-custom-scrollbars@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/react-custom-scrollbars/-/react-custom-scrollbars-4.2.1.tgz#830fd9502927e97e8a78c2086813899b2a8b66db"
dependencies:
dom-css "^2.0.0"
prop-types "^15.5.10"
raf "^3.1.0"
react-dev-utils@^3.1.0: react-dev-utils@^3.1.0:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-3.1.1.tgz#09ae7209a81384248db56547e718e65bd3b20eb5" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-3.1.1.tgz#09ae7209a81384248db56547e718e65bd3b20eb5"
@ -7233,6 +7263,12 @@ to-arraybuffer@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
to-camel-case@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/to-camel-case/-/to-camel-case-1.0.0.tgz#1a56054b2f9d696298ce66a60897322b6f423e46"
dependencies:
to-space-case "^1.0.0"
to-descriptor@^1.0.1: to-descriptor@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/to-descriptor/-/to-descriptor-1.0.1.tgz#a0e678c34ebc7d2dae464d8372bc21479d9c2bcd" resolved "https://registry.yarnpkg.com/to-descriptor/-/to-descriptor-1.0.1.tgz#a0e678c34ebc7d2dae464d8372bc21479d9c2bcd"
@ -7241,6 +7277,16 @@ to-fast-properties@^1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
to-no-case@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/to-no-case/-/to-no-case-1.0.2.tgz#c722907164ef6b178132c8e69930212d1b4aa16a"
to-space-case@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/to-space-case/-/to-space-case-1.0.0.tgz#b052daafb1b2b29dc770cea0163e5ec0ebc9fc17"
dependencies:
to-no-case "^1.0.0"
toposort@^1.0.0: toposort@^1.0.0:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.6.tgz#c31748e55d210effc00fdcdc7d6e68d7d7bb9cec" resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.6.tgz#c31748e55d210effc00fdcdc7d6e68d7d7bb9cec"