finish drag and drop system

This commit is contained in:
Katalina / stardust 2017-12-27 13:16:26 -06:00
parent e36be9e381
commit 7806219464
19 changed files with 465 additions and 90 deletions

View file

@ -5,7 +5,7 @@
"dependencies": {
"color": "^2.0.1",
"custom-react-scripts": "0.2.1",
"eslint": "^4.13.0",
"eslint": "^4.14.0",
"history": "^4.7.2",
"immutable": "^3.8.2",
"prop-types": "^15.6.0",
@ -22,7 +22,7 @@
"react-transition-group": "^2.2.1",
"redux": "^3.7.2",
"redux-devtools": "^3.4.1",
"redux-devtools-dock-monitor": "^1.1.2",
"redux-devtools-dock-monitor": "^1.1.3",
"redux-devtools-log-monitor": "^1.4.0",
"redux-logger": "^3.0.6",
"redux-saga": "^0.16.0",
@ -37,7 +37,7 @@
},
"proxy": "http://localhost:6769",
"devDependencies": {
"eslint-config-standard": "^10.2.1",
"eslint-config-standard": "^11.0.0-beta.0",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^5.2.1",
"eslint-plugin-promise": "^3.6.0",

View file

@ -1,10 +1,12 @@
import React, { Component } from 'react'
import { BrowserRouter } from 'react-router-dom'
import { Provider } from 'react-redux'
import createHistory from 'history/createBrowserHistory'
import { ConnectedRouter } from 'react-router-redux'
import { DragDropContext } from 'react-dnd'
import HTML5Backend from 'react-dnd-html5-backend'
import createHistory from 'history/createBrowserHistory'
import configureStore from './store/configureStore'
import './App.css'
import './generic.sass'
import Wrapper from './components/wrapper'
import AppRouter from './router'
@ -15,6 +17,7 @@ const store = configureStore(undefined, history)
window.__APP_STORE__ = store
@DragDropContext(HTML5Backend)
class App extends Component {
componentWillMount () {
store.dispatch(userInit)

View file

@ -1,21 +1,41 @@
import React, { Component } from 'react'
import { DropTarget } from 'react-dnd'
import Role from '../role'
import Role from '../role/draggable'
import CategoryEditor from './CategoryEditor'
@DropTarget(Symbol.for('dnd: role'), {
drop (props, monitor, element) {
props.onDrop(monitor.getItem())
},
canDrop (props) {
return props.mode !== Symbol.for('edit')
}
}, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
isOverCurrent: monitor.isOver({ shallow: true }),
canDrop: monitor.canDrop(),
itemType: monitor.getItemType()
}))
class Category extends Component {
render () {
const { category, name } = this.props
const { category, name, isOver, connectDropTarget, mode, ...rest } = this.props
return <div key={name} className="role-picker__category">
if (mode === Symbol.for('edit')) {
return <CategoryEditor category={category} name={name} {...rest} />
}
return connectDropTarget(<div key={name} className={`role-picker__category ${(isOver) ? 'is-over' : ''}`}>
<h4>{ name }</h4>
{
category.get('roles_map')
.sortBy(r => r.get('position'))
.reverse()
.map((r, k) => <Role key={k} role={r} type='drag' />)
.map((r, k) => <Role key={k} role={r} categoryId={name} />)
.toArray()
}
</div>
</div>)
}
}
export default Category

View file

@ -0,0 +1,41 @@
import React, { Component } from 'react'
export default class CategoryEditor extends Component {
render () {
const {
name,
category
} = this.props
return <div className="role-editor__category editor">
<form onSubmit={(e) => e.preventDefault()} className="uk-form-stacked uk-light">
<div>
<label className="uk-form-label">Category Name</label>
<div className="uk-form-controls">
<input type="text" className="uk-input" placeholder='' value={name} onChange={this.props.onEdit('name', Symbol.for('edit: text'))} />
</div>
</div>
<div style={{ marginTop: 10 }}>
<div className="uk-form-controls">
<label uk-tooltip="delay: 1s" title="Hides and disables roles in this category from being used.">
<input
style={{ marginRight: 5 }}
type="checkbox"
className="uk-checkbox"
checked={category.get('hidden')}
onChange={this.props.onEdit('hidden', Symbol.for('edit: bool'))}
/>
Hidden
</label>
</div>
</div>
<div className='role-editor__actions'>
<button className="uk-button rp-button secondary role-editor__actions_delete" onClick={this.props.onDelete}>
<i uk-icon="icon: trash" />
</button>
<button className="uk-button rp-button primary role-editor__actions_save" onClick={this.props.onSave}>Save</button>
</div>
</form>
</div>
}
}

View file

@ -2,5 +2,45 @@
&__grid
display: grid
grid-template-areas: 'left right'
grid-template-columns: 1fr
grid-template-rows: 1fr 1fr
grid-template-columns: 1fr 1fr
grid-template-rows: 1fr
&__actions
display: flex
margin-top: 10px
button
padding: 0
&_delete
flex: 1
margin-right: 5px
&_save
flex: 4
.role-editor__category
box-sizing: border-box
background-color: var(--c-1)
padding: 15px
margin: 10px
min-width: 220px - 30px
&.add-button
height: 100px
display: flex
align-items: center
justify-content: center
text-align: center
color: var(--c-5)
font-size: 2em
i
transition: transform 0.15s ease-in-out, color 0.15s ease-in-out
&:hover
i
transform: scale(1.1)
color: var(--c-7)

View file

@ -0,0 +1,88 @@
import { Set } from 'immutable'
import * as UIActions from '../../actions/ui'
import { getViewMap } from '../role-picker/actions'
export const constructView = id => (dispatch, getState) => {
const server = getState().servers.get(id)
let { viewMap } = getViewMap(server)
viewMap = viewMap.map(c => c.set('mode', Symbol.for('drop')))
dispatch({
type: Symbol.for('re: setup'),
data: {
viewMap: viewMap
}
})
dispatch(UIActions.fadeIn)
}
export const addRoleToCategory = (name, oldName, role, flip = true) => (dispatch) => {
dispatch({
type: Symbol.for('re: add role to category'),
data: {
name,
role
}
})
if (flip) {
dispatch(removeRoleFromCategory(oldName, name, role, false))
}
}
export const removeRoleFromCategory = (name, oldName, role, flip = true) => (dispatch) => {
dispatch({
type: Symbol.for('re: remove role from category'),
data: {
name,
role
}
})
if (flip) {
dispatch(addRoleToCategory(oldName, name, role, false))
}
}
export const editCategory = (stuff) => dispatch => {
}
export const saveCategory = (name) => ({
type: Symbol.for('re: switch category mode'),
data: {
name,
mode: Symbol.for('drop')
}
})
export const deleteCategory = (name) => ({
type: Symbol.for('re: delete category'),
data: name
})
export const createCategory = (dispatch, getState) => {
const { roleEditor } = getState()
const vm = roleEditor.get('viewMap')
let name = 'New Category'
let idx = 1
while (vm.has(name)) {
idx++
name = `New Category ${idx}`
}
dispatch({
type: Symbol.for('re: set category'),
data: {
name,
roles: Set([]),
roles_map: Set([]),
hidden: true,
mode: Symbol.for('edit')
}
})
}

View file

@ -1,13 +1,15 @@
import React, { Component } from 'react'
import { Set } from 'immutable'
import { connect } from 'react-redux'
import { DropTarget } from 'react-dnd'
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 CategoryEditor from './CategoryEditor'
import Role from '../role/draggable'
import { Scrollbars } from 'react-custom-scrollbars';
const mapState = ({ rolePicker, roleEditor, servers }, ownProps) => ({
@ -17,43 +19,110 @@ const mapState = ({ rolePicker, roleEditor, servers }, ownProps) => ({
})
@connect(mapState)
@DropTarget(Symbol.for('dnd: role'), {
drop (props, monitor, element) {
element.dropRole({}, 'Uncategorized')(monitor.getItem())
}
}, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
isOverCurrent: monitor.isOver({ shallow: true }),
canDrop: monitor.canDrop(),
itemType: monitor.getItemType()
}))
class RoleEditor extends Component {
componentWillMount () {
const { dispatch, match: { params: { server } } } = this.props
dispatch(PickerActions.setup(server))
dispatch(Actions.constructView(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))))
dispatch(UIActions.fadeOut(() => dispatch(Actions.constructView(nextProps.match.params.server))))
}
}
dropRole = (category, name) => ({role, category}) => {
const { dispatch } = this.props
console.log(role)
dispatch(Actions.addRoleToCategory(name, category, role))
}
createCategory = () => {
const { dispatch } = this.props
dispatch(Actions.createCategory)
}
saveCategory = (category, name) => () => {
const { dispatch } = this.props
dispatch(Actions.saveCategory(name))
}
deleteCategory = (category, name) => () => {
const { dispatch } = this.props
dispatch(Actions.deleteCategory(name))
}
editCategory = (category, name) => (key, type) => event => {
const { dispatch } = this.props
let value
switch (type) {
case Symbol.for('edit: text'):
value = event.target.value
break
case Symbol.for('edit: bool'):
value = event.target.checked
break
default:
value = null
}
dispatch(Actions.editCategory({ category, name, key, type, value }))
}
render () {
const vm = this.props.rp.get('viewMap')
console.log(vm.toJS())
const vm = this.props.editor.get('viewMap')
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()
.filter((_, k) => k !== 'Uncategorized')
.map((c, name) => <Category
key={name}
name={name}
category={c}
mode={c.get('mode')}
onDrop={this.dropRole(c, name)}
onEdit={this.editCategory(c, name)}
// onEditOpen={this.openEditor(c, name)}
onSave={this.saveCategory(c, name)}
onDelete={this.deleteCategory(c, name)}
/>)
.toArray()
}
<div onClick={this.createCategory} uk-tooltip="pos: bottom" title="Add new category" className="role-editor__category add-button">
<i uk-icon="icon: plus" />
</div>
</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()
this.props.connectDropTarget(
<div className="role-editor__grid__right">
{
(vm.getIn(['Uncategorized', 'roles_map']) || Set())
.sortBy(r => r.get('position'))
.reverse()
.map((r, k) => <Role key={k} categoryId='Uncategorized' role={r} />)
.toArray()
}
</div>)
}
</div>
</div>
</div>
}

View file

@ -29,6 +29,10 @@ class Category extends Component {
return null
}
if (category.get('roles').count() === 0) {
return null
}
return <div key={name} className="role-picker__category">
<h4>{ name }</h4>
{

View file

@ -50,39 +50,3 @@
&.hidden
opacity: 0
// display: none
.action__button
border: 0
border-radius: 2px
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: 2px
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

@ -16,8 +16,7 @@ export const setup = id => async dispatch => {
dispatch(constructView(id))
}
export const constructView = id => (dispatch, getState) => {
const server = getState().servers.get(id)
export const getViewMap = server => {
const roles = server.get('roles')
const categories = server.get('categories')
@ -28,7 +27,7 @@ export const constructView = id => (dispatch, getState) => {
// console.log('roles', allRoles.toJS(), accountedRoles.toJS(), unaccountedRoles.toJS())
const vm = categories.set('Uncategorized', fromJS({
const viewMap = categories.set('Uncategorized', fromJS({
roles: unaccountedRoles,
hidden: false,
type: 'multi'
@ -43,11 +42,21 @@ export const constructView = id => (dispatch, getState) => {
const selected = roles.reduce((acc, r) => acc.set(r.get('id'), r.get('selected')), Map())
console.log(categories, selected)
return {
viewMap,
selected
}
}
export const constructView = id => (dispatch, getState) => {
const server = getState().servers.get(id)
const { viewMap, selected } = getViewMap(server)
dispatch({
type: Symbol.for('setup role picker'),
data: {
viewMap: vm,
viewMap: viewMap,
rolesSelected: selected,
originalRolesSelected: selected,
hidden: false

View file

@ -77,10 +77,10 @@ class RolePicker extends Component {
<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">
<button disabled={!this.rolesHaveChanged} onClick={() => dispatch(Actions.resetSelected)} className="uk-button rp-button secondary">
Reset
</button>
<button disabled={!this.rolesHaveChanged} onClick={() => dispatch(Actions.submitSelected(this.props.match.params.server))} className="uk-button action__button primary">
<button disabled={!this.rolesHaveChanged} onClick={() => dispatch(Actions.submitSelected(this.props.match.params.server))} className="uk-button rp-button primary">
Save Changes
</button>
</div>

View file

@ -3,7 +3,6 @@
border-radius: 32px
box-sizing: border-box
height: 32px
margin: 0 6px 6px 0
padding: 4px
display: inline-flex
font-weight: 600
@ -13,8 +12,11 @@
vertical-align: baseline
transition: background-color 0.15s ease-in-out, transform 0.15s ease-in-out, box-shadow 0.15s ease-in-out
transform: translateZ(0)
margin: 0 6px 6px 0
position: relative
&.role__button
.role__option
&:hover
cursor: inherit
@ -67,7 +69,7 @@
&.role__drag
box-shadow: none
&:hover
&:hover, &.is-dragging
transform: translateZ(0) translateY(-1px)
box-shadow: 0 1px 2px rgba(0,0,0,0.3)
background-color: rgba(#ccc,0.03)
@ -75,5 +77,26 @@
&:active
cursor: grabbing
&::after
position: absolute
top: -1px
bottom: -1px
left: -1px
right: -1px
border-radius: 32px
background-color: var(--c-3)
border: 2px dashed var(--c-7)
content: ""
opacity: 0
transition: opacity 0.15s ease-in-out
box-sizing: border-box
&.is-dragging
transition: border-color 0s ease-out 0.15s
border-color: transparent
&::after
opacity: 1
.role__option:active, .role:active .role__option:not(:active)
transform: translateY(0) translateZ(0px) !important

View file

@ -0,0 +1,19 @@
import React, { Component } from 'react'
import { DragSource } from 'react-dnd'
import Role from './index'
@DragSource(Symbol.for('dnd: role'), {
beginDrag ({ role, categoryId }) {
return { role, category: categoryId }
}
},
(connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
}))
export default class DraggableRole extends Component {
render () {
return <Role {...this.props} type='drag' />
}
}

View file

@ -15,9 +15,11 @@ class Role extends Component {
}
render () {
let { role, selected, disabled, type, provided } = this.props
let { role, selected, disabled, type, isDragging } = this.props
type = type || 'button'
// console.log(this.props)
let color = Color(role.get('color'))
if (color.rgbNumber() === 0) {
@ -27,14 +29,14 @@ class Role extends Component {
const c = color
let hc = color.lighten(0.1)
return <div
const out = <div
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}`}
className={`role font-sans-serif ${(disabled) ? 'disabled' : ''} ${(isDragging) ? 'is-dragging' : ''} role__${type}`}
style={{
'--role-color-hex': c.string(),
'--role-color-hover': hc.string(),
@ -45,6 +47,12 @@ class Role extends Component {
{role.get('name')}
</div>
</div>
if (type === 'drag' && this.props.connectDragSource != null) {
return this.props.connectDragSource(out)
}
return out
}
}

37
UI/src/generic.sass Normal file
View file

@ -0,0 +1,37 @@
.rp-button
border: 0
border-radius: 2px
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: 2px
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)
color: var(--c-9)
&.secondary
background-color: var(--c-3)
color: var(--c-7)

View file

@ -3,6 +3,7 @@ import { combineReducers } from 'redux'
import servers from './servers'
import user from './user'
import rolePicker from './role-picker'
import roleEditor from './role-editor'
import { routerMiddleware } from 'react-router-redux'
// import roles from './roles'
@ -15,6 +16,7 @@ const appState = (state = initialState, { type, data }) => {
switch (type) {
case Symbol.for('app ready'):
return {
...state,
ready: true,
fade: false
}
@ -36,7 +38,8 @@ const rootReducer = combineReducers({
user,
router: routerMiddleware,
// roles,
rolePicker
rolePicker,
roleEditor
})
export default rootReducer

View file

@ -0,0 +1,43 @@
import { Map, OrderedMap, fromJS } from 'immutable'
const initialState = Map({
viewMap: OrderedMap({})
})
const reducer = (state = initialState, { type, data }) => {
switch (type) {
case Symbol.for('re: setup'):
const { viewMap, ...rest } = data
return Map({ viewMap: OrderedMap(viewMap), ...rest })
case Symbol.for('re: set category'):
return state.setIn(['viewMap', data.name], Map(data))
case Symbol.for('re: delete category'):
return state.deleteIn(['viewMap', data])
case Symbol.for('re: switch category mode'):
return state.setIn(['viewMap', data.name, 'mode'], data.mode)
case Symbol.for('re: add role to category'):
const category = state.getIn(['viewMap', data.name])
return state.setIn(['viewMap', data.name],
category
.set('roles', category.get('roles').add(data.role.get('id')))
.set('roles_map', category.get('roles_map').add(data.role))
)
case Symbol.for('re: remove role from category'):
const rmCat = state.getIn(['viewMap', data.name])
return state.setIn(['viewMap', data.name],
rmCat
.set('roles', rmCat.get('roles').filterNot(r => r === data.role.get('id')))
.set('roles_map', rmCat.get('roles_map').filterNot(r => r.get('id') === data.role.get('id')))
)
default:
return state
}
}
export default reducer

View file

@ -1,11 +1,11 @@
import React, { Component, Fragment } from 'react'
import { Route } from 'react-router-dom'
import { connect } from 'react-redux'
import { withRouter } from 'react-router'
import Servers from '../components/servers'
import OauthCallback from '../components/oauth-callback'
import OauthFlow from '../components/oauth-flow'
import { withRouter } from 'react-router';
const aaa = (props) => (<div>{ JSON.stringify(props) }</div>)

View file

@ -2140,7 +2140,7 @@ date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
debug@*, debug@^3.0.1, debug@^3.1.0:
debug@*, debug@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
dependencies:
@ -2623,9 +2623,9 @@ eslint-config-react-app@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-2.0.1.tgz#fd0503da01ae608f0c6ae8861de084975142230e"
eslint-config-standard@^10.2.1:
version "10.2.1"
resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-10.2.1.tgz#c061e4d066f379dc17cd562c64e819b4dd454591"
eslint-config-standard@^11.0.0-beta.0:
version "11.0.0-beta.0"
resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-11.0.0-beta.0.tgz#f8afe69803d95c685a4b8392b8793188eb03cbb3"
eslint-import-resolver-node@^0.3.1:
version "0.3.1"
@ -2740,6 +2740,10 @@ eslint-scope@^3.7.1:
esrecurse "^4.1.0"
estraverse "^4.1.1"
eslint-visitor-keys@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
eslint@4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.4.1.tgz#99cd7eafcffca2ff99a5c8f5f2a474d6364b4bd3"
@ -2781,21 +2785,21 @@ eslint@4.4.1:
table "^4.0.1"
text-table "~0.2.0"
eslint@^4.13.0:
version "4.13.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.13.0.tgz#1991aa359586af83877bde59de9d41f53e20826d"
eslint@^4.14.0:
version "4.14.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.14.0.tgz#96609768d1dd23304faba2d94b7fefe5a5447a82"
dependencies:
ajv "^5.3.0"
babel-code-frame "^6.22.0"
chalk "^2.1.0"
concat-stream "^1.6.0"
cross-spawn "^5.1.0"
debug "^3.0.1"
debug "^3.1.0"
doctrine "^2.0.2"
eslint-scope "^3.7.1"
eslint-visitor-keys "^1.0.0"
espree "^3.5.2"
esquery "^1.0.0"
estraverse "^4.2.0"
esutils "^2.0.2"
file-entry-cache "^2.0.0"
functional-red-black-tree "^1.0.1"
@ -6300,9 +6304,9 @@ reduce-function-call@^1.0.1:
dependencies:
balanced-match "^0.4.2"
redux-devtools-dock-monitor@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/redux-devtools-dock-monitor/-/redux-devtools-dock-monitor-1.1.2.tgz#eb213a021f8c25b892f6c98bdb87368615e3d201"
redux-devtools-dock-monitor@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/redux-devtools-dock-monitor/-/redux-devtools-dock-monitor-1.1.3.tgz#1205e823c82536570aac8551a1c4b70972cba6aa"
dependencies:
babel-runtime "^6.2.0"
parse-key "^0.2.1"