-
-
- {organization &&
-
-
-
-
-
- {organization.name}
-
-
- {members && members.map(m => {
- return ()
- })}
-
-
- }
-
-
+
-
- {spaces &&
-
- }
-
+
+
+ {meIsMember &&
+
+ }
+
+
+ {(openTab === 'MODELS' || !meIsMember) && spaces &&
+
+ }
+
+ {(openTab === 'MEMBERS') && meIsMember && members && organization &&
+ {this.setState({subMembersTab: name})}}
+ httpRequests={this.props.httpRequests}
+ meIsAdmin={meIsAdmin}
+ />
+ }
)
}
}
+
+const OrganizationHeader = ({organization}) => (
+
+
+
+ {organization &&
+
+
+
+
+ {organization.name}
+
+
+
+ }
+
+
+)
+
+const OrganizationTabButtons = ({tabs, openTab, changeTab}) => (
+
+)
+
+const MembersTab = ({subTab, members, invitations, admin_id, onRemove, addUser, onChangeSubTab, httpRequests, meIsAdmin}) => (
+
+ {subTab === 'ADD' &&
+
+ }
+ {subTab === 'INDEX' &&
+
+ }
+
+)
+
+const MembersIndexSubTab = ({subTab, members, invitations, admin_id, onChangeSubTab, onRemove, meIsAdmin}) => (
+
+
+ {subTab === 'INDEX' && meIsAdmin &&
+
{onChangeSubTab('ADD')}}>
+ Add Members
+
+ }
+
+
+ {subTab === 'INDEX' &&
+
+
+ {members.map(m => {
+ return (
+ {onRemove(m)}}
+ meIsAdmin={meIsAdmin}
+ />
+ )
+ })}
+ {meIsAdmin && invitations.map(i => {
+ return (
+
+ )
+ })}
+
+
+ }
+
+
+)
+
+const Invitee = ({email}) => (
+
+)
+
+const Member = ({user, isAdmin, onRemove, meIsAdmin}) => (
+
+ {meIsAdmin &&
+
+
+
+ {isAdmin ? 'Admin' : 'Editor'}
+
+
joined
+
+ {user.membershipId && !isAdmin &&
+
+
+
+ }
+
+
+ }
+ {!meIsAdmin &&
+
+ }
+
+)
+
+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 MembersAddSubTab extends Component{
+ state = { value: '' }
+
+ componentDidMount() {
+ this.refs.input.focus()
+ }
+
+ _submit() {
+ this.props.addUser(this.state.value)
+ this.setState({value: ''})
+ }
+
+ _onChange(e) {
+ 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)
+ const isEmpty = _.isEmpty(value)
+ const buttonColor = (isValid || isEmpty) ? 'green' : 'grey'
+
+ const requests = _.orderBy(_.cloneDeep(this.props.httpRequests), ['created_at'], ['desc'])
+ return(
+
+
+
{this.props.onChangeSubTab('INDEX')}}>
+ 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.
+
+
+ Email Address
+
+
+
+ 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 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 (
+
+ {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/organizationMemberSelector.js b/src/components/organizations/show/organizationMemberSelector.js
index b8464fc15..4ebf73576 100644
--- a/src/components/organizations/show/organizationMemberSelector.js
+++ b/src/components/organizations/show/organizationMemberSelector.js
@@ -2,16 +2,20 @@ 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),
+ memberships: e.organization.organizationMemberships(organizationId, memberships),
+ invitations: e.organization.organizationInvitations(organizationId, invitations),
}
}
);
diff --git a/src/components/organizations/show/style.css b/src/components/organizations/show/style.css
index 032933816..bd8e72a30 100644
--- a/src/components/organizations/show/style.css
+++ b/src/components/organizations/show/style.css
@@ -1,28 +1,134 @@
-.organizationShow .main-organization-tag {
- text-align: center;
- margin-right: 0.5em;
- margin-top: 0.25em;
-}
+@import './styles/variables.css';
+
+.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;
+ }
-.organizationShow .main-organization-tag img{
- max-width: 10em;
- max-height: 8em;
- border-radius: 0.5em;
- margin-bottom: 10px;
+ .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 .members {
- margin-top: 30px;
- float: left;
+.OrganizationShow .MembersAddSubTab .ui.form{
+ font-size: 1.2em;
+ margin-top: 4em;
+ margin-bottom: 4em;
}
-.organizationShow .member {
- float: left;
- margin-right: 10px;
- margin-bottom: 10px;
+.OrganizationShow .MembersAddSubTab p{
+ color: #666;
+ font-size: 1.2em;
+ line-height: 1.7em;
}
-.organizationShow .member img{
- width: 40px;
- border-radius: 3em;
+.OrganizationShow .MembersTab {
+ margin-top: 7em;
+
+ .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;
+ font-size: 1.2em !important;
+ padding: 7px 10px !important;
+ }
+
+ .button.remove i{
+ margin: 0 !important;
+ }
+
+ .name {
+ float: left;
+ 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;
+ }
+
+ .avatar {
+ float: left;
+ width: 40px;
+ border-radius: 3px;
+ margin-right: 1em;
+ 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 {
+ border-bottom: none;
+ }
+ }
}
diff --git a/src/components/spaces/cards/style.css b/src/components/spaces/cards/style.css
index 413c33826..46cd36d48 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 0 auto;
}
@@ -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/lib/engine/organization.js b/src/lib/engine/organization.js
index cf71b5625..3bb6afc57 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,18 @@ 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))
+}
+
+export function organizationInvitations(organizationId, invitations) {
+ return _.filter(invitations, e => sameId(e.organization_id, organizationId))
}
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/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/Organizations.js b/src/lib/guesstimate_api/resources/Organizations.js
index a84ac0660..0c4748f41 100644
--- a/src/lib/guesstimate_api/resources/Organizations.js
+++ b/src/lib/guesstimate_api/resources/Organizations.js
@@ -14,4 +14,19 @@ export default class Organizations extends AbstractResource {
this.guesstimateMethod({url, method})(callback)
}
+
+ addMember({organizationId, email}, callback) {
+ const url = `organizations/${organizationId}/members`
+ const method = 'POST'
+ const data = {email}
+
+ 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/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/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..55115e7cd
--- /dev/null
+++ b/src/modules/httpRequests/reducer.js
@@ -0,0 +1,47 @@
+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,
+ created_at: Date.now(),
+ 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,
+ response: 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/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}
+ Confirm
+ Cancel
+
+ )
+ }
+}
+
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/modules/organizations/actions.js b/src/modules/organizations/actions.js
index 1e62842b1..758e9cda6 100644
--- a/src/modules/organizations/actions.js
+++ b/src/modules/organizations/actions.js
@@ -1,9 +1,11 @@
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 {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')
@@ -23,11 +25,11 @@ 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 : []
- const formatted = members.map(m => _.pick(m, ['id', 'user_id', 'organization_id']))
- dispatch(userOrganizationMembershipActions.sActions.fetchSuccess(formatted))
+ dispatch(userOrganizationMembershipActions.fetchSuccess(memberships))
+ dispatch(userOrganizationInvitationActions.fetchSuccess(invitations))
}
})
}
@@ -35,7 +37,26 @@ 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']))
dispatch(sActions.fetchSuccess(formatted))
}
}
+
+export function addMember(organizationId, email) {
+ return (dispatch, getState) => {
+ const cid = cuid()
+ 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) {
+ dispatch(sActions.createError(err, object))
+ }
+ else if (membership) {
+ dispatch(userActions.fetchSuccess([membership._embedded.user]))
+ dispatch(membershipActions.createSuccess([membership]))
+ }
+ })
+ }
+}
diff --git a/src/modules/reducers.js b/src/modules/reducers.js
index 91719e892..a450360fe 100644
--- a/src/modules/reducers.js
+++ b/src/modules/reducers.js
@@ -16,6 +16,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 };
@@ -33,6 +34,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)),
@@ -40,6 +42,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/userOrganizationInvitations/actions.js b/src/modules/userOrganizationInvitations/actions.js
new file mode 100644
index 000000000..06ada6809
--- /dev/null
+++ b/src/modules/userOrganizationInvitations/actions.js
@@ -0,0 +1,45 @@
+import {actionCreatorsFor} from 'redux-crud'
+
+import cuid from 'cuid'
+
+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']
+
+function api(state) {
+ function getToken(state) {
+ return _.get(state, 'me.token')
+ }
+ return setupGuesstimateApi(getToken(state))
+}
+
+export function fetchByOrganizationId(organizationId) {
+ return (dispatch, getState) => {
+ api(getState()).organizations.getInvitations({organizationId}, (err, invitations) => {
+ if (err) {
+ dispatch(displayErrorsActions.newError())
+ captureApiError('OrganizationsInvitationsFetch', null, null, err, {url: 'fetchMembers'})
+ } else if (invitations) {
+ dispatch(fetchSuccess(invitations.items))
+
+ 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')).filter(u => !!u)
+ dispatch(userActions.fetchSuccess(users))
+ }
+ })
+ }
+}
+
+export function fetchSuccess(invitations) {
+ const formatted = invitations.map(m => _.pick(m, relevantAttributes))
+ return sActions.fetchSuccess(formatted)
+}
diff --git a/src/modules/userOrganizationMemberships/actions.js b/src/modules/userOrganizationMemberships/actions.js
index a54aa7e56..f0a4baa05 100644
--- a/src/modules/userOrganizationMemberships/actions.js
+++ b/src/modules/userOrganizationMemberships/actions.js
@@ -1,12 +1,19 @@
import {actionCreatorsFor} from 'redux-crud'
-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 {rootUrl} from 'servers/guesstimate-api/constants.js'
-import {captureApiError} from 'lib/errors/index.js'
-import {setupGuesstimateApi} from 'servers/guesstimate-api/constants.js'
-export const sActions = actionCreatorsFor('userOrganizationMemberships')
+import cuid from 'cuid'
+
+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'
+
+const sActions = actionCreatorsFor('userOrganizationMemberships')
+const relevantAttributes = ['id', 'user_id', 'organization_id', 'invitation_id']
function api(state) {
function getToken(state) {
@@ -17,16 +24,12 @@ 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))
-
- const users = members.items.map(m => _.get(m, '_embedded.user')).filter(u => !!u)
- dispatch(userActions.fetchSuccess(users))
+ } else if (memberships) {
+ dispatch(fetchSuccess(memberships.items))
}
})
}
@@ -39,8 +42,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')).filter(o => !!o)
dispatch(organizationActions.fetchSuccess(organizations))
@@ -48,3 +50,52 @@ export function fetchByUserId(userId) {
})
}
}
+
+export function fetchSuccess(memberships) {
+ 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) {
+ 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) {
+ dispatch(invitationActions.fetchSuccess([invitation]))
+
+ const membership = _.get(invitation, '_embedded.membership')
+ const user = _.get(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/users/actions.js b/src/modules/users/actions.js
index 6fe9de727..072760aaa 100644
--- a/src/modules/users/actions.js
+++ b/src/modules/users/actions.js
@@ -54,7 +54,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/routes/main.css b/src/routes/main.css
index 12541667f..ac1446c0a 100644
--- a/src/routes/main.css
+++ b/src/routes/main.css
@@ -25,3 +25,23 @@ 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;
+ line-height: 1.8em;
+}
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'}) },
})
diff --git a/src/styles/variables.css b/src/styles/variables.css
index d2f203d3a..e12c02724 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: #1EB128;
+
$purple-1: #7970A9;
$purple-2: #6C5A8C;
$purple-3: #EDE4F9;