From 3387f1d68dbcccfcd1826244abd9d79deabba921 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Thu, 26 May 2016 14:52:58 -0700 Subject: [PATCH 01/30] Basic org page with members tab --- src/components/organizations/show/index.js | 70 ++++++++++++++++----- src/components/organizations/show/style.css | 40 ++++++++++-- src/components/spaces/cards/style.css | 8 +-- src/modules/organizations/actions.js | 3 +- src/routes/router.js | 2 +- 5 files changed, 97 insertions(+), 26 deletions(-) diff --git a/src/components/organizations/show/index.js b/src/components/organizations/show/index.js index 8ff2c0eab..e3ec7780c 100644 --- a/src/components/organizations/show/index.js +++ b/src/components/organizations/show/index.js @@ -5,6 +5,7 @@ import * as spaceActions from 'gModules/spaces/actions' import * as organizationActions from 'gModules/organizations/actions' import { organizationSpaceSelector } from './organizationSpaceSelector.js'; import { organizationMemberSelector } from './organizationMemberSelector.js'; +import SpaceCards from 'gComponents/spaces/cards' import Container from 'gComponents/utility/container/Container.js' import e from 'gEngine/engine' import './style.css' @@ -16,9 +17,22 @@ function mapStateToProps(state) { } } -const Member = ({user}) => ( +const Member = ({user, isAdmin}) => (
- +
+
+ +
{user.name}
+
+
+ {isAdmin ? 'Admin' : 'Editor'} +
+
+ +
+
) @@ -29,7 +43,8 @@ export default class OrganizationShow extends Component{ displayName: 'OrganizationShow' state = { - attemptedFetch: false + attemptedFetch: false, + openTab: 'MEMBERS' } componentWillMount() { @@ -56,17 +71,24 @@ export default class OrganizationShow extends Component{ } } + changeTab(tab) { + this.setState({openTab: tab}) + } + render () { const {organizationId, organizations, members} = this.props + const {openTab} = this.state const spaces = _.orderBy(this.props.organizationSpaces.asMutable(), ['updated_at'], ['desc']) const organization = organizations.find(u => u.id.toString() === organizationId.toString()) return (
-
-
-
+
+ +
+
+
{organization &&
@@ -77,21 +99,39 @@ export default class OrganizationShow extends Component{

{organization.name}

-
- {members && members.map(m => { - return () - })} -
}
-
- {spaces && - - } +
+ { [{name: 'Models', key: 'MODELS'}, {name: 'Members', key: 'MEMBERS'}].map( e => { + const className = `item ${(openTab === e.key) ? 'active' : ''}` + return ( + {this.changeTab(e.key)}}> {e.name} + ) + })}
+ + {(openTab === 'MODELS') && spaces && + + } + + {(openTab === 'MEMBERS') && members && +
+
+
+
+ {members.map(m => { + return () + })} +
+
+
+ }
diff --git a/src/components/organizations/show/style.css b/src/components/organizations/show/style.css index 032933816..bd6d7757c 100644 --- a/src/components/organizations/show/style.css +++ b/src/components/organizations/show/style.css @@ -1,3 +1,5 @@ +@import './styles/variables.css'; + .organizationShow .main-organization-tag { text-align: center; margin-right: 0.5em; @@ -12,17 +14,45 @@ } .organizationShow .members { - margin-top: 30px; - float: left; + width: 100%; + background-color: white; + border-radius: 3px; + padding: 0 1em; + margin-top: 4em; } .organizationShow .member { + padding: 1em 0.2em; + border-bottom: 1px solid #eee; +} + +.organizationShow .member:last-child { + border-bottom: none; +} + +.organizationShow .member .role{ + margin-top: 0.6em; + color: $grey-2; + font-weight: bold; + font-size: 1.2em; +} + +.organizationShow .member .button.remove{ + margin-top: 0.2em; + float: right; +} + +.organizationShow .member--name { float: left; - margin-right: 10px; - margin-bottom: 10px; + font-size: 1.4em; + font-weight: 800; + color: $black-3; + margin-top: 0.5em; } .organizationShow .member img{ + float: left; width: 40px; - border-radius: 3em; + border-radius: 3px; + margin-right: 1em; } diff --git a/src/components/spaces/cards/style.css b/src/components/spaces/cards/style.css index 7658c30f0..552f6c8ed 100644 --- a/src/components/spaces/cards/style.css +++ b/src/components/spaces/cards/style.css @@ -27,7 +27,7 @@ .SpaceCard .image { min-height: 11em; position: relative; - background-color: #D8DADC; + background-color: #D9DEE2; float: left; flex: 0; } @@ -51,9 +51,9 @@ } .SpaceCard .image .snapshot.blank img{ - width: 31%; - max-width: 13em; - opacity: 0.3; + height: 67%; + width: auto; + opacity: 0.23; margin-bottom: 1em; } diff --git a/src/modules/organizations/actions.js b/src/modules/organizations/actions.js index 33c1cade8..4c33c5663 100644 --- a/src/modules/organizations/actions.js +++ b/src/modules/organizations/actions.js @@ -30,7 +30,8 @@ export function fetchById(organizationId) { export function fetchSuccess(organizations) { return (dispatch) => { - const formatted = organizations.map(o => _.pick(o, ['id', 'name', 'picture'])) + const formatted = organizations.map(o => _.pick(o, ['id', 'name', 'picture', 'admin_id'])) + console.log('formatted', formatted) dispatch(sActions.fetchSuccess(formatted)) } } diff --git a/src/routes/router.js b/src/routes/router.js index 363259ccc..83f930d65 100644 --- a/src/routes/router.js +++ b/src/routes/router.js @@ -77,6 +77,6 @@ export default Router.extend({ faq() { this.render() }, subscribe(id) { this.render() }, userShow(id) { this.render(, {backgroundColor: 'GREY'}) }, - organizationShow(id) { this.render() }, + organizationShow(id) { this.render(, {backgroundColor: 'GREY'}) }, pricing() { this.render(, {backgroundColor: 'BLUE'}) }, }) From 523e9176c796763e3b3ec6beb326f6daa6fbb103 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Thu, 26 May 2016 16:55:20 -0700 Subject: [PATCH 02/30] Basic user org membership works --- src/components/organizations/show/index.js | 24 +++++++++++++++---- src/lib/engine/organization.js | 15 +++++++++++- src/lib/guesstimate_api/index.js | 2 ++ .../resources/UserOrganizationMemberships.js | 9 +++++++ .../userOrganizationMemberships/actions.js | 13 ++++++++++ 5 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 src/lib/guesstimate_api/resources/UserOrganizationMemberships.js diff --git a/src/components/organizations/show/index.js b/src/components/organizations/show/index.js index e3ec7780c..b19e193cb 100644 --- a/src/components/organizations/show/index.js +++ b/src/components/organizations/show/index.js @@ -3,6 +3,7 @@ import { connect } from 'react-redux'; import SpaceList from 'gComponents/spaces/list' import * as spaceActions from 'gModules/spaces/actions' import * as organizationActions from 'gModules/organizations/actions' +import * as userOrganizationMembershipActions from 'gModules/userOrganizationMemberships/actions.js' import { organizationSpaceSelector } from './organizationSpaceSelector.js'; import { organizationMemberSelector } from './organizationMemberSelector.js'; import SpaceCards from 'gComponents/spaces/cards' @@ -17,7 +18,7 @@ function mapStateToProps(state) { } } -const Member = ({user, isAdmin}) => ( +const Member = ({user, isAdmin, onRemove}) => (
@@ -28,9 +29,11 @@ const Member = ({user, isAdmin}) => ( {isAdmin ? 'Admin' : 'Editor'}
- + {user.membershipId && !isAdmin && + + }
@@ -75,6 +78,10 @@ export default class OrganizationShow extends Component{ this.setState({openTab: tab}) } + destroyMembership(user) { + this.props.dispatch(userOrganizationMembershipActions.destroy(user.membershipId)) + } + render () { const {organizationId, organizations, members} = this.props const {openTab} = this.state @@ -126,7 +133,14 @@ export default class OrganizationShow extends Component{
{members.map(m => { - return () + return ( + {this.destroyMembership(m)}} + /> + ) })}
diff --git a/src/lib/engine/organization.js b/src/lib/engine/organization.js index cf71b5625..6db58000b 100644 --- a/src/lib/engine/organization.js +++ b/src/lib/engine/organization.js @@ -1,5 +1,9 @@ import * as _userOrganizationMemberships from './userOrganizationMemberships' +function sameId(first, second) { + return (parseInt(first) === parseInt(second)) +} + export function url(organization) { return (!!organization) ? urlById(organization.id) : '' } @@ -9,5 +13,14 @@ export function urlById(id) { } export function organizationUsers(organizationId, users, memberships) { - return _.filter(users, u => _userOrganizationMemberships.isMember(organizationId, u.id, memberships)) + let filteredMemberships = organizationMemberships(organizationId, memberships) + let filteredUsers = _.filter(users, u => _userOrganizationMemberships.isMember(organizationId, u.id, filteredMemberships)) + return filteredUsers.map(e => { + let membership = filteredMemberships.find(m => sameId(m.user_id, e.id)) + return {...e, membershipId: (membership && membership.id)} + }) +} + +export function organizationMemberships(organizationId, memberships) { + return _.filter(memberships, e => sameId(e.organization_id, organizationId)) } diff --git a/src/lib/guesstimate_api/index.js b/src/lib/guesstimate_api/index.js index 1420f7612..4bc104c55 100644 --- a/src/lib/guesstimate_api/index.js +++ b/src/lib/guesstimate_api/index.js @@ -3,6 +3,7 @@ import Organizations from './resources/Organizations.js' import Users from './resources/Users.js' import Accounts from './resources/Accounts.js' import Copies from './resources/Copies.js' +import UserOrganizationMemberships from './resources/UserOrganizationMemberships.js' export default class GuesstimateApi { @@ -14,5 +15,6 @@ export default class GuesstimateApi { this.organizations = new Organizations(this) this.copies = new Copies(this) this.accounts = new Accounts(this) + this.userOrganizationMemberships = new UserOrganizationMemberships(this) } } diff --git a/src/lib/guesstimate_api/resources/UserOrganizationMemberships.js b/src/lib/guesstimate_api/resources/UserOrganizationMemberships.js new file mode 100644 index 000000000..f4848b565 --- /dev/null +++ b/src/lib/guesstimate_api/resources/UserOrganizationMemberships.js @@ -0,0 +1,9 @@ +import AbstractResource from '../AbstractResource.js' + +export default class UserOrganizationMemberships extends AbstractResource { + destroy({userOrganizationMembershipId}, callback) { + const url = `user_organization_memberships/${userOrganizationMembershipId}` + const method = 'DELETE' + this.guesstimateMethod({url, method})(callback) + } +} diff --git a/src/modules/userOrganizationMemberships/actions.js b/src/modules/userOrganizationMemberships/actions.js index 0f0031bd4..065a5a865 100644 --- a/src/modules/userOrganizationMemberships/actions.js +++ b/src/modules/userOrganizationMemberships/actions.js @@ -48,3 +48,16 @@ export function fetchByUserId(userId) { }) } } + +export function destroy(id) { + return (dispatch, getState) => { + dispatch(sActions.deleteStart({id})); + api(getState()).userOrganizationMemberships.destroy({userOrganizationMembershipId: id}, (err, value) => { + if (err) { + captureApiError('OrganizationsMemberDestroy', null, null, err, {url: 'destroyOrganizationMember'}) + } else { + dispatch(sActions.deleteSuccess({id})) + } + }) + } +} From d99cae972f5b962aabb71fd65f02c702a4b36993 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Fri, 27 May 2016 00:51:23 -0700 Subject: [PATCH 03/30] Very basic version of user org addition --- src/components/organizations/show/index.js | 125 ++++++++++++------ src/components/organizations/show/style.css | 46 ++++++- .../resources/Organizations.js | 7 + src/modules/organizations/actions.js | 21 +++ .../userOrganizationMemberships/actions.js | 18 ++- 5 files changed, 169 insertions(+), 48 deletions(-) diff --git a/src/components/organizations/show/index.js b/src/components/organizations/show/index.js index b19e193cb..321b3eb3d 100644 --- a/src/components/organizations/show/index.js +++ b/src/components/organizations/show/index.js @@ -10,6 +10,7 @@ import SpaceCards from 'gComponents/spaces/cards' import Container from 'gComponents/utility/container/Container.js' import e from 'gEngine/engine' import './style.css' +import Icon from 'react-fa' function mapStateToProps(state) { return { @@ -23,7 +24,7 @@ const Member = ({user, isAdmin, onRemove}) => (
-
{user.name}
+ {user.name}
{isAdmin ? 'Admin' : 'Editor'} @@ -47,7 +48,8 @@ export default class OrganizationShow extends Component{ state = { attemptedFetch: false, - openTab: 'MEMBERS' + openTab: 'MEMBERS', + subMembersTab: 'INDEX' } componentWillMount() { @@ -82,6 +84,10 @@ export default class OrganizationShow extends Component{ this.props.dispatch(userOrganizationMembershipActions.destroy(user.membershipId)) } + addUser() { + this.props.dispatch(organizationActions.addMember(this.props.organizationId, 'foo@bar.com')) + } + render () { const {organizationId, organizations, members} = this.props const {openTab} = this.state @@ -102,53 +108,96 @@ export default class OrganizationShow extends Component{ +

+ {organization.name} +

-

- {organization.name} -

}
-
- { [{name: 'Models', key: 'MODELS'}, {name: 'Members', key: 'MEMBERS'}].map( e => { - const className = `item ${(openTab === e.key) ? 'active' : ''}` - return ( - {this.changeTab(e.key)}}> {e.name} - ) - })} -
- - {(openTab === 'MODELS') && spaces && - - } - - {(openTab === 'MEMBERS') && members && -
-
-
-
- {members.map(m => { - return ( - {this.destroyMembership(m)}} - /> - ) - })} -
+
+
+
+ { [{name: 'Members', key: 'MEMBERS'}, {name: 'Models', key: 'MODELS'}].map( e => { + const className = `item ${(openTab === e.key) ? 'active' : ''}` + return ( + {this.changeTab(e.key)}}> {e.name} + ) + })}
- } +
+ +
+ {(openTab === 'MODELS') && spaces && + + } + + {(openTab === 'MEMBERS') && members && organization && + {this.setState({subMembersTab: name})}} + /> + } +
) } } + +const MembersTab = ({subTab, members, admin_id, onRemove, addUser, changeSubTab}) => ( +
+
+
{changeSubTab('ADD')}}> + Add Users +
+
+
+ {subTab === 'INDEX' && +
+
+ {members.map(m => { + return ( + {onRemove(m)}} + /> + ) + })} +
+ +
+ } + {subTab === 'ADD' && +
+

Invite New Members

+

Members have viewing & editing access to all organization models.

+ +
{changeSubTab('INDEX')}}> close
+ +
+
+ + +
+
+ +
+ } +
+
+) + diff --git a/src/components/organizations/show/style.css b/src/components/organizations/show/style.css index bd6d7757c..77f49cf65 100644 --- a/src/components/organizations/show/style.css +++ b/src/components/organizations/show/style.css @@ -1,5 +1,45 @@ @import './styles/variables.css'; +.organizationShow .header-background{ + background-color: white; + border-bottom: 1px solid #D4D4D5; +} + +.organizationShow .header-background .wrap{ + position: relative; + padding-top: 3em; + padding-bottom: 2em; +} + +.organizationShow .ui.menu { + float: right; + margin-top: 0; +} + +.organizationShow .ui.menu .item{ + font-weight: 800; + font-size: 1.2em; + color: #444; +} + +.organizationShow .main-section { + margin-top: 1em; +} + +.organizationShow .ui.menu .item.active{ + background: #CACACA; +} + +.organizationShow .ui.tabular.menu.reversed .active.item{ + background: none #F7F5F5; + border-radius: 0.15rem 0.15rem 0 0 !important; +} + +.organizationShow .main-organization-tag h1 { + margin-bottom: 0; + margin-top: 0.5rem; +} + .organizationShow .main-organization-tag { text-align: center; margin-right: 0.5em; @@ -7,10 +47,10 @@ } .organizationShow .main-organization-tag img{ - max-width: 10em; - max-height: 8em; + max-width: 4em; + max-height: 5em; border-radius: 0.5em; - margin-bottom: 10px; + margin-bottom: 2px; } .organizationShow .members { diff --git a/src/lib/guesstimate_api/resources/Organizations.js b/src/lib/guesstimate_api/resources/Organizations.js index a84ac0660..e9e066b5c 100644 --- a/src/lib/guesstimate_api/resources/Organizations.js +++ b/src/lib/guesstimate_api/resources/Organizations.js @@ -14,4 +14,11 @@ export default class Organizations extends AbstractResource { this.guesstimateMethod({url, method})(callback) } + + addMember({organizationId, email}, callback) { + const url = `organizations/${organizationId}/members` + const method = 'POST' + + this.guesstimateMethod({url, method})(callback) + } } diff --git a/src/modules/organizations/actions.js b/src/modules/organizations/actions.js index 4c33c5663..478514f5e 100644 --- a/src/modules/organizations/actions.js +++ b/src/modules/organizations/actions.js @@ -1,6 +1,8 @@ import {actionCreatorsFor} from 'redux-crud' +import cuid from 'cuid' import * as displayErrorsActions from 'gModules/displayErrors/actions.js' import * as membershipActions from 'gModules/userOrganizationMemberships/actions.js' +import * as userActions from 'gModules/users/actions.js' import {captureApiError} from 'lib/errors/index.js' import {setupGuesstimateApi} from 'servers/guesstimate-api/constants.js' import * as userOrganizationMembershipActions from 'gModules/userOrganizationMemberships/actions.js' @@ -35,3 +37,22 @@ export function fetchSuccess(organizations) { dispatch(sActions.fetchSuccess(formatted)) } } + +export function addMember(organizationId, email) { + return (dispatch, getState) => { + const cid = cuid() + let object = {id: cid} + + const action = sActions.createStart(object); + + api(getState()).organizations.addMember({organizationId, email}, (err, membership) => { + if (err) { + captureApiError('OrganizationMemberCreate', null, null, err, {url: 'OrganizationMemberCreate'}) + } + else if (membership) { + dispatch(userActions.fetchSuccess([membership._embedded.user])) + dispatch(membershipActions.fetchSuccess([membership])) + } + }) + } +} diff --git a/src/modules/userOrganizationMemberships/actions.js b/src/modules/userOrganizationMemberships/actions.js index 065a5a865..5c72faf7c 100644 --- a/src/modules/userOrganizationMemberships/actions.js +++ b/src/modules/userOrganizationMemberships/actions.js @@ -7,6 +7,7 @@ import {captureApiError} from 'lib/errors/index.js' import {setupGuesstimateApi} from 'servers/guesstimate-api/constants.js' let sActions = actionCreatorsFor('userOrganizationMemberships') +let relevantAttributes = ['id', 'user_id', 'organization_id'] function api(state) { function getToken(state) { @@ -17,15 +18,14 @@ function api(state) { export function fetchByOrganizationId(organizationId) { return (dispatch, getState) => { - api(getState()).organizations.getMembers({organizationId}, (err, members) => { + api(getState()).organizations.getMembers({organizationId}, (err, memberships) => { if (err) { dispatch(displayErrorsActions.newError()) captureApiError('OrganizationsMemberFetch', null, null, err, {url: 'fetchMembers'}) - } else if (members) { - const formatted = members.items.map(m => _.pick(m, ['id', 'user_id', 'organization_id'])) - dispatch(sActions.fetchSuccess(formatted)) + } else if (memberships) { + dispatch(fetchSuccess(memberships.items)) - const users = members.items.map(m => _.get(m, '_embedded.user')) + const users = memberships.items.map(m => _.get(m, '_embedded.user')) dispatch(userActions.fetchSuccess(users)) } }) @@ -39,8 +39,7 @@ export function fetchByUserId(userId) { dispatch(displayErrorsActions.newError()) captureApiError('OrganizationsMemberFetch', null, null, err, {url: 'fetchMembers'}) } else if (memberships) { - const formatted = memberships.items.map(m => _.pick(m, ['id', 'user_id', 'organization_id'])) - dispatch(sActions.fetchSuccess(formatted)) + dispatch(fetchSuccess(memberships.items)) const organizations = memberships.items.map(m => _.get(m, '_embedded.organization')) dispatch(organizationActions.fetchSuccess(organizations)) @@ -49,6 +48,11 @@ export function fetchByUserId(userId) { } } +export function fetchSuccess(memberships) { + const formatted = memberships.map(m => _.pick(m, relevantAttributes)) + return sActions.fetchSuccess(formatted) +} + export function destroy(id) { return (dispatch, getState) => { dispatch(sActions.deleteStart({id})); From bf1d70f4905700ccf7158946bf0d71ce6053e47f Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Sat, 28 May 2016 13:40:09 -0700 Subject: [PATCH 04/30] Basic method for adding users to organization --- src/components/metrics/card/token/style.css | 2 +- src/components/organizations/show/index.js | 101 +++++++++++++----- src/components/organizations/show/style.css | 16 ++- .../resources/Organizations.js | 3 +- src/modules/organizations/actions.js | 7 +- .../userOrganizationMemberships/actions.js | 20 ++++ src/modules/users/actions.js | 2 +- src/styles/variables.css | 3 + 8 files changed, 118 insertions(+), 36 deletions(-) diff --git a/src/components/metrics/card/token/style.css b/src/components/metrics/card/token/style.css index f1fedd6bd..c06613ce4 100644 --- a/src/components/metrics/card/token/style.css +++ b/src/components/metrics/card/token/style.css @@ -7,7 +7,7 @@ .MetricToken .ui.tiny.label { font-size: 0.85rem; - background-color: #25B530 !important; + background-color: $green-1 !important; border-radius: 2px; padding: 0.4em 0.45em 0.35em; margin-top: 2px; diff --git a/src/components/organizations/show/index.js b/src/components/organizations/show/index.js index 321b3eb3d..0acd62ef5 100644 --- a/src/components/organizations/show/index.js +++ b/src/components/organizations/show/index.js @@ -1,4 +1,5 @@ import React, {Component, PropTypes} from 'react' +import ReactDOM from 'react-dom' import { connect } from 'react-redux'; import SpaceList from 'gComponents/spaces/list' import * as spaceActions from 'gModules/spaces/actions' @@ -22,10 +23,13 @@ function mapStateToProps(state) { const Member = ({user, isAdmin, onRemove}) => (
-
+ +
+ {user.sign_in_count > 0 ? 'joined' : 'invited'} +
{isAdmin ? 'Admin' : 'Editor'}
@@ -77,15 +81,19 @@ export default class OrganizationShow extends Component{ } changeTab(tab) { - this.setState({openTab: tab}) + this.setState({ + openTab: tab, + subMembersTab: 'INDEX' + }) } destroyMembership(user) { - this.props.dispatch(userOrganizationMembershipActions.destroy(user.membershipId)) + this.props.dispatch(userOrganizationMembershipActions.destroy(user.membershipId)) } - addUser() { - this.props.dispatch(organizationActions.addMember(this.props.organizationId, 'foo@bar.com')) + addUser(email) { + console.log("adding user", email) + this.props.dispatch(userOrganizationMembershipActions.createWithEmail(this.props.organizationId, email)) } render () { @@ -143,7 +151,7 @@ export default class OrganizationShow extends Component{ subTab={this.state.subMembersTab} members={members} admin_id={organization.admin_id} - onRemove={this.destroyMembership} + onRemove={this.destroyMembership.bind(this)} addUser={this.addUser.bind(this)} changeSubTab={(name) => {this.setState({subMembersTab: name})}} /> @@ -157,14 +165,30 @@ export default class OrganizationShow extends Component{ } const MembersTab = ({subTab, members, admin_id, onRemove, addUser, changeSubTab}) => ( -
+
-
{changeSubTab('ADD')}}> - Add Users -
+ {subTab === 'INDEX' && +
{changeSubTab('ADD')}}> + Add Users +
+ } + {subTab === 'ADD' && +
{changeSubTab('INDEX')}}> + Back
+ } +
- {subTab === 'INDEX' && + {subTab === 'ADD' && +
+

Invite New Members

+
+

Members have viewing & editing access to all organization models. If you are on a plan, your pricing will be adjusted within 24 hours.

+
+ +
+ } + {subTab !== 'INDEX1' &&
{members.map(m => { @@ -179,25 +203,48 @@ const MembersTab = ({subTab, members, admin_id, onRemove, addUser, changeSubTab} })}
-
- } - {subTab === 'ADD' && -
-

Invite New Members

-

Members have viewing & editing access to all organization models.

- -
{changeSubTab('INDEX')}}> close
- -
-
- - -
-
-
}
) +function validateEmail(email) { + var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + return re.test(email); +} + +class InviteUserForm extends Component{ + state = { value: '' } + + _submit() { + this.props.addUser(this.state.value) + } + + _value() { + return this.refs.input && this.refs.input.value + } + + _onChange(e) { + this.setState({value: this._value()}) + } + + render() { + const {value} = this.state + const isValid = validateEmail(value) + const isEmpty = _.isEmpty(value) + const buttonColor = (isValid || isEmpty) ? 'green' : 'grey' + + return( +
+
+ + +
+
+ Invite User +
+
+ ) + } +} diff --git a/src/components/organizations/show/style.css b/src/components/organizations/show/style.css index 77f49cf65..6e70e57b0 100644 --- a/src/components/organizations/show/style.css +++ b/src/components/organizations/show/style.css @@ -22,8 +22,21 @@ color: #444; } +.organizationShow .ui.form{ + font-size: 1.2em; + margin-top: 4em; +} + .organizationShow .main-section { - margin-top: 1em; + margin-top: 2em; +} + +.organizationShow .tab-members{ + margin-top: 7em; +} + +.organizationShow .tab-members .ui.button.green{ + background-color: $green-2; } .organizationShow .ui.menu .item.active{ @@ -58,7 +71,6 @@ background-color: white; border-radius: 3px; padding: 0 1em; - margin-top: 4em; } .organizationShow .member { diff --git a/src/lib/guesstimate_api/resources/Organizations.js b/src/lib/guesstimate_api/resources/Organizations.js index e9e066b5c..cc0feab82 100644 --- a/src/lib/guesstimate_api/resources/Organizations.js +++ b/src/lib/guesstimate_api/resources/Organizations.js @@ -18,7 +18,8 @@ export default class Organizations extends AbstractResource { addMember({organizationId, email}, callback) { const url = `organizations/${organizationId}/members` const method = 'POST' + const data = {email} - this.guesstimateMethod({url, method})(callback) + this.guesstimateMethod({url, method, data})(callback) } } diff --git a/src/modules/organizations/actions.js b/src/modules/organizations/actions.js index 478514f5e..2bed8635c 100644 --- a/src/modules/organizations/actions.js +++ b/src/modules/organizations/actions.js @@ -2,7 +2,6 @@ import {actionCreatorsFor} from 'redux-crud' import cuid from 'cuid' import * as displayErrorsActions from 'gModules/displayErrors/actions.js' import * as membershipActions from 'gModules/userOrganizationMemberships/actions.js' -import * as userActions from 'gModules/users/actions.js' import {captureApiError} from 'lib/errors/index.js' import {setupGuesstimateApi} from 'servers/guesstimate-api/constants.js' import * as userOrganizationMembershipActions from 'gModules/userOrganizationMemberships/actions.js' @@ -41,17 +40,17 @@ export function fetchSuccess(organizations) { export function addMember(organizationId, email) { return (dispatch, getState) => { const cid = cuid() - let object = {id: cid} + let object = {id: cid, organization_id: organizationId, user_id: 4} const action = sActions.createStart(object); api(getState()).organizations.addMember({organizationId, email}, (err, membership) => { if (err) { - captureApiError('OrganizationMemberCreate', null, null, err, {url: 'OrganizationMemberCreate'}) + dispatch(sActions.createError(err, object)) } else if (membership) { dispatch(userActions.fetchSuccess([membership._embedded.user])) - dispatch(membershipActions.fetchSuccess([membership])) + dispatch(membershipActions.createSuccess([membership])) } }) } diff --git a/src/modules/userOrganizationMemberships/actions.js b/src/modules/userOrganizationMemberships/actions.js index 5c72faf7c..30dac284f 100644 --- a/src/modules/userOrganizationMemberships/actions.js +++ b/src/modules/userOrganizationMemberships/actions.js @@ -1,4 +1,5 @@ import {actionCreatorsFor} from 'redux-crud' +import cuid from 'cuid' import * as displayErrorsActions from 'gModules/displayErrors/actions.js' import * as userActions from 'gModules/users/actions.js' import * as organizationActions from 'gModules/organizations/actions.js' @@ -65,3 +66,22 @@ export function destroy(id) { }) } } + +export function createWithEmail(organizationId, email) { + return (dispatch, getState) => { + const cid = cuid() + let object = {id: cid, organization_id: organizationId} + + const action = sActions.createStart(object); + + api(getState()).organizations.addMember({organizationId, email}, (err, membership) => { + if (err) { + dispatch(sActions.createError(err, object)) + } + else if (membership) { + dispatch(userActions.fetchSuccess([membership._embedded.user])) + dispatch(sActions.createSuccess(membership)) + } + }) + } +} diff --git a/src/modules/users/actions.js b/src/modules/users/actions.js index 7acd48035..4aff172aa 100644 --- a/src/modules/users/actions.js +++ b/src/modules/users/actions.js @@ -56,7 +56,7 @@ export function fetchById(userId) { } function formatUsers(unformatted) { - return unformatted.map(u => _.pick(u, ['auth0_id', 'id', 'name', 'picture'])) + return unformatted.map(u => _.pick(u, ['auth0_id', 'id', 'name', 'picture', 'sign_in_count'])) } export function fromSearch(spaces) { diff --git a/src/styles/variables.css b/src/styles/variables.css index d2f203d3a..948c148d5 100644 --- a/src/styles/variables.css +++ b/src/styles/variables.css @@ -16,6 +16,9 @@ $grey-666: #666; $grey-999: #999; $grey-eee: #eee; +$green-1: #25B530; +$green-2: #2A8A31; + $purple-1: #7970A9; $purple-2: #6C5A8C; $purple-3: #EDE4F9; From 54d2c705ead9dfbb867791acdbdc498bd20fe159 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Sat, 28 May 2016 14:40:07 -0700 Subject: [PATCH 05/30] Added httpRequests reducer --- src/lib/guesstimate_api/AbstractResource.js | 2 +- src/modules/httpRequests/actions.js | 11 +++++ src/modules/httpRequests/reducer.js | 46 +++++++++++++++++++ src/modules/reducers.js | 2 + .../userOrganizationMemberships/actions.js | 5 +- 5 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 src/modules/httpRequests/actions.js create mode 100644 src/modules/httpRequests/reducer.js diff --git a/src/lib/guesstimate_api/AbstractResource.js b/src/lib/guesstimate_api/AbstractResource.js index 18d41031d..232c7b8bb 100644 --- a/src/lib/guesstimate_api/AbstractResource.js +++ b/src/lib/guesstimate_api/AbstractResource.js @@ -27,7 +27,7 @@ export default class AbstractResource { const params = this.guesstimateRequest({url, method, data}) const request = $.ajax(params) request.done((response) => {callback(null, response)}) - request.fail((jqXHR, textStatus, errorThrown) => {callback(errorThrown, null)}) + request.fail((jqXHR, textStatus, errorThrown) => {callback({jqXHR, textStatus, errorThrown}, null)}) } } } diff --git a/src/modules/httpRequests/actions.js b/src/modules/httpRequests/actions.js new file mode 100644 index 000000000..64c5a9cd5 --- /dev/null +++ b/src/modules/httpRequests/actions.js @@ -0,0 +1,11 @@ +export const start = ({id, entity, metadata}) => { + return { type: 'HTTP_REQUEST_START', id, entity, metadata} +} + +export const success = ({id, response}) => { + return { type: 'HTTP_REQUEST_SUCCESS', id, response} +} + +export const failure = ({id, error}) => { + return { type: 'HTTP_REQUEST_FAILURE', id, error} +} diff --git a/src/modules/httpRequests/reducer.js b/src/modules/httpRequests/reducer.js new file mode 100644 index 000000000..ed466433a --- /dev/null +++ b/src/modules/httpRequests/reducer.js @@ -0,0 +1,46 @@ +function findIndex(state, id) { return state.findIndex(y => y.id === id) } + +export const httpRequestsR = (state = [], action) => { + let id, request, i + + switch (action.type) { + case 'HTTP_REQUEST_START': + request = { + id: action.id, + entity: action.entity, + metadata: action.metadata, + busy: true, + success: false + } + return [...state, request] + case 'HTTP_REQUEST_SUCCESS': + id = action.id + i = findIndex(state, id) + + if (i !== -1) { + request = { + ...state[i], + busy: false, + success: true, + reponse: action.response + } + return [ ...state.slice(0, i), request, ...state.slice(i+1, state.length) ]; + } + case 'HTTP_REQUEST_FAILURE': + id = action.id + i = findIndex(state, id) + + if (i !== -1) { + request = { + ...state[i], + busy: false, + success: false, + error: action.error + } + return [ ...state.slice(0, i), request, ...state.slice(i+1, state.length) ]; + } + default: + return state + } +} + diff --git a/src/modules/reducers.js b/src/modules/reducers.js index e78aef507..4cfbd5574 100644 --- a/src/modules/reducers.js +++ b/src/modules/reducers.js @@ -17,6 +17,7 @@ import firstSubscriptionsR from './first_subscription/reducer' import modalR from './modal/reducer' import {copiedR} from './copied/reducer' import {checkpointsR} from './checkpoints/reducer' +import {httpRequestsR} from './httpRequests/reducer.js' export function changeSelect(location) { return { type: 'CHANGE_SELECT', location }; @@ -42,6 +43,7 @@ const rootReducer = function app(state = {}, action){ modal: SI(modalR(state.modal, action)), copied: SI(copiedR(state.copied, action)), checkpoints: SI(checkpointsR(state.checkpoints, action)), + httpRequests: SI(httpRequestsR(state.httpRequests, action)) } } diff --git a/src/modules/userOrganizationMemberships/actions.js b/src/modules/userOrganizationMemberships/actions.js index 30dac284f..59c66f082 100644 --- a/src/modules/userOrganizationMemberships/actions.js +++ b/src/modules/userOrganizationMemberships/actions.js @@ -3,6 +3,7 @@ import cuid from 'cuid' import * as displayErrorsActions from 'gModules/displayErrors/actions.js' import * as userActions from 'gModules/users/actions.js' import * as organizationActions from 'gModules/organizations/actions.js' +import * as httpRequestActions from 'gModules/httpRequests/actions.js' import {rootUrl} from 'servers/guesstimate-api/constants.js' import {captureApiError} from 'lib/errors/index.js' import {setupGuesstimateApi} from 'servers/guesstimate-api/constants.js' @@ -73,14 +74,16 @@ export function createWithEmail(organizationId, email) { let object = {id: cid, organization_id: organizationId} const action = sActions.createStart(object); - + dispatch(httpRequestActions.start({id: cid, entity: 'foobar', metadata: {organizationId}})) api(getState()).organizations.addMember({organizationId, email}, (err, membership) => { if (err) { dispatch(sActions.createError(err, object)) + dispatch(httpRequestActions.failure({id: cid, error: err})) } else if (membership) { dispatch(userActions.fetchSuccess([membership._embedded.user])) dispatch(sActions.createSuccess(membership)) + dispatch(httpRequestActions.success({id: cid, response: {membership}})) } }) } From d2456bb9f2af43192daf3f93772a5fcea23347ed Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Sat, 28 May 2016 17:40:41 -0700 Subject: [PATCH 06/30] Added appropriate http responses in organizations --- .../organizations/show/httpRequestSelector.js | 31 +++++++ src/components/organizations/show/index.js | 80 ++++++++++++++----- src/components/organizations/show/style.css | 1 + src/modules/httpRequests/reducer.js | 1 + .../userOrganizationMemberships/actions.js | 3 +- 5 files changed, 96 insertions(+), 20 deletions(-) create mode 100644 src/components/organizations/show/httpRequestSelector.js diff --git a/src/components/organizations/show/httpRequestSelector.js b/src/components/organizations/show/httpRequestSelector.js new file mode 100644 index 000000000..f79d6cb7a --- /dev/null +++ b/src/components/organizations/show/httpRequestSelector.js @@ -0,0 +1,31 @@ +import { createSelector } from 'reselect'; + +const specificHttpRequestSelector = state => state.httpRequests +const organizationIdSelector = (state, props) => props.organizationId + +export const httpRequestSelector = createSelector( + specificHttpRequestSelector, + organizationIdSelector, + (httpRequests, organizationId) => { + + let relevantRequests = httpRequests.filter(r => ( + (r.metadata.organizationId === organizationId) && + (r.entity === 'userOrganizationMembershipCreate') + )) + + let formattedRequests = relevantRequests.map(r => ( + { + id: r.id, + busy: r.busy, + success: r.success, + email: r.metadata.email, + created_at: r.created_at, + isExistingMember: (r.success) + + })) + + return { + httpRequests: formattedRequests + } + } +); diff --git a/src/components/organizations/show/index.js b/src/components/organizations/show/index.js index 0acd62ef5..19e6253ce 100644 --- a/src/components/organizations/show/index.js +++ b/src/components/organizations/show/index.js @@ -7,6 +7,7 @@ import * as organizationActions from 'gModules/organizations/actions' import * as userOrganizationMembershipActions from 'gModules/userOrganizationMemberships/actions.js' import { organizationSpaceSelector } from './organizationSpaceSelector.js'; import { organizationMemberSelector } from './organizationMemberSelector.js'; +import { httpRequestSelector } from './httpRequestSelector.js'; import SpaceCards from 'gComponents/spaces/cards' import Container from 'gComponents/utility/container/Container.js' import e from 'gEngine/engine' @@ -47,13 +48,14 @@ const Member = ({user, isAdmin, onRemove}) => ( @connect(mapStateToProps) @connect(organizationSpaceSelector) @connect(organizationMemberSelector) +@connect(httpRequestSelector) export default class OrganizationShow extends Component{ displayName: 'OrganizationShow' state = { attemptedFetch: false, openTab: 'MEMBERS', - subMembersTab: 'INDEX' + subMembersTab: 'ADD' } componentWillMount() { @@ -92,7 +94,6 @@ export default class OrganizationShow extends Component{ } addUser(email) { - console.log("adding user", email) this.props.dispatch(userOrganizationMembershipActions.createWithEmail(this.props.organizationId, email)) } @@ -131,7 +132,7 @@ export default class OrganizationShow extends Component{ { [{name: 'Members', key: 'MEMBERS'}, {name: 'Models', key: 'MODELS'}].map( e => { const className = `item ${(openTab === e.key) ? 'active' : ''}` return ( - {this.changeTab(e.key)}}> {e.name} + {this.changeTab(e.key)}}> {e.name} ) })}
@@ -154,6 +155,7 @@ export default class OrganizationShow extends Component{ onRemove={this.destroyMembership.bind(this)} addUser={this.addUser.bind(this)} changeSubTab={(name) => {this.setState({subMembersTab: name})}} + httpRequests={this.props.httpRequests} /> }
@@ -164,7 +166,7 @@ export default class OrganizationShow extends Component{ } } -const MembersTab = ({subTab, members, admin_id, onRemove, addUser, changeSubTab}) => ( +const MembersTab = ({subTab, members, admin_id, onRemove, addUser, changeSubTab, httpRequests}) => (
{subTab === 'INDEX' && @@ -185,10 +187,11 @@ const MembersTab = ({subTab, members, admin_id, onRemove, addUser, changeSubTab}

Members have viewing & editing access to all organization models. If you are on a plan, your pricing will be adjusted within 24 hours.

- +
} - {subTab !== 'INDEX1' && + + {subTab === 'INDEX' &&
{members.map(m => { @@ -219,14 +222,11 @@ class InviteUserForm extends Component{ _submit() { this.props.addUser(this.state.value) - } - - _value() { - return this.refs.input && this.refs.input.value + this.setState({value: ''}) } _onChange(e) { - this.setState({value: this._value()}) + this.setState({value: e.target.value}) } render() { @@ -235,16 +235,60 @@ class InviteUserForm extends Component{ const isEmpty = _.isEmpty(value) const buttonColor = (isValid || isEmpty) ? 'green' : 'grey' + const requests = _.orderBy(_.cloneDeep(this.props.httpRequests), ['created_at'], ['desc']) return( -
-
- - -
-
- Invite User +
+
+
+ + +
+
+ Invite User +
+
+
+ {_.map(requests, (request) => { + return( + + ) + })}
) } } + +const InvitationHttpRequest = ({busy, success, email, isExistingMember}) => { + let status = httpStatus(busy, success) + + if (status === 'sending'){ return ( +
+ Sending... +
+ )} else if (status === 'failure'){ return ( +
+ Invitation failed to send. +
+ )} else if (isExistingMember){ return ( +
+ {email} was added to your organization. +
+ )} else { return( +
+ {email} was sent an email invitation to join your organization. +
+ )} +} + +function httpStatus(busy, success) { + if (busy) { return 'sending' } + else if (success) { return 'success' } + else { return 'failure' } +} diff --git a/src/components/organizations/show/style.css b/src/components/organizations/show/style.css index 6e70e57b0..6f8188ae6 100644 --- a/src/components/organizations/show/style.css +++ b/src/components/organizations/show/style.css @@ -25,6 +25,7 @@ .organizationShow .ui.form{ font-size: 1.2em; margin-top: 4em; + margin-bottom: 4em; } .organizationShow .main-section { diff --git a/src/modules/httpRequests/reducer.js b/src/modules/httpRequests/reducer.js index ed466433a..11a6fe239 100644 --- a/src/modules/httpRequests/reducer.js +++ b/src/modules/httpRequests/reducer.js @@ -7,6 +7,7 @@ export const httpRequestsR = (state = [], action) => { case 'HTTP_REQUEST_START': request = { id: action.id, + created_at: Date.now(), entity: action.entity, metadata: action.metadata, busy: true, diff --git a/src/modules/userOrganizationMemberships/actions.js b/src/modules/userOrganizationMemberships/actions.js index 59c66f082..1f8ae6778 100644 --- a/src/modules/userOrganizationMemberships/actions.js +++ b/src/modules/userOrganizationMemberships/actions.js @@ -73,8 +73,7 @@ export function createWithEmail(organizationId, email) { const cid = cuid() let object = {id: cid, organization_id: organizationId} - const action = sActions.createStart(object); - dispatch(httpRequestActions.start({id: cid, entity: 'foobar', metadata: {organizationId}})) + dispatch(httpRequestActions.start({id: cid, entity: 'userOrganizationMembershipCreate', metadata: {organizationId, email}})) api(getState()).organizations.addMember({organizationId, email}, (err, membership) => { if (err) { dispatch(sActions.createError(err, object)) From f15dfc871b81d8ecfdd4a014588b2aa34a6e8616 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Sat, 28 May 2016 18:36:24 -0700 Subject: [PATCH 07/30] Distinguish users who already exist --- .../organizations/show/httpRequestSelector.js | 11 +++++++---- src/modules/httpRequests/reducer.js | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/organizations/show/httpRequestSelector.js b/src/components/organizations/show/httpRequestSelector.js index f79d6cb7a..4a7258c9d 100644 --- a/src/components/organizations/show/httpRequestSelector.js +++ b/src/components/organizations/show/httpRequestSelector.js @@ -3,6 +3,11 @@ import { createSelector } from 'reselect'; const specificHttpRequestSelector = state => state.httpRequests const organizationIdSelector = (state, props) => props.organizationId +function isExistingMember(request) { + let sign_in_count = _.get(request, 'response.membership._embedded.user.sign_in_count') + return (sign_in_count > 0) +} + export const httpRequestSelector = createSelector( specificHttpRequestSelector, organizationIdSelector, @@ -13,15 +18,13 @@ export const httpRequestSelector = createSelector( (r.entity === 'userOrganizationMembershipCreate') )) - let formattedRequests = relevantRequests.map(r => ( - { + let formattedRequests = relevantRequests.map(r => ({ id: r.id, busy: r.busy, success: r.success, email: r.metadata.email, created_at: r.created_at, - isExistingMember: (r.success) - + isExistingMember: isExistingMember(r) })) return { diff --git a/src/modules/httpRequests/reducer.js b/src/modules/httpRequests/reducer.js index 11a6fe239..55115e7cd 100644 --- a/src/modules/httpRequests/reducer.js +++ b/src/modules/httpRequests/reducer.js @@ -23,7 +23,7 @@ export const httpRequestsR = (state = [], action) => { ...state[i], busy: false, success: true, - reponse: action.response + response: action.response } return [ ...state.slice(0, i), request, ...state.slice(i+1, state.length) ]; } From 0149af3ad1be28965448ad3b096315c7341ac61a Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Sat, 28 May 2016 18:41:01 -0700 Subject: [PATCH 08/30] User input form should be immediately focussed and enter should work --- src/components/organizations/show/index.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/components/organizations/show/index.js b/src/components/organizations/show/index.js index 19e6253ce..dfe1b2599 100644 --- a/src/components/organizations/show/index.js +++ b/src/components/organizations/show/index.js @@ -220,6 +220,10 @@ function validateEmail(email) { class InviteUserForm extends Component{ state = { value: '' } + componentDidMount() { + this.refs.input.focus() + } + _submit() { this.props.addUser(this.state.value) this.setState({value: ''}) @@ -229,6 +233,12 @@ class InviteUserForm extends Component{ this.setState({value: e.target.value}) } + _onKeyDown(e) { + if (e.keyCode === 13 && validateEmail(this.state.value)) { + this._submit(); + } + } + render() { const {value} = this.state const isValid = validateEmail(value) @@ -241,7 +251,14 @@ class InviteUserForm extends Component{
- +
Invite User From 8432041ae4a6a2604ab06b766d342c8b6927518c Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Sat, 28 May 2016 21:08:34 -0700 Subject: [PATCH 09/30] Cleaned styles and components for organizations show page --- src/components/organizations/show/index.js | 264 +++++++++++--------- src/components/organizations/show/style.css | 197 +++++++-------- src/styles/variables.css | 2 +- 3 files changed, 236 insertions(+), 227 deletions(-) diff --git a/src/components/organizations/show/index.js b/src/components/organizations/show/index.js index dfe1b2599..7a317b626 100644 --- a/src/components/organizations/show/index.js +++ b/src/components/organizations/show/index.js @@ -21,30 +21,6 @@ function mapStateToProps(state) { } } -const Member = ({user, isAdmin, onRemove}) => ( -
-
-
- - {user.name} -
-
- {user.sign_in_count > 0 ? 'joined' : 'invited'} -
-
- {isAdmin ? 'Admin' : 'Editor'} -
-
- {user.membershipId && !isAdmin && - - } -
-
-
-) - @connect(mapStateToProps) @connect(organizationSpaceSelector) @connect(organizationMemberSelector) @@ -55,7 +31,7 @@ export default class OrganizationShow extends Component{ state = { attemptedFetch: false, openTab: 'MEMBERS', - subMembersTab: 'ADD' + subMembersTab: 'INDEX' } componentWillMount() { @@ -105,60 +81,35 @@ export default class OrganizationShow extends Component{ return ( -
-
- -
-
-
- {organization && -
-
- -

- {organization.name} -

-
-
- } -
-
+
-
-
-
- { [{name: 'Members', key: 'MEMBERS'}, {name: 'Models', key: 'MODELS'}].map( e => { - const className = `item ${(openTab === e.key) ? 'active' : ''}` - return ( - {this.changeTab(e.key)}}> {e.name} - ) - })} -
-
-
+ -
- {(openTab === 'MODELS') && spaces && - - } - - {(openTab === 'MEMBERS') && members && organization && - {this.setState({subMembersTab: name})}} - httpRequests={this.props.httpRequests} - /> - } -
+ + +
+ {(openTab === 'MODELS') && spaces && + + } + + {(openTab === 'MEMBERS') && members && organization && + {this.setState({subMembersTab: name})}} + httpRequests={this.props.httpRequests} + /> + }
@@ -166,31 +117,51 @@ export default class OrganizationShow extends Component{ } } -const MembersTab = ({subTab, members, admin_id, onRemove, addUser, changeSubTab, httpRequests}) => ( -
+const OrganizationHeader = ({organization}) => ( +
+
+
+ {organization && +
+
+ +

+ {organization.name} +

+
+
+ } +
+
+) + +const OrganizationTabButtons = ({tabs, openTab, changeTab}) => ( +
+
+
+ { tabs.map( e => { + const className = `item ${(openTab === e.key) ? 'active' : ''}` + return ( + {changeTab(e.key)}}> {e.name} + ) + })} +
+
+
+) + +const MembersIndexSubTab = ({subTab, members, admin_id, onChangeSubTab, onRemove}) => ( +
{subTab === 'INDEX' && -
{changeSubTab('ADD')}}> +
{onChangeSubTab('ADD')}}> Add Users
} - {subTab === 'ADD' && -
{changeSubTab('INDEX')}}> - Back -
- }
- {subTab === 'ADD' && -
-

Invite New Members

-
-

Members have viewing & editing access to all organization models. If you are on a plan, your pricing will be adjusted within 24 hours.

-
- -
- } - {subTab === 'INDEX' &&
@@ -205,19 +176,53 @@ const MembersTab = ({subTab, members, admin_id, onRemove, addUser, changeSubTab, ) })}
-
}
) +const MembersTab = ({subTab, members, admin_id, onRemove, addUser, onChangeSubTab, httpRequests}) => ( +
+ {subTab === 'ADD' && + + } + {subTab === 'INDEX' && + + } +
+) + +const Member = ({user, isAdmin, onRemove}) => ( +
+
+
+ + {user.name} +
+
+ {user.sign_in_count > 0 ? 'joined' : 'invited'} +
+
+ {isAdmin ? 'Admin' : 'Editor'} +
+
+ {user.membershipId && !isAdmin && + + } +
+
+
+) + function validateEmail(email) { var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; return re.test(email); } -class InviteUserForm extends Component{ +class MembersAddSubTab extends Component{ state = { value: '' } componentDidMount() { @@ -247,36 +252,45 @@ class InviteUserForm extends Component{ const requests = _.orderBy(_.cloneDeep(this.props.httpRequests), ['created_at'], ['desc']) return( -
-
-
- - -
-
- Invite User -
-
+
+
+
{this.props.onChangeSubTab('INDEX')}}> + Member List
- {_.map(requests, (request) => { - return( - +

Invite New Members

+

Members have viewing & editing access to all organization models. If you are on a plan, your pricing will be adjusted within 24 hours.

+
+
+ + - ) - })} +
+
+ Invite User +
+
+
+
+ {_.map(requests, (request) => { + return( + + ) + })} +
) } @@ -291,7 +305,7 @@ const InvitationHttpRequest = ({busy, success, email, isExistingMember}) => {
)} else if (status === 'failure'){ return (
- Invitation failed to send. + Invitation to {email} failed. This could be because they are already part of the organization or because of a server problem. If it continues, please let us know.
)} else if (isExistingMember){ return (
diff --git a/src/components/organizations/show/style.css b/src/components/organizations/show/style.css index 6f8188ae6..61788f9bc 100644 --- a/src/components/organizations/show/style.css +++ b/src/components/organizations/show/style.css @@ -1,111 +1,106 @@ @import './styles/variables.css'; -.organizationShow .header-background{ - background-color: white; - border-bottom: 1px solid #D4D4D5; -} - -.organizationShow .header-background .wrap{ - position: relative; - padding-top: 3em; - padding-bottom: 2em; -} - -.organizationShow .ui.menu { - float: right; - margin-top: 0; -} - -.organizationShow .ui.menu .item{ - font-weight: 800; - font-size: 1.2em; - color: #444; -} - -.organizationShow .ui.form{ +.OrganizationShow { + .main-section { + margin-top: 2em; + } + + .ui.menu .item.active{ + background: #CACACA; + } + + .ui.tabular.menu.reversed .active.item{ + background: none #F7F5F5; + border-radius: 0.15rem 0.15rem 0 0 !important; + } + + .OrganizationHeader { + h1 { + margin-bottom: 0; + margin-top: 0.5rem; + } + + .center-display { + text-align: center; + margin-right: 0.5em; + margin-top: 0.25em; + } + + img { + max-width: 4em; + max-height: 5em; + border-radius: 0.5em; + margin-bottom: 2px; + } + } + + .OrganizationTabButtons { + .ui.menu { + float: right; + margin-top: 0; + } + + .ui.menu .item{ + font-weight: 800; + font-size: 1.2em; + color: #444; + } + } +} + +.OrganizationShow .MembersAddSubTab .ui.form{ font-size: 1.2em; margin-top: 4em; margin-bottom: 4em; } -.organizationShow .main-section { - margin-top: 2em; -} - -.organizationShow .tab-members{ +.OrganizationShow .MembersTab { margin-top: 7em; -} - -.organizationShow .tab-members .ui.button.green{ - background-color: $green-2; -} - -.organizationShow .ui.menu .item.active{ - background: #CACACA; -} - -.organizationShow .ui.tabular.menu.reversed .active.item{ - background: none #F7F5F5; - border-radius: 0.15rem 0.15rem 0 0 !important; -} - -.organizationShow .main-organization-tag h1 { - margin-bottom: 0; - margin-top: 0.5rem; -} - -.organizationShow .main-organization-tag { - text-align: center; - margin-right: 0.5em; - margin-top: 0.25em; -} - -.organizationShow .main-organization-tag img{ - max-width: 4em; - max-height: 5em; - border-radius: 0.5em; - margin-bottom: 2px; -} - -.organizationShow .members { - width: 100%; - background-color: white; - border-radius: 3px; - padding: 0 1em; -} - -.organizationShow .member { - padding: 1em 0.2em; - border-bottom: 1px solid #eee; -} - -.organizationShow .member:last-child { - border-bottom: none; -} - -.organizationShow .member .role{ - margin-top: 0.6em; - color: $grey-2; - font-weight: bold; - font-size: 1.2em; -} - -.organizationShow .member .button.remove{ - margin-top: 0.2em; - float: right; -} - -.organizationShow .member--name { - float: left; - font-size: 1.4em; - font-weight: 800; - color: $black-3; - margin-top: 0.5em; -} -.organizationShow .member img{ - float: left; - width: 40px; - border-radius: 3px; - margin-right: 1em; + .ui.button.green{ + background-color: $green-2; + } + + .members { + width: 100%; + background-color: white; + border-radius: 3px; + padding: 0 1em; + } + + .Member { + padding: 1em 0.2em; + border-bottom: 1px solid #eee; + + .role{ + margin-top: 0.6em; + color: $grey-2; + font-weight: bold; + font-size: 1.2em; + } + + .button.remove{ + margin-top: 0.2em; + float: right; + } + + .member--name { + float: left; + font-size: 1.4em; + font-weight: 800; + color: $black-3; + margin-top: 0.5em; + } + + img{ + float: left; + width: 40px; + border-radius: 3px; + margin-right: 1em; + } + + &:last-child { + border-bottom: none; + } + } } diff --git a/src/styles/variables.css b/src/styles/variables.css index 948c148d5..e12c02724 100644 --- a/src/styles/variables.css +++ b/src/styles/variables.css @@ -17,7 +17,7 @@ $grey-999: #999; $grey-eee: #eee; $green-1: #25B530; -$green-2: #2A8A31; +$green-2: #1EB128; $purple-1: #7970A9; $purple-2: #6C5A8C; From e0d47780aa31cce78c163ca8ca54a1bfce940335 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Sat, 28 May 2016 23:21:04 -0700 Subject: [PATCH 10/30] Added basic confirmation modal for user removal --- src/components/organizations/show/index.js | 46 ++++++++++++++------- src/components/organizations/show/style.css | 24 +++++++++-- src/modules/modal/actions.js | 4 ++ src/modules/modal/routes.js | 17 +++++++- src/routes/main.css | 19 +++++++++ 5 files changed, 91 insertions(+), 19 deletions(-) diff --git a/src/components/organizations/show/index.js b/src/components/organizations/show/index.js index 7a317b626..773cb560d 100644 --- a/src/components/organizations/show/index.js +++ b/src/components/organizations/show/index.js @@ -14,6 +14,8 @@ import e from 'gEngine/engine' import './style.css' import Icon from 'react-fa' +import * as modalActions from 'gModules/modal/actions.js' + function mapStateToProps(state) { return { me: state.me, @@ -65,14 +67,30 @@ export default class OrganizationShow extends Component{ }) } - destroyMembership(user) { - this.props.dispatch(userOrganizationMembershipActions.destroy(user.membershipId)) + destroyMembership(membershipId) { + this.props.dispatch(userOrganizationMembershipActions.destroy(membershipId)) } addUser(email) { this.props.dispatch(userOrganizationMembershipActions.createWithEmail(this.props.organizationId, email)) } + onRemove(member) { + this.confirmRemove(member) + } + + confirmRemove({email, name, membershipId}) { + const removeCallback = () => { + this.destroyMembership(membershipId) + this.props.dispatch(modalActions.close()) + } + + const message = `Are you sure you want to remove ${name} from this organization?` + + this.props.dispatch(modalActions.openConfirmation({onConfirm: removeCallback, message})) + } + + render () { const {organizationId, organizations, members} = this.props const {openTab} = this.state @@ -104,7 +122,7 @@ export default class OrganizationShow extends Component{ subTab={this.state.subMembersTab} members={members} admin_id={organization.admin_id} - onRemove={this.destroyMembership.bind(this)} + onRemove={this.onRemove.bind(this)} addUser={this.addUser.bind(this)} onChangeSubTab={(name) => {this.setState({subMembersTab: name})}} httpRequests={this.props.httpRequests} @@ -157,11 +175,11 @@ const MembersIndexSubTab = ({subTab, members, admin_id, onChangeSubTab, onRemove
{subTab === 'INDEX' &&
{onChangeSubTab('ADD')}}> - Add Users + Add Members
}
-
+
{subTab === 'INDEX' &&
@@ -196,20 +214,20 @@ const MembersTab = ({subTab, members, admin_id, onRemove, addUser, onChangeSubTa const Member = ({user, isAdmin, onRemove}) => (
-
+ -
- {user.sign_in_count > 0 ? 'joined' : 'invited'} + {user.name}
{isAdmin ? 'Admin' : 'Editor'}
+
+ {user.sign_in_count > 0 ? 'joined' : 'invited'} +
{user.membershipId && !isAdmin && - }
@@ -258,9 +276,9 @@ class MembersAddSubTab extends Component{ Member List
-
+

Invite New Members

-

Members have viewing & editing access to all organization models. If you are on a plan, your pricing will be adjusted within 24 hours.

+

Members have viewing & editing access to all organization models.
If you are on a plan, your pricing will be adjusted within 24 hours.

diff --git a/src/components/organizations/show/style.css b/src/components/organizations/show/style.css index 61788f9bc..81cdf5aee 100644 --- a/src/components/organizations/show/style.css +++ b/src/components/organizations/show/style.css @@ -54,6 +54,12 @@ margin-bottom: 4em; } +.OrganizationShow .MembersAddSubTab p{ + color: #666; + font-size: 1.2em; + line-height: 1.7em; +} + .OrganizationShow .MembersTab { margin-top: 7em; @@ -79,19 +85,31 @@ font-size: 1.2em; } - .button.remove{ + .button.remove { margin-top: 0.2em; float: right; + font-size: 1.2em !important; + padding: 7px 10px !important; + } + + .button.remove i{ + margin: 0 !important; } - .member--name { + .name { float: left; - font-size: 1.4em; + font-size: 1.3em; font-weight: 800; color: $black-3; margin-top: 0.5em; } + .invitation-status { + font-size: 1.2em; + color: #666; + margin-top: 0.6em; + } + img{ float: left; width: 40px; diff --git a/src/modules/modal/actions.js b/src/modules/modal/actions.js index 1c555a567..f478739eb 100644 --- a/src/modules/modal/actions.js +++ b/src/modules/modal/actions.js @@ -6,6 +6,10 @@ export function openFirstSubscription(planId) { return { type: 'MODAL_CHANGE', componentName: 'firstSubscription', props: {planId} }; } +export function openConfirmation(props) { + return { type: 'MODAL_CHANGE', componentName: 'confirmation', props }; +} + export function change({componentName, props}) { return { type: 'MODAL_CHANGE', componentName, props }; } diff --git a/src/modules/modal/routes.js b/src/modules/modal/routes.js index efd70983c..68d135861 100644 --- a/src/modules/modal/routes.js +++ b/src/modules/modal/routes.js @@ -7,9 +7,22 @@ import NavHelper from 'gComponents/utility/NavHelper/index.js'; import FirstSubscriptionContainer from 'gComponents/subscriptions/FirstSubscription/container.js' import * as modalActions from 'gModules/modal/actions.js' +export default class Confirmation extends Component{ + render() { + return ( +
+

{this.props.message}

+ + +
+ ) + } +} + const routes = [ {name: 'settings', component: SettingsContainer}, - {name: 'firstSubscription', component: FirstSubscriptionContainer} + {name: 'firstSubscription', component: FirstSubscriptionContainer}, + {name: 'confirmation', component: Confirmation} ] function getComponent(componentName) { @@ -38,7 +51,7 @@ export default class ModalRouter extends Component{ bottom: 'inherit', background: 'none', border: 'none', - padding: '0', + padding: 0, marginRight: 'auto', marginLeft: 'auto', marginTop: '14%' diff --git a/src/routes/main.css b/src/routes/main.css index 12541667f..092e9871c 100644 --- a/src/routes/main.css +++ b/src/routes/main.css @@ -25,3 +25,22 @@ body { body.remove-elev #elevio-widget { display: none!important; } + +.MainConfirmation { + background: white; + padding: 3em 4em; + border-radius: 2px; + text-align: center; + min-width: 12em; + max-width: 600px; +} + +.MainConfirmation .ui.button { + margin-right: 1em; +} + +.MainConfirmation h1{ + margin-bottom: 2em; + font-size: 1.6em; + color: #333; +} From 1447e39c844bc0f99eec67774d7eb7efce308383 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Sun, 29 May 2016 00:02:27 -0700 Subject: [PATCH 11/30] Hide org members actions from non-admins --- src/components/organizations/show/index.js | 77 +++++++++++++--------- src/routes/main.css | 1 + 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/src/components/organizations/show/index.js b/src/components/organizations/show/index.js index 773cb560d..4e5436970 100644 --- a/src/components/organizations/show/index.js +++ b/src/components/organizations/show/index.js @@ -96,6 +96,7 @@ export default class OrganizationShow extends Component{ const {openTab} = this.state const spaces = _.orderBy(this.props.organizationSpaces.asMutable(), ['updated_at'], ['desc']) const organization = organizations.find(u => u.id.toString() === organizationId.toString()) + const meIsAdmin = !!organization && (organization.admin_id === this.props.me.id) return ( @@ -126,6 +127,7 @@ export default class OrganizationShow extends Component{ addUser={this.addUser.bind(this)} onChangeSubTab={(name) => {this.setState({subMembersTab: name})}} httpRequests={this.props.httpRequests} + meIsAdmin={meIsAdmin} /> }
@@ -170,16 +172,27 @@ const OrganizationTabButtons = ({tabs, openTab, changeTab}) => (
) -const MembersIndexSubTab = ({subTab, members, admin_id, onChangeSubTab, onRemove}) => ( +const MembersTab = ({subTab, members, admin_id, onRemove, addUser, onChangeSubTab, httpRequests, meIsAdmin}) => ( +
+ {subTab === 'ADD' && + + } + {subTab === 'INDEX' && + + } +
+) + +const MembersIndexSubTab = ({subTab, members, admin_id, onChangeSubTab, onRemove, meIsAdmin}) => (
- {subTab === 'INDEX' && + {subTab === 'INDEX' && meIsAdmin &&
{onChangeSubTab('ADD')}}> Add Members
}
-
+
{subTab === 'INDEX' &&
@@ -190,6 +203,7 @@ const MembersIndexSubTab = ({subTab, members, admin_id, onChangeSubTab, onRemove user={m} isAdmin={admin_id === m.id} onRemove={() => {onRemove(m)}} + meIsAdmin={meIsAdmin} /> ) })} @@ -200,38 +214,37 @@ const MembersIndexSubTab = ({subTab, members, admin_id, onChangeSubTab, onRemove
) -const MembersTab = ({subTab, members, admin_id, onRemove, addUser, onChangeSubTab, httpRequests}) => ( -
- {subTab === 'ADD' && - - } - {subTab === 'INDEX' && - - } -
-) - -const Member = ({user, isAdmin, onRemove}) => ( +const Member = ({user, isAdmin, onRemove, meIsAdmin}) => (
-
-
- - {user.name} -
-
- {isAdmin ? 'Admin' : 'Editor'} -
-
- {user.sign_in_count > 0 ? 'joined' : 'invited'} + {meIsAdmin && +
+
+ + {user.name} +
+
+ {isAdmin ? 'Admin' : 'Editor'} +
+
+ {user.sign_in_count > 0 ? 'joined' : 'invited'} +
+
+ {user.membershipId && !isAdmin && + + } +
-
- {user.membershipId && !isAdmin && - - } + } + {!meIsAdmin && +
+
+ + {user.name} +
-
+ }
) diff --git a/src/routes/main.css b/src/routes/main.css index 092e9871c..ac1446c0a 100644 --- a/src/routes/main.css +++ b/src/routes/main.css @@ -43,4 +43,5 @@ body.remove-elev #elevio-widget { margin-bottom: 2em; font-size: 1.6em; color: #333; + line-height: 1.8em; } From 8777722ccaea094ad1cf423291724c7d2c33c9bf Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Sun, 29 May 2016 00:21:10 -0700 Subject: [PATCH 12/30] Hide user page from non-members --- src/components/organizations/show/index.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/components/organizations/show/index.js b/src/components/organizations/show/index.js index 4e5436970..ef6e251ef 100644 --- a/src/components/organizations/show/index.js +++ b/src/components/organizations/show/index.js @@ -32,7 +32,7 @@ export default class OrganizationShow extends Component{ state = { attemptedFetch: false, - openTab: 'MEMBERS', + openTab: 'MODELS', subMembersTab: 'INDEX' } @@ -97,6 +97,7 @@ export default class OrganizationShow extends Component{ const spaces = _.orderBy(this.props.organizationSpaces.asMutable(), ['updated_at'], ['desc']) const organization = organizations.find(u => u.id.toString() === organizationId.toString()) const meIsAdmin = !!organization && (organization.admin_id === this.props.me.id) + const meIsMember = meIsAdmin || !!(members.find(m => m.id === this.props.me.id)) return ( @@ -104,11 +105,13 @@ export default class OrganizationShow extends Component{ - + {meIsMember && + + }
{(openTab === 'MODELS') && spaces && @@ -218,17 +221,17 @@ const Member = ({user, isAdmin, onRemove, meIsAdmin}) => (
{meIsAdmin &&
-
+
{isAdmin ? 'Admin' : 'Editor'}
-
+
{user.sign_in_count > 0 ? 'joined' : 'invited'}
-
+
{user.membershipId && !isAdmin &&
) -const MembersTab = ({subTab, members, admin_id, onRemove, addUser, onChangeSubTab, httpRequests, meIsAdmin}) => ( +const MembersTab = ({subTab, members, invitations, admin_id, onRemove, addUser, onChangeSubTab, httpRequests, meIsAdmin}) => (
{subTab === 'ADD' && } {subTab === 'INDEX' && - + }
) -const MembersIndexSubTab = ({subTab, members, admin_id, onChangeSubTab, onRemove, meIsAdmin}) => ( +const MembersIndexSubTab = ({subTab, members, invitations, admin_id, onChangeSubTab, onRemove, meIsAdmin}) => (
{subTab === 'INDEX' && meIsAdmin && @@ -208,7 +209,15 @@ const MembersIndexSubTab = ({subTab, members, admin_id, onChangeSubTab, onRemove onRemove={() => {onRemove(m)}} meIsAdmin={meIsAdmin} /> - ) + ) + })} + {invitations.map(i => { + return ( + + ) })}
@@ -217,6 +226,17 @@ const MembersIndexSubTab = ({subTab, members, admin_id, onChangeSubTab, onRemove
) +const Invitee = ({email}) => ( +
+
+
+ + {email} +
+
+
+) + const Member = ({user, isAdmin, onRemove, meIsAdmin}) => (
{meIsAdmin && diff --git a/src/components/organizations/show/organizationMemberSelector.js b/src/components/organizations/show/organizationMemberSelector.js index b8464fc15..61bceb3ce 100644 --- a/src/components/organizations/show/organizationMemberSelector.js +++ b/src/components/organizations/show/organizationMemberSelector.js @@ -2,16 +2,19 @@ import { createSelector } from 'reselect'; import e from 'gEngine/engine' const userOrganizationMembershipSelector = state => state.userOrganizationMemberships +const userOrganizationInvitationSelector = state => state.userOrganizationInvitations const userSelector = state => state.users const organizationIdSelector = (state, props) => props.organizationId export const organizationMemberSelector = createSelector( userOrganizationMembershipSelector, + userOrganizationInvitationSelector, userSelector, organizationIdSelector, - (memberships, users, organizationId) => { + (memberships, invitations, users, organizationId) => { return { - members: e.organization.organizationUsers(organizationId, users, memberships) + members: e.organization.organizationUsers(organizationId, users, memberships), + invitations: e.organization.organizationInvitations(organizationId, invitations), } } ); diff --git a/src/lib/engine/organization.js b/src/lib/engine/organization.js index 6db58000b..3bb6afc57 100644 --- a/src/lib/engine/organization.js +++ b/src/lib/engine/organization.js @@ -24,3 +24,7 @@ export function organizationUsers(organizationId, users, memberships) { export function organizationMemberships(organizationId, memberships) { return _.filter(memberships, e => sameId(e.organization_id, organizationId)) } + +export function organizationInvitations(organizationId, invitations) { + return _.filter(invitations, e => sameId(e.organization_id, organizationId)) +} diff --git a/src/lib/guesstimate_api/resources/Organizations.js b/src/lib/guesstimate_api/resources/Organizations.js index cc0feab82..0c4748f41 100644 --- a/src/lib/guesstimate_api/resources/Organizations.js +++ b/src/lib/guesstimate_api/resources/Organizations.js @@ -22,4 +22,11 @@ export default class Organizations extends AbstractResource { this.guesstimateMethod({url, method, data})(callback) } + + getInvitations({organizationId}, callback) { + const url = `organizations/${organizationId}/invitees` + const method = 'GET' + + this.guesstimateMethod({url, method})(callback) + } } diff --git a/src/modules/organizations/actions.js b/src/modules/organizations/actions.js index 2bed8635c..1b8693f21 100644 --- a/src/modules/organizations/actions.js +++ b/src/modules/organizations/actions.js @@ -5,6 +5,7 @@ import * as membershipActions from 'gModules/userOrganizationMemberships/actions import {captureApiError} from 'lib/errors/index.js' import {setupGuesstimateApi} from 'servers/guesstimate-api/constants.js' import * as userOrganizationMembershipActions from 'gModules/userOrganizationMemberships/actions.js' +import * as userOrganizationInvitationActions from 'gModules/userOrganizationInvitations/actions.js' let sActions = actionCreatorsFor('organizations') @@ -22,7 +23,9 @@ export function fetchById(organizationId) { dispatch(displayErrorsActions.newError()) captureApiError('OrganizationsFetch', null, null, err, {url: 'fetch'}) } else if (organization) { + // TODO(matthew): Unnecessary requests. dispatch(userOrganizationMembershipActions.fetchByOrganizationId(organizationId)) + dispatch(userOrganizationInvitationActions.fetchByOrganizationId(organizationId)) dispatch(sActions.fetchSuccess([organization])) } }) diff --git a/src/modules/reducers.js b/src/modules/reducers.js index 4cfbd5574..71985a4a9 100644 --- a/src/modules/reducers.js +++ b/src/modules/reducers.js @@ -36,6 +36,7 @@ const rootReducer = function app(state = {}, action){ users: SI(reduxCrud.reducersFor('users')(state.users, action)), organizations: SI(reduxCrud.reducersFor('organizations')(state.organizations, action)), userOrganizationMemberships: SI(reduxCrud.reducersFor('userOrganizationMemberships')(state.userOrganizationMemberships, action)), + userOrganizationInvitations: SI(reduxCrud.reducersFor('userOrganizationInvitations')(state.userOrganizationInvitations, action)), me: SI(meR(state.me, action)), canvasState: SI(canvasStateR(state.canvasState, action)), searchSpaces: SI(searchSpacesR(state.searchSpaces, action)), diff --git a/src/modules/spaces/actions.js b/src/modules/spaces/actions.js index 62716a98c..06f75253f 100644 --- a/src/modules/spaces/actions.js +++ b/src/modules/spaces/actions.js @@ -99,7 +99,7 @@ export function fetch({userId, organizationId}) { const formatted = value.items.map(d => _.pick(d, ['id', 'name', 'description', 'user_id', 'organization_id', 'updated_at', 'metric_count', 'is_private', 'screenshot', 'big_screenshot'])) dispatch(sActions.fetchSuccess(formatted)) - const users = value.items.map(d => _.get(d, 'user')) + const users = value.items.map(d => _.get(d, 'user')).filter(u => !!u) dispatch(userActions.fetchSuccess(users)) } }) diff --git a/src/modules/userOrganizationInvitations/actions.js b/src/modules/userOrganizationInvitations/actions.js index e1c9265be..d8a65850f 100644 --- a/src/modules/userOrganizationInvitations/actions.js +++ b/src/modules/userOrganizationInvitations/actions.js @@ -2,7 +2,7 @@ import {actionCreatorsFor} from 'redux-crud' import cuid from 'cuid' import * as displayErrorsActions from 'gModules/displayErrors/actions.js' import * as userActions from 'gModules/users/actions.js' -import * as membershipActions from 'gModules/memberships/actions.js' +import * as membershipActions from 'gModules/userOrganizationMemberships/actions.js' import * as organizationActions from 'gModules/organizations/actions.js' import * as httpRequestActions from 'gModules/httpRequests/actions.js' import {rootUrl} from 'servers/guesstimate-api/constants.js' @@ -10,7 +10,7 @@ import {captureApiError} from 'lib/errors/index.js' import {setupGuesstimateApi} from 'servers/guesstimate-api/constants.js' let sActions = actionCreatorsFor('userOrganizationInvitations') -let relevantAttributes = ['id', 'user_id', 'organization_id'] +let relevantAttributes = ['id', 'email', 'organization_id'] function api(state) { function getToken(state) { @@ -25,73 +25,21 @@ export function fetchByOrganizationId(organizationId) { if (err) { dispatch(displayErrorsActions.newError()) captureApiError('OrganizationsInvitationsFetch', null, null, err, {url: 'fetchMembers'}) - } else if (Invitations) { + } else if (invitations) { + debugger dispatch(fetchSuccess(invitations.items)) - const memberships = invitations.items.map(i => _.get(i, '_embedded.membership')) + const memberships = invitations.items.map(i => _.get(i, '_embedded.membership')).filter(m => !!m) dispatch(membershipActions.fetchSuccess(memberships)) - const users = memberships.map(m => _.get(m, '_embedded.user')) + const users = memberships.map(m => _.get(m, '_embedded.user')).filter(u => !!u) dispatch(userActions.fetchSuccess(users)) } }) } } -export function fetchByUserId(userId) { - return (dispatch, getState) => { - api(getState()).users.getMemberships({userId}, (err, memberships) => { - if (err) { - dispatch(displayErrorsActions.newError()) - captureApiError('OrganizationsMemberFetch', null, null, err, {url: 'fetchMembers'}) - } else if (memberships) { - dispatch(fetchSuccess(memberships.items)) - - const organizations = memberships.items.map(m => _.get(m, '_embedded.organization')) - dispatch(organizationActions.fetchSuccess(organizations)) - } - }) - } -} - -export function fetchSuccess(memberships) { - const formatted = memberships.map(m => _.pick(m, relevantAttributes)) +export function fetchSuccess(invitations) { + const formatted = invitations.map(m => _.pick(m, relevantAttributes)) return sActions.fetchSuccess(formatted) } - -export function destroy(id) { - return (dispatch, getState) => { - dispatch(sActions.deleteStart({id})); - api(getState()).userOrganizationMemberships.destroy({userOrganizationMembershipId: id}, (err, value) => { - if (err) { - captureApiError('OrganizationsMemberDestroy', null, null, err, {url: 'destroyOrganizationMember'}) - } else { - dispatch(sActions.deleteSuccess({id})) - } - }) - } -} - -export function createWithEmail(organizationId, email) { - return (dispatch, getState) => { - const cid = cuid() - let object = {id: cid, organization_id: organizationId} - - dispatch(httpRequestActions.start({id: cid, entity: 'userOrganizationMembershipCreate', metadata: {organizationId, email}})) - api(getState()).organizations.addMember({organizationId, email}, (err, invitation) => { - if (err) { - dispatch(sActions.createError(err, object)) - dispatch(httpRequestActions.failure({id: cid, error: err})) - } - else if (invitation) { - const membership = _.get(invitation, '_embedded.membership') - const user = _.get(invitation, '_embedded.membership._embedded.user') - - if (membership) { dispatch(sActions.createSuccess(membership)) } - if (user) { dispatch(userActions.fetchSuccess([user]))} - - dispatch(httpRequestActions.success({id: cid, response: {hasUser: (!!user)}})) - } - }) - } -} diff --git a/src/modules/userOrganizationMemberships/actions.js b/src/modules/userOrganizationMemberships/actions.js index 704ccf8af..f18d23cb2 100644 --- a/src/modules/userOrganizationMemberships/actions.js +++ b/src/modules/userOrganizationMemberships/actions.js @@ -27,7 +27,7 @@ export function fetchByOrganizationId(organizationId) { } else if (memberships) { dispatch(fetchSuccess(memberships.items)) - const users = memberships.items.map(m => _.get(m, '_embedded.user')) + const users = memberships.items.map(m => _.get(m, '_embedded.user')).filter(u => !!u) dispatch(userActions.fetchSuccess(users)) } }) From f04155dbe7aa5992ea0e93a48841f7121eb2376c Mon Sep 17 00:00:00 2001 From: Matthew McDermott Date: Tue, 31 May 2016 14:47:27 -0700 Subject: [PATCH 16/30] Minor refinements and some basic style. --- src/components/organizations/show/index.js | 76 +++++++++++-------- src/components/organizations/show/style.css | 5 +- .../userOrganizationInvitations/actions.js | 1 - 3 files changed, 48 insertions(+), 34 deletions(-) diff --git a/src/components/organizations/show/index.js b/src/components/organizations/show/index.js index 53f111333..515f51a64 100644 --- a/src/components/organizations/show/index.js +++ b/src/components/organizations/show/index.js @@ -1,20 +1,23 @@ import React, {Component, PropTypes} from 'react' +import { connect } from 'react-redux' + import ReactDOM from 'react-dom' -import { connect } from 'react-redux'; +import Icon from 'react-fa' + import SpaceList from 'gComponents/spaces/list' import * as spaceActions from 'gModules/spaces/actions' import * as organizationActions from 'gModules/organizations/actions' -import * as userOrganizationMembershipActions from 'gModules/userOrganizationMemberships/actions.js' -import { organizationSpaceSelector } from './organizationSpaceSelector.js'; -import { organizationMemberSelector } from './organizationMemberSelector.js'; -import { httpRequestSelector } from './httpRequestSelector.js'; +import * as userOrganizationMembershipActions from 'gModules/userOrganizationMemberships/actions' +import { organizationSpaceSelector } from './organizationSpaceSelector' +import { organizationMemberSelector } from './organizationMemberSelector' +import { httpRequestSelector } from './httpRequestSelector' import SpaceCards from 'gComponents/spaces/cards' -import Container from 'gComponents/utility/container/Container.js' +import Container from 'gComponents/utility/container/Container' import e from 'gEngine/engine' -import './style.css' -import Icon from 'react-fa' -import * as modalActions from 'gModules/modal/actions.js' +import * as modalActions from 'gModules/modal/actions' + +import './style.css' function mapStateToProps(state) { return { @@ -93,6 +96,7 @@ export default class OrganizationShow extends Component{ render () { const {organizationId, organizations, members, invitations} = this.props + const unjoinedInvitees = invitations.filter(i => !_.some(members, m => m.email === i.email)) const {openTab} = this.state const spaces = _.orderBy(this.props.organizationSpaces.asMutable(), ['updated_at'], ['desc']) const organization = organizations.find(u => u.id.toString() === organizationId.toString()) @@ -125,7 +129,7 @@ export default class OrganizationShow extends Component{ ) })} @@ -226,31 +231,40 @@ const MembersIndexSubTab = ({subTab, members, invitations, admin_id, onChangeSub
) -const Invitee = ({email}) => ( -
+const Invitee = ({email, meIsAdmin}) => ( +
-
- - {email} -
+ {meIsAdmin && +
+
+
{email}
+
+
+
invited
+
+ } + {!meIsAdmin && +
+
+
{email}
+
+ }
) const Member = ({user, isAdmin, onRemove, meIsAdmin}) => (
- {meIsAdmin && -
+
+ {meIsAdmin &&
{isAdmin ? 'Admin' : 'Editor'}
-
- {user.sign_in_count > 0 ? 'joined' : 'invited'} -
+
joined
{user.membershipId && !isAdmin && }
-
- } - {!meIsAdmin && -
-
- - {user.name} -
-
- } + } + {!meIsAdmin && +
+ + {user.name} +
+ } +
) diff --git a/src/components/organizations/show/style.css b/src/components/organizations/show/style.css index 81cdf5aee..2e177964c 100644 --- a/src/components/organizations/show/style.css +++ b/src/components/organizations/show/style.css @@ -110,11 +110,14 @@ margin-top: 0.6em; } - img{ + .avatar { float: left; width: 40px; border-radius: 3px; margin-right: 1em; + font-size: 2.9em; + margin-left: 0.3em; + color: $blue-5; } &:last-child { diff --git a/src/modules/userOrganizationInvitations/actions.js b/src/modules/userOrganizationInvitations/actions.js index d8a65850f..d250798c9 100644 --- a/src/modules/userOrganizationInvitations/actions.js +++ b/src/modules/userOrganizationInvitations/actions.js @@ -26,7 +26,6 @@ export function fetchByOrganizationId(organizationId) { dispatch(displayErrorsActions.newError()) captureApiError('OrganizationsInvitationsFetch', null, null, err, {url: 'fetchMembers'}) } else if (invitations) { - debugger dispatch(fetchSuccess(invitations.items)) const memberships = invitations.items.map(i => _.get(i, '_embedded.membership')).filter(m => !!m) From 2e4e540c2e194acc75d6313ef28b1ec9984ba70f Mon Sep 17 00:00:00 2001 From: Matthew McDermott Date: Tue, 31 May 2016 15:01:18 -0700 Subject: [PATCH 17/30] Viewing setup all working. --- src/components/organizations/show/index.js | 43 +++++++++++-------- .../show/organizationMemberSelector.js | 1 + .../userOrganizationMemberships/actions.js | 2 +- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/components/organizations/show/index.js b/src/components/organizations/show/index.js index 515f51a64..02d757d74 100644 --- a/src/components/organizations/show/index.js +++ b/src/components/organizations/show/index.js @@ -95,8 +95,9 @@ export default class OrganizationShow extends Component{ render () { - const {organizationId, organizations, members, invitations} = this.props - const unjoinedInvitees = invitations.filter(i => !_.some(members, m => m.email === i.email)) + const {organizationId, organizations, members, memberships, invitations} = this.props + const unjoinedInvitees = invitations.filter(i => !_.some(memberships, m => m.invitation_id === i.id)) + debugger const {openTab} = this.state const spaces = _.orderBy(this.props.organizationSpaces.asMutable(), ['updated_at'], ['desc']) const organization = organizations.find(u => u.id.toString() === organizationId.toString()) @@ -233,30 +234,32 @@ const MembersIndexSubTab = ({subTab, members, invitations, admin_id, onChangeSub const Invitee = ({email, meIsAdmin}) => (
-
- {meIsAdmin && -
+ {meIsAdmin && +
+
{email}
invited
- } - {!meIsAdmin && +
+ } + {!meIsAdmin && +
{email}
- } -
+
+ }
) const Member = ({user, isAdmin, onRemove, meIsAdmin}) => (
-
- {meIsAdmin && + {meIsAdmin && +
{user.name} @@ -272,14 +275,16 @@ const Member = ({user, isAdmin, onRemove, meIsAdmin}) => ( }
- } - {!meIsAdmin && -
- - {user.name} -
- } -
+
+ } + {!meIsAdmin && +
+
+ + {user.name} +
+
+ }
) diff --git a/src/components/organizations/show/organizationMemberSelector.js b/src/components/organizations/show/organizationMemberSelector.js index 61bceb3ce..4ebf73576 100644 --- a/src/components/organizations/show/organizationMemberSelector.js +++ b/src/components/organizations/show/organizationMemberSelector.js @@ -14,6 +14,7 @@ export const organizationMemberSelector = createSelector( (memberships, invitations, users, organizationId) => { return { members: e.organization.organizationUsers(organizationId, users, memberships), + memberships: e.organization.organizationMemberships(organizationId, memberships), invitations: e.organization.organizationInvitations(organizationId, invitations), } } diff --git a/src/modules/userOrganizationMemberships/actions.js b/src/modules/userOrganizationMemberships/actions.js index f18d23cb2..b632510a1 100644 --- a/src/modules/userOrganizationMemberships/actions.js +++ b/src/modules/userOrganizationMemberships/actions.js @@ -9,7 +9,7 @@ import {captureApiError} from 'lib/errors/index.js' import {setupGuesstimateApi} from 'servers/guesstimate-api/constants.js' let sActions = actionCreatorsFor('userOrganizationMemberships') -let relevantAttributes = ['id', 'user_id', 'organization_id'] +let relevantAttributes = ['id', 'user_id', 'organization_id', 'invitation_id'] function api(state) { function getToken(state) { From 6cf5a57d852da829a27e52ed151f32861b1a742c Mon Sep 17 00:00:00 2001 From: Matthew McDermott Date: Tue, 31 May 2016 15:12:14 -0700 Subject: [PATCH 18/30] Receive the new invitations after membership creation. --- src/modules/userOrganizationMemberships/actions.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/modules/userOrganizationMemberships/actions.js b/src/modules/userOrganizationMemberships/actions.js index b632510a1..a08b31d87 100644 --- a/src/modules/userOrganizationMemberships/actions.js +++ b/src/modules/userOrganizationMemberships/actions.js @@ -2,6 +2,7 @@ import {actionCreatorsFor} from 'redux-crud' import cuid from 'cuid' import * as displayErrorsActions from 'gModules/displayErrors/actions.js' import * as userActions from 'gModules/users/actions.js' +import * as invitationActions from 'gModules/userOrganizationInvitations/actions.js' import * as organizationActions from 'gModules/organizations/actions.js' import * as httpRequestActions from 'gModules/httpRequests/actions.js' import {rootUrl} from 'servers/guesstimate-api/constants.js' @@ -80,6 +81,8 @@ export function createWithEmail(organizationId, email) { dispatch(httpRequestActions.failure({id: cid, error: err})) } else if (invitation) { + dispatch(invitationActions.fetchSuccess([invitation])) + const membership = _.get(invitation, '_embedded.membership') const user = _.get(invitation, '_embedded.membership._embedded.user') From fdc069c4076f3ff34309b42f3ee3b8111b9910d0 Mon Sep 17 00:00:00 2001 From: Matthew McDermott Date: Tue, 31 May 2016 15:17:29 -0700 Subject: [PATCH 19/30] Basic cleanup. --- src/components/organizations/show/index.js | 1 - .../userOrganizationInvitations/actions.js | 17 ++++++++------- .../userOrganizationMemberships/actions.js | 21 +++++++++++-------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/components/organizations/show/index.js b/src/components/organizations/show/index.js index 02d757d74..bd70d8408 100644 --- a/src/components/organizations/show/index.js +++ b/src/components/organizations/show/index.js @@ -97,7 +97,6 @@ export default class OrganizationShow extends Component{ render () { const {organizationId, organizations, members, memberships, invitations} = this.props const unjoinedInvitees = invitations.filter(i => !_.some(memberships, m => m.invitation_id === i.id)) - debugger const {openTab} = this.state const spaces = _.orderBy(this.props.organizationSpaces.asMutable(), ['updated_at'], ['desc']) const organization = organizations.find(u => u.id.toString() === organizationId.toString()) diff --git a/src/modules/userOrganizationInvitations/actions.js b/src/modules/userOrganizationInvitations/actions.js index d250798c9..06ada6809 100644 --- a/src/modules/userOrganizationInvitations/actions.js +++ b/src/modules/userOrganizationInvitations/actions.js @@ -1,13 +1,14 @@ import {actionCreatorsFor} from 'redux-crud' + import cuid from 'cuid' -import * as displayErrorsActions from 'gModules/displayErrors/actions.js' -import * as userActions from 'gModules/users/actions.js' -import * as membershipActions from 'gModules/userOrganizationMemberships/actions.js' -import * as organizationActions from 'gModules/organizations/actions.js' -import * as httpRequestActions from 'gModules/httpRequests/actions.js' -import {rootUrl} from 'servers/guesstimate-api/constants.js' -import {captureApiError} from 'lib/errors/index.js' -import {setupGuesstimateApi} from 'servers/guesstimate-api/constants.js' + +import * as displayErrorsActions from 'gModules/displayErrors/actions' +import * as userActions from 'gModules/users/actions' +import * as membershipActions from 'gModules/userOrganizationMemberships/actions' + +import {captureApiError} from 'lib/errors/index' + +import {setupGuesstimateApi} from 'servers/guesstimate-api/constants' let sActions = actionCreatorsFor('userOrganizationInvitations') let relevantAttributes = ['id', 'email', 'organization_id'] diff --git a/src/modules/userOrganizationMemberships/actions.js b/src/modules/userOrganizationMemberships/actions.js index a08b31d87..c5eef708f 100644 --- a/src/modules/userOrganizationMemberships/actions.js +++ b/src/modules/userOrganizationMemberships/actions.js @@ -1,13 +1,16 @@ import {actionCreatorsFor} from 'redux-crud' + import cuid from 'cuid' -import * as displayErrorsActions from 'gModules/displayErrors/actions.js' -import * as userActions from 'gModules/users/actions.js' -import * as invitationActions from 'gModules/userOrganizationInvitations/actions.js' -import * as organizationActions from 'gModules/organizations/actions.js' -import * as httpRequestActions from 'gModules/httpRequests/actions.js' -import {rootUrl} from 'servers/guesstimate-api/constants.js' -import {captureApiError} from 'lib/errors/index.js' -import {setupGuesstimateApi} from 'servers/guesstimate-api/constants.js' + +import * as displayErrorsActions from 'gModules/displayErrors/actions' +import * as userActions from 'gModules/users/actions' +import * as invitationActions from 'gModules/userOrganizationInvitations/actions' +import * as organizationActions from 'gModules/organizations/actions' +import * as httpRequestActions from 'gModules/httpRequests/actions' + +import {captureApiError} from 'lib/errors/index' + +import {setupGuesstimateApi} from 'servers/guesstimate-api/constants' let sActions = actionCreatorsFor('userOrganizationMemberships') let relevantAttributes = ['id', 'user_id', 'organization_id', 'invitation_id'] @@ -84,7 +87,7 @@ export function createWithEmail(organizationId, email) { dispatch(invitationActions.fetchSuccess([invitation])) const membership = _.get(invitation, '_embedded.membership') - const user = _.get(invitation, '_embedded.membership._embedded.user') + const user = _.get(membership, '_embedded.user') if (membership) { dispatch(sActions.createSuccess(membership)) } if (user) { dispatch(userActions.fetchSuccess([user]))} From 9ef42923ef975fe0aa13de4fc4b9285c37ba5d7d Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Tue, 31 May 2016 15:55:42 -0700 Subject: [PATCH 20/30] Minor changes to mail icon --- src/components/organizations/show/style.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/organizations/show/style.css b/src/components/organizations/show/style.css index 2e177964c..bd8e72a30 100644 --- a/src/components/organizations/show/style.css +++ b/src/components/organizations/show/style.css @@ -118,6 +118,13 @@ font-size: 2.9em; margin-left: 0.3em; color: $blue-5; + text-align: center; + } + + .avatar .fa{ + font-size: 0.6em; + margin-top: 5px; + opacity: 0.7; } &:last-child { From f39334f11fde83e9eb8caae83e3552607261d9ab Mon Sep 17 00:00:00 2001 From: Matthew McDermott Date: Wed, 1 Jun 2016 11:31:00 -0700 Subject: [PATCH 21/30] Begin passing updated_at back up. --- src/modules/spaces/actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/spaces/actions.js b/src/modules/spaces/actions.js index b6e7204b7..4ad43741e 100644 --- a/src/modules/spaces/actions.js +++ b/src/modules/spaces/actions.js @@ -158,7 +158,7 @@ export function generalUpdate(spaceId, params) { dispatch(sActions.updateStart(space)) dispatch(changeActionState('SAVING')) - const updateMsg = {...params} + const updateMsg = {...params, previous_updated_at: space.updated_at} api(getState()).models.update(spaceId, updateMsg, (err, value) => { if (err) { if (err === 'Conflict') { From c6cbbce07e51e633e56b25d1b2340b00a7c813ac Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Wed, 1 Jun 2016 12:30:21 -0700 Subject: [PATCH 22/30] Organization users page should hide for logged out users --- src/components/organizations/show/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/organizations/show/index.js b/src/components/organizations/show/index.js index bd70d8408..53760078c 100644 --- a/src/components/organizations/show/index.js +++ b/src/components/organizations/show/index.js @@ -118,14 +118,14 @@ export default class OrganizationShow extends Component{ }
- {(openTab === 'MODELS') && spaces && + {(openTab === 'MODELS' || !meIsMember) && spaces && } - {(openTab === 'MEMBERS') && members && organization && + {(openTab === 'MEMBERS') && meIsMember && members && organization && Date: Wed, 1 Jun 2016 14:17:51 -0700 Subject: [PATCH 23/30] WIP --- src/modules/organizations/actions.js | 7 ++++++- src/modules/spaces/actions.js | 2 +- src/modules/userOrganizationMemberships/actions.js | 9 +++++---- src/modules/users/actions.js | 11 +++++++---- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/modules/organizations/actions.js b/src/modules/organizations/actions.js index 33c1cade8..1e62842b1 100644 --- a/src/modules/organizations/actions.js +++ b/src/modules/organizations/actions.js @@ -21,8 +21,13 @@ export function fetchById(organizationId) { dispatch(displayErrorsActions.newError()) captureApiError('OrganizationsFetch', null, null, err, {url: 'fetch'}) } else if (organization) { - dispatch(userOrganizationMembershipActions.fetchByOrganizationId(organizationId)) dispatch(sActions.fetchSuccess([organization])) + + const members = !!organization.members ? organization.members : [] + const invitations = !!organization.invitations ? organization.invitations : [] + + const formatted = members.map(m => _.pick(m, ['id', 'user_id', 'organization_id'])) + dispatch(userOrganizationMembershipActions.sActions.fetchSuccess(formatted)) } }) } diff --git a/src/modules/spaces/actions.js b/src/modules/spaces/actions.js index b6e7204b7..ac762bc95 100644 --- a/src/modules/spaces/actions.js +++ b/src/modules/spaces/actions.js @@ -86,7 +86,7 @@ export function fetch({userId, organizationId}) { const formatted = value.items.map(d => _.pick(d, ['id', 'name', 'description', 'user_id', 'organization_id', 'updated_at', 'metric_count', 'is_private', 'screenshot', 'big_screenshot'])) dispatch(sActions.fetchSuccess(formatted)) - const users = value.items.map(d => _.get(d, 'user')) + const users = value.items.map(d => _.get(d, 'user')).filter(u => !!u) dispatch(userActions.fetchSuccess(users)) } }) diff --git a/src/modules/userOrganizationMemberships/actions.js b/src/modules/userOrganizationMemberships/actions.js index 0f0031bd4..e0a75027c 100644 --- a/src/modules/userOrganizationMemberships/actions.js +++ b/src/modules/userOrganizationMemberships/actions.js @@ -6,7 +6,7 @@ import {rootUrl} from 'servers/guesstimate-api/constants.js' import {captureApiError} from 'lib/errors/index.js' import {setupGuesstimateApi} from 'servers/guesstimate-api/constants.js' -let sActions = actionCreatorsFor('userOrganizationMemberships') +export const sActions = actionCreatorsFor('userOrganizationMemberships') function api(state) { function getToken(state) { @@ -22,10 +22,11 @@ export function fetchByOrganizationId(organizationId) { dispatch(displayErrorsActions.newError()) captureApiError('OrganizationsMemberFetch', null, null, err, {url: 'fetchMembers'}) } else if (members) { + console.log("sneaky!") const formatted = members.items.map(m => _.pick(m, ['id', 'user_id', 'organization_id'])) dispatch(sActions.fetchSuccess(formatted)) - const users = members.items.map(m => _.get(m, '_embedded.user')) + const users = members.items.map(m => _.get(m, '_embedded.user')).filter(u => !!u) dispatch(userActions.fetchSuccess(users)) } }) @@ -42,8 +43,8 @@ export function fetchByUserId(userId) { const formatted = memberships.items.map(m => _.pick(m, ['id', 'user_id', 'organization_id'])) dispatch(sActions.fetchSuccess(formatted)) - const organizations = memberships.items.map(m => _.get(m, '_embedded.organization')) - dispatch(organizationActions.fetchSuccess(organizations)) + //const organizations = memberships.items.map(m => _.get(m, '_embedded.organization')).filter(o => !!o) + //dispatch(organizationActions.fetchSuccess(organizations)) } }) } diff --git a/src/modules/users/actions.js b/src/modules/users/actions.js index 7acd48035..276d2d688 100644 --- a/src/modules/users/actions.js +++ b/src/modules/users/actions.js @@ -26,8 +26,8 @@ export function fetch({auth0_id}) { if (err) { dispatch(displayErrorsActions.newError()) captureApiError('UsersFetch', null, null, null, {url: 'usersFetchError'}) - } - else if (data) { + } else if (data) { + console.log("UsersFetch") const action = sActions.fetchSuccess(data.items) const me = data.items[0] dispatch(meActions.guesstimateMeLoaded(me)) @@ -43,8 +43,8 @@ export function fetchById(userId) { if (err) { dispatch(displayErrorsActions.newError()) captureApiError('UsersFetch', null, null, err, {url: 'fetch'}) - } - else if (user) { + } else if (user) { + console.log("fetchById") dispatch(sActions.fetchSuccess([user])) if (getState().me.id === user.id){ dispatch(meActions.guesstimateMeLoaded(user)) @@ -62,6 +62,7 @@ function formatUsers(unformatted) { export function fromSearch(spaces) { return (dispatch) => { const users = spaces.map(s => s.user_info) + console.log("fromSearch") dispatch(sActions.fetchSuccess(formatUsers(users))) } } @@ -79,6 +80,7 @@ export function create(object) { } else if (_.isEmpty(user)) { generalError('UserCreate-EmptyResponse', {cid: newUser.id, url: 'userscreate'}) + console.log("fetchSuccess") dispatch(sActions.fetchSuccess([user])) dispatch(displayErrorsActions.newError()) } else { @@ -91,6 +93,7 @@ export function create(object) { export function fetchSuccess(users) { return (dispatch, getState) => { + console.log("WOOOO") dispatch(sActions.fetchSuccess(formatUsers(_.uniqBy(users, 'id')))) } } From f38730101ac7319b3de76b71c8bc9884cf850d7a Mon Sep 17 00:00:00 2001 From: Matthew McDermott Date: Wed, 1 Jun 2016 14:44:53 -0700 Subject: [PATCH 24/30] removing console.log statements --- src/modules/userOrganizationMemberships/actions.js | 1 - src/modules/users/actions.js | 5 ----- 2 files changed, 6 deletions(-) diff --git a/src/modules/userOrganizationMemberships/actions.js b/src/modules/userOrganizationMemberships/actions.js index e0a75027c..73cc9e131 100644 --- a/src/modules/userOrganizationMemberships/actions.js +++ b/src/modules/userOrganizationMemberships/actions.js @@ -22,7 +22,6 @@ export function fetchByOrganizationId(organizationId) { dispatch(displayErrorsActions.newError()) captureApiError('OrganizationsMemberFetch', null, null, err, {url: 'fetchMembers'}) } else if (members) { - console.log("sneaky!") const formatted = members.items.map(m => _.pick(m, ['id', 'user_id', 'organization_id'])) dispatch(sActions.fetchSuccess(formatted)) diff --git a/src/modules/users/actions.js b/src/modules/users/actions.js index 276d2d688..6fe9de727 100644 --- a/src/modules/users/actions.js +++ b/src/modules/users/actions.js @@ -27,7 +27,6 @@ export function fetch({auth0_id}) { dispatch(displayErrorsActions.newError()) captureApiError('UsersFetch', null, null, null, {url: 'usersFetchError'}) } else if (data) { - console.log("UsersFetch") const action = sActions.fetchSuccess(data.items) const me = data.items[0] dispatch(meActions.guesstimateMeLoaded(me)) @@ -44,7 +43,6 @@ export function fetchById(userId) { dispatch(displayErrorsActions.newError()) captureApiError('UsersFetch', null, null, err, {url: 'fetch'}) } else if (user) { - console.log("fetchById") dispatch(sActions.fetchSuccess([user])) if (getState().me.id === user.id){ dispatch(meActions.guesstimateMeLoaded(user)) @@ -62,7 +60,6 @@ function formatUsers(unformatted) { export function fromSearch(spaces) { return (dispatch) => { const users = spaces.map(s => s.user_info) - console.log("fromSearch") dispatch(sActions.fetchSuccess(formatUsers(users))) } } @@ -80,7 +77,6 @@ export function create(object) { } else if (_.isEmpty(user)) { generalError('UserCreate-EmptyResponse', {cid: newUser.id, url: 'userscreate'}) - console.log("fetchSuccess") dispatch(sActions.fetchSuccess([user])) dispatch(displayErrorsActions.newError()) } else { @@ -93,7 +89,6 @@ export function create(object) { export function fetchSuccess(users) { return (dispatch, getState) => { - console.log("WOOOO") dispatch(sActions.fetchSuccess(formatUsers(_.uniqBy(users, 'id')))) } } From dde15fb351bd17158344355664d43f70e0a5114a Mon Sep 17 00:00:00 2001 From: Matthew McDermott Date: Wed, 1 Jun 2016 15:33:56 -0700 Subject: [PATCH 25/30] Always fetch and grab orgs from memberships. --- src/components/organizations/show/index.js | 14 +++----------- src/modules/userOrganizationMemberships/actions.js | 4 ++-- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/components/organizations/show/index.js b/src/components/organizations/show/index.js index 8ff2c0eab..28617ed92 100644 --- a/src/components/organizations/show/index.js +++ b/src/components/organizations/show/index.js @@ -33,19 +33,11 @@ export default class OrganizationShow extends Component{ } componentWillMount() { - this.considerFetch(this.props) + this.fetchData() } - componentDidUpdate(newProps) { - this.considerFetch(newProps) - } - - considerFetch(props) { - const needsData = !(_.has(props, 'organizationSpaces') && props.organizationSpaces.length > 0) - - if (needsData) { - this.fetchData() - } + componentDidUpdate() { + this.fetchData() } fetchData() { diff --git a/src/modules/userOrganizationMemberships/actions.js b/src/modules/userOrganizationMemberships/actions.js index 73cc9e131..a54aa7e56 100644 --- a/src/modules/userOrganizationMemberships/actions.js +++ b/src/modules/userOrganizationMemberships/actions.js @@ -42,8 +42,8 @@ export function fetchByUserId(userId) { const formatted = memberships.items.map(m => _.pick(m, ['id', 'user_id', 'organization_id'])) dispatch(sActions.fetchSuccess(formatted)) - //const organizations = memberships.items.map(m => _.get(m, '_embedded.organization')).filter(o => !!o) - //dispatch(organizationActions.fetchSuccess(organizations)) + const organizations = memberships.items.map(m => _.get(m, '_embedded.organization')).filter(o => !!o) + dispatch(organizationActions.fetchSuccess(organizations)) } }) } From e9a39633333ff6e278fa5b320f794cf940fde9c7 Mon Sep 17 00:00:00 2001 From: Matthew McDermott Date: Wed, 1 Jun 2016 16:28:21 -0700 Subject: [PATCH 26/30] Added necessary class to MeIsMember mode. --- src/components/organizations/show/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/organizations/show/index.js b/src/components/organizations/show/index.js index 34d4ac871..4ecf2ad0d 100644 --- a/src/components/organizations/show/index.js +++ b/src/components/organizations/show/index.js @@ -223,6 +223,7 @@ const MembersIndexSubTab = ({subTab, members, invitations, admin_id, onChangeSub
) +// TODO(matthew): Invitee only renders if I'm an admin. So strip out the unnecessary stuff. const Invitee = ({email, meIsAdmin}) => (
{meIsAdmin && @@ -271,7 +272,7 @@ const Member = ({user, isAdmin, onRemove, meIsAdmin}) => ( {!meIsAdmin && From 52a9badbad16b8c955513e7d19fc41f98dccfd32 Mon Sep 17 00:00:00 2001 From: Matthew McDermott Date: Wed, 1 Jun 2016 16:34:49 -0700 Subject: [PATCH 27/30] Removing unnecessary meIsAdmin dependence. --- src/components/organizations/show/index.js | 29 +++++++--------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/src/components/organizations/show/index.js b/src/components/organizations/show/index.js index 4ecf2ad0d..25a61c5c4 100644 --- a/src/components/organizations/show/index.js +++ b/src/components/organizations/show/index.js @@ -207,12 +207,11 @@ const MembersIndexSubTab = ({subTab, members, invitations, admin_id, onChangeSub /> ) })} - {invitations.map(i => { + {meIsAdmin && invitations.map(i => { return ( ) })} @@ -226,25 +225,15 @@ const MembersIndexSubTab = ({subTab, members, invitations, admin_id, onChangeSub // TODO(matthew): Invitee only renders if I'm an admin. So strip out the unnecessary stuff. const Invitee = ({email, meIsAdmin}) => (
- {meIsAdmin && -
-
-
-
{email}
-
-
-
invited
-
+
+
+
+
{email}
- } - {!meIsAdmin && -
-
-
-
{email}
-
-
- } +
+
invited
+
+
) From 1674df720e6b0ddbed89694ab30b26cbc3702cb4 Mon Sep 17 00:00:00 2001 From: Matthew McDermott Date: Wed, 1 Jun 2016 16:39:19 -0700 Subject: [PATCH 28/30] Grab memberships (fixed typo members -> memberships) --- src/modules/organizations/actions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/organizations/actions.js b/src/modules/organizations/actions.js index 99cf4a276..758e9cda6 100644 --- a/src/modules/organizations/actions.js +++ b/src/modules/organizations/actions.js @@ -25,10 +25,10 @@ export function fetchById(organizationId) { } else if (organization) { dispatch(sActions.fetchSuccess([organization])) - const members = !!organization.members ? organization.members : [] + const memberships = !!organization.memberships ? organization.memberships : [] const invitations = !!organization.invitations ? organization.invitations : [] - dispatch(userOrganizationMembershipActions.fetchSuccess(members)) + dispatch(userOrganizationMembershipActions.fetchSuccess(memberships)) dispatch(userOrganizationInvitationActions.fetchSuccess(invitations)) } }) From e98997583740bd10ef80343f70bd3b70e6ff8d63 Mon Sep 17 00:00:00 2001 From: Matthew McDermott Date: Wed, 1 Jun 2016 16:48:00 -0700 Subject: [PATCH 29/30] Need to extract users from memberships with this new way. --- src/modules/userOrganizationMemberships/actions.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/modules/userOrganizationMemberships/actions.js b/src/modules/userOrganizationMemberships/actions.js index 220e1d602..f0a4baa05 100644 --- a/src/modules/userOrganizationMemberships/actions.js +++ b/src/modules/userOrganizationMemberships/actions.js @@ -30,9 +30,6 @@ export function fetchByOrganizationId(organizationId) { captureApiError('OrganizationsMemberFetch', null, null, err, {url: 'fetchMembers'}) } else if (memberships) { dispatch(fetchSuccess(memberships.items)) - - const users = memberships.items.map(m => _.get(m, '_embedded.user')).filter(u => !!u) - dispatch(userActions.fetchSuccess(users)) } }) } @@ -55,8 +52,13 @@ export function fetchByUserId(userId) { } export function fetchSuccess(memberships) { - const formatted = memberships.map(m => _.pick(m, relevantAttributes)) - return sActions.fetchSuccess(formatted) + return (dispatch, getState) => { + const formatted = memberships.map(m => _.pick(m, relevantAttributes)) + dispatch(sActions.fetchSuccess(formatted)) + + const users = memberships.map(m => _.get(m, '_embedded.user')).filter(u => !!u) + dispatch(userActions.fetchSuccess(users)) + } } export function destroy(id) { From f4cc4c48a4c85f82670c98b93bb3d4e10d341581 Mon Sep 17 00:00:00 2001 From: Matthew McDermott Date: Wed, 1 Jun 2016 17:23:12 -0700 Subject: [PATCH 30/30] Removing unnecessary cruft. --- src/components/organizations/show/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/organizations/show/index.js b/src/components/organizations/show/index.js index 25a61c5c4..93601f25a 100644 --- a/src/components/organizations/show/index.js +++ b/src/components/organizations/show/index.js @@ -222,8 +222,7 @@ const MembersIndexSubTab = ({subTab, members, invitations, admin_id, onChangeSub
) -// TODO(matthew): Invitee only renders if I'm an admin. So strip out the unnecessary stuff. -const Invitee = ({email, meIsAdmin}) => ( +const Invitee = ({email}) => (