mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-09-21 15:36:22 +08:00
Merge pull request #793 from mlorb/ml-sci-1498
Adds invite users modal [SCI-1498]
This commit is contained in:
commit
668a92a3d8
5
Gemfile
5
Gemfile
|
@ -70,7 +70,10 @@ gem 'paperclip', '~> 5.1' # File attachment, image attachment library
|
||||||
gem 'aws-sdk', '~> 2'
|
gem 'aws-sdk', '~> 2'
|
||||||
|
|
||||||
gem 'delayed_job_active_record'
|
gem 'delayed_job_active_record'
|
||||||
gem 'devise-async'
|
gem 'devise-async',
|
||||||
|
git: 'https://github.com/mhfs/devise-async.git',
|
||||||
|
branch: 'devise-4.x'
|
||||||
|
|
||||||
gem 'ruby-graphviz', '~> 1.2' # Graphviz for rails
|
gem 'ruby-graphviz', '~> 1.2' # Graphviz for rails
|
||||||
gem 'tinymce-rails', '~> 4.6.4' # Rich text editor
|
gem 'tinymce-rails', '~> 4.6.4' # Rich text editor
|
||||||
|
|
||||||
|
|
12
Gemfile.lock
12
Gemfile.lock
|
@ -21,6 +21,14 @@ GIT
|
||||||
activejob (>= 4.2)
|
activejob (>= 4.2)
|
||||||
paperclip (>= 3.3)
|
paperclip (>= 3.3)
|
||||||
|
|
||||||
|
GIT
|
||||||
|
remote: https://github.com/mhfs/devise-async.git
|
||||||
|
revision: 177f6363a002f7ff28f1d289c8cab7ad8d9cb8c5
|
||||||
|
branch: devise-4.x
|
||||||
|
specs:
|
||||||
|
devise-async (0.10.2)
|
||||||
|
devise (>= 4.0)
|
||||||
|
|
||||||
GIT
|
GIT
|
||||||
remote: https://github.com/phatworx/devise_security_extension.git
|
remote: https://github.com/phatworx/devise_security_extension.git
|
||||||
revision: b2ee978af7d49f0fb0e7271c6ac074dfb4d39353
|
revision: b2ee978af7d49f0fb0e7271c6ac074dfb4d39353
|
||||||
|
@ -175,8 +183,6 @@ GEM
|
||||||
railties (>= 4.1.0, < 5.2)
|
railties (>= 4.1.0, < 5.2)
|
||||||
responders
|
responders
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
devise-async (0.7.0)
|
|
||||||
devise (>= 2.2)
|
|
||||||
devise_invitable (1.7.2)
|
devise_invitable (1.7.2)
|
||||||
actionmailer (>= 4.1.0)
|
actionmailer (>= 4.1.0)
|
||||||
devise (>= 4.0.0)
|
devise (>= 4.0.0)
|
||||||
|
@ -487,7 +493,7 @@ DEPENDENCIES
|
||||||
delayed_job_active_record
|
delayed_job_active_record
|
||||||
delayed_paperclip!
|
delayed_paperclip!
|
||||||
devise (~> 4.3.0)
|
devise (~> 4.3.0)
|
||||||
devise-async
|
devise-async!
|
||||||
devise_invitable
|
devise_invitable
|
||||||
devise_security_extension!
|
devise_security_extension!
|
||||||
factory_girl_rails
|
factory_girl_rails
|
||||||
|
|
41
app/controllers/client_api/users/invitations_controller.rb
Normal file
41
app/controllers/client_api/users/invitations_controller.rb
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
module ClientApi
|
||||||
|
module Users
|
||||||
|
class InvitationsController < Devise::InvitationsController
|
||||||
|
before_action :check_invite_users_permission, only: :invite_users
|
||||||
|
|
||||||
|
def invite_users
|
||||||
|
invite_service =
|
||||||
|
ClientApi::InvitationsService.new(user: current_user,
|
||||||
|
team: @team,
|
||||||
|
role: params['user_role'],
|
||||||
|
emails: params[:emails])
|
||||||
|
invite_results = invite_service.invitation
|
||||||
|
success_response(invite_results)
|
||||||
|
end
|
||||||
|
|
||||||
|
def success_response(invite_results)
|
||||||
|
respond_to do |format|
|
||||||
|
format.json do
|
||||||
|
render template: '/client_api/users/invite_users',
|
||||||
|
status: :ok,
|
||||||
|
locals: { invite_results: invite_results, team: @team }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def check_invite_users_permission
|
||||||
|
@team = Team.find_by_id(params[:team_id])
|
||||||
|
if @team && !is_admin_of_team(@team)
|
||||||
|
respond_to do |format|
|
||||||
|
format.json do
|
||||||
|
render json: t('client_api.invite_users.permission_error'),
|
||||||
|
status: 422
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,28 @@
|
||||||
|
import React from "react";
|
||||||
|
import { func } from "prop-types";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import { DropdownButton, MenuItem } from "react-bootstrap";
|
||||||
|
|
||||||
|
const InviteUsersButton = props => (
|
||||||
|
<DropdownButton
|
||||||
|
bsStyle={"primary"}
|
||||||
|
title={<FormattedMessage id="invite_users.dropdown_button.invite" />}
|
||||||
|
id="invite_users.submit_button"
|
||||||
|
>
|
||||||
|
<MenuItem onClick={() => props.handleClick("guest")}>
|
||||||
|
<FormattedMessage id="invite_users.dropdown_button.guest" />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => props.handleClick("normal_user")}>
|
||||||
|
<FormattedMessage id="invite_users.dropdown_button.normal_user" />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => props.handleClick("admin")}>
|
||||||
|
<FormattedMessage id="invite_users.dropdown_button.admin" />
|
||||||
|
</MenuItem>
|
||||||
|
</DropdownButton>
|
||||||
|
);
|
||||||
|
|
||||||
|
InviteUsersButton.propTypes = {
|
||||||
|
handleClick: func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InviteUsersButton;
|
|
@ -0,0 +1,42 @@
|
||||||
|
import React from "react";
|
||||||
|
import { string, func, arrayOf } from "prop-types";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import { FormGroup, HelpBlock } from "react-bootstrap";
|
||||||
|
import TagsInput from "react-tagsinput";
|
||||||
|
|
||||||
|
import { INVITE_USERS_LIMIT } from "../../../config/constants/numeric";
|
||||||
|
|
||||||
|
const InviteUsersForm = props => (
|
||||||
|
<FormGroup controlId="form-invite-user">
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id="invite_users.input_text"
|
||||||
|
values={{ team: props.teamName }}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<TagsInput
|
||||||
|
value={props.tags}
|
||||||
|
addKeys={[9, 13, 188]}
|
||||||
|
addOnPaste
|
||||||
|
onlyUnique
|
||||||
|
maxTags={INVITE_USERS_LIMIT}
|
||||||
|
inputProps={{
|
||||||
|
placeholder: ""
|
||||||
|
}}
|
||||||
|
onChange={props.handleChange}
|
||||||
|
/>
|
||||||
|
<HelpBlock>
|
||||||
|
<em>
|
||||||
|
<FormattedMessage id="invite_users.input_help" />
|
||||||
|
</em>
|
||||||
|
</HelpBlock>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
|
||||||
|
InviteUsersForm.propTypes = {
|
||||||
|
tags: arrayOf(string.isRequired).isRequired,
|
||||||
|
handleChange: func.isRequired,
|
||||||
|
teamName: string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InviteUsersForm;
|
|
@ -0,0 +1,38 @@
|
||||||
|
import React from "react";
|
||||||
|
import { shape, arrayOf, string } from "prop-types";
|
||||||
|
import { Alert } from "react-bootstrap";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
|
const InviteUsersResults = props => (
|
||||||
|
<div>
|
||||||
|
<h5>
|
||||||
|
<FormattedMessage id="invite_users.results_title" />
|
||||||
|
</h5>
|
||||||
|
<hr />
|
||||||
|
{props.results.invite_results.map(result => (
|
||||||
|
<Alert bsStyle={result.alert} key={result.email}>
|
||||||
|
<strong>{result.email}</strong>
|
||||||
|
-
|
||||||
|
<FormattedMessage
|
||||||
|
id={`invite_users.results_msg.${result.status}`}
|
||||||
|
values={{
|
||||||
|
team: props.results.team_name,
|
||||||
|
role: (
|
||||||
|
<FormattedMessage id={`invite_users.roles.${result.user_role}`} />
|
||||||
|
),
|
||||||
|
nr: result.invite_limit
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Alert>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
InviteUsersResults.propTypes = {
|
||||||
|
results: shape({
|
||||||
|
invite_results: arrayOf.isRequired,
|
||||||
|
team_name: string.isRequired
|
||||||
|
}).isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InviteUsersResults;
|
120
app/javascript/src/components/InviteUsersModal/index.jsx
Normal file
120
app/javascript/src/components/InviteUsersModal/index.jsx
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import { bool, func, shape, number, string } from "prop-types";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import { Modal, ButtonToolbar, Button } from "react-bootstrap";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import axios from "../../config/axios";
|
||||||
|
|
||||||
|
import {
|
||||||
|
INVITE_USERS_PATH,
|
||||||
|
TEAM_DETAILS_PATH
|
||||||
|
} from "../../config/api_endpoints";
|
||||||
|
import InviteUsersForm from "./components/InviteUsersForm";
|
||||||
|
import InviteUsersResults from "./components/InviteUsersResults";
|
||||||
|
import InviteUsersButton from "./components/InviteUsersButton";
|
||||||
|
|
||||||
|
const StyledButtonToolbar = styled(ButtonToolbar)`float: right;`;
|
||||||
|
|
||||||
|
class InviteUsersModal extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
showInviteUsersResults: false,
|
||||||
|
inputTags: [],
|
||||||
|
inviteResults: []
|
||||||
|
};
|
||||||
|
this.handleInputChange = this.handleInputChange.bind(this);
|
||||||
|
this.inviteAs = this.inviteAs.bind(this);
|
||||||
|
this.handleCloseModal = this.handleCloseModal.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCloseModal() {
|
||||||
|
const path = TEAM_DETAILS_PATH.replace(":team_id", this.props.team.id);
|
||||||
|
this.props.onCloseModal();
|
||||||
|
this.setState({
|
||||||
|
showInviteUsersResults: false,
|
||||||
|
inputTags: [],
|
||||||
|
inviteResults: []
|
||||||
|
});
|
||||||
|
// Update team members table
|
||||||
|
axios.get(path).then(response => {
|
||||||
|
const { users } = response.data.team_details;
|
||||||
|
this.props.updateUsersCallback(users);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInputChange(inputTags) {
|
||||||
|
this.setState({ inputTags });
|
||||||
|
}
|
||||||
|
|
||||||
|
inviteAs(role) {
|
||||||
|
axios
|
||||||
|
.put(INVITE_USERS_PATH, {
|
||||||
|
user_role: role,
|
||||||
|
emails: this.state.inputTags,
|
||||||
|
team_id: this.props.team.id
|
||||||
|
})
|
||||||
|
.then(({ data }) => {
|
||||||
|
this.setState({ inviteResults: data, showInviteUsersResults: true});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log("Invite As Error: ", error);
|
||||||
|
if (error.response) {
|
||||||
|
console.log("Error message:", error.response.data);
|
||||||
|
// TO DO: put this error in flash msg
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let modalBody = null;
|
||||||
|
let inviteButton = null;
|
||||||
|
if (this.state.showInviteUsersResults) {
|
||||||
|
modalBody = <InviteUsersResults results={this.state.inviteResults} />;
|
||||||
|
inviteButton = null;
|
||||||
|
} else {
|
||||||
|
modalBody = (
|
||||||
|
<InviteUsersForm
|
||||||
|
tags={this.state.inputTags}
|
||||||
|
handleChange={this.handleInputChange}
|
||||||
|
teamName={this.props.team.name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
inviteButton = <InviteUsersButton handleClick={this.inviteAs} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal show={this.props.showModal} onHide={this.handleCloseModal}>
|
||||||
|
<Modal.Header closeButton>
|
||||||
|
<Modal.Title>
|
||||||
|
<FormattedMessage
|
||||||
|
id="invite_users.modal_title"
|
||||||
|
values={{ team: this.props.team.name }}
|
||||||
|
/>
|
||||||
|
</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>{modalBody}</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<StyledButtonToolbar>
|
||||||
|
<Button onClick={this.handleCloseModal}>
|
||||||
|
<FormattedMessage id="general.cancel" />
|
||||||
|
</Button>
|
||||||
|
{inviteButton}
|
||||||
|
</StyledButtonToolbar>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InviteUsersModal.propTypes = {
|
||||||
|
showModal: bool.isRequired,
|
||||||
|
onCloseModal: func.isRequired,
|
||||||
|
team: shape({
|
||||||
|
id: number.isRequired,
|
||||||
|
name: string.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
updateUsersCallback: func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InviteUsersModal;
|
|
@ -39,6 +39,7 @@ export const CHANGE_USER_RECENT_NOTIFICATION_EMAIL_PATH =
|
||||||
"/client_api/users/change_recent_notification_email";
|
"/client_api/users/change_recent_notification_email";
|
||||||
export const CHANGE_USER_SYSTEM_MESSAGE_NOTIFICATION_EMAIL_PATH =
|
export const CHANGE_USER_SYSTEM_MESSAGE_NOTIFICATION_EMAIL_PATH =
|
||||||
"/client_api/users/change_system_notification_email";
|
"/client_api/users/change_system_notification_email";
|
||||||
|
export const INVITE_USERS_PATH = "/client_api/users/invite_users";
|
||||||
|
|
||||||
// info dropdown_title
|
// info dropdown_title
|
||||||
export const CUSTOMER_SUPPORT_LINK = "http://scinote.net/support";
|
export const CUSTOMER_SUPPORT_LINK = "http://scinote.net/support";
|
||||||
|
|
|
@ -2,3 +2,4 @@ export const ENTER_KEY_CODE = 13;
|
||||||
export const NAME_MIN_LENGTH = 2;
|
export const NAME_MIN_LENGTH = 2;
|
||||||
export const NAME_MAX_LENGTH = 255;
|
export const NAME_MAX_LENGTH = 255;
|
||||||
export const TEXT_MAX_LENGTH = 10000;
|
export const TEXT_MAX_LENGTH = 10000;
|
||||||
|
export const INVITE_USERS_LIMIT = 20;
|
||||||
|
|
|
@ -21,6 +21,43 @@ export default {
|
||||||
notifications_label: "Notifications",
|
notifications_label: "Notifications",
|
||||||
info_label: "Info"
|
info_label: "Info"
|
||||||
},
|
},
|
||||||
|
invite_users: {
|
||||||
|
modal_title: "Invite users to team {team}",
|
||||||
|
input_text: "Invite more people to team {team} and start using sciNote.",
|
||||||
|
input_help:
|
||||||
|
"Input one or multiple emails, confirm each email with ENTER key.",
|
||||||
|
dropdown_button: {
|
||||||
|
invite: "Invite user/s",
|
||||||
|
guest: "as Guest/s",
|
||||||
|
normal_user: "as Normal user/s",
|
||||||
|
admin: "as Administrator/s"
|
||||||
|
},
|
||||||
|
results_title: "Invitation results:",
|
||||||
|
roles: {
|
||||||
|
guest: "Guest",
|
||||||
|
normal_user: "Normal user",
|
||||||
|
admin: "Administrator"
|
||||||
|
},
|
||||||
|
results_msg: {
|
||||||
|
user_exists: "User is already a member of sciNote.",
|
||||||
|
user_exists_unconfirmed:
|
||||||
|
"User is already a member of sciNote but is not confirmed yet.",
|
||||||
|
user_exists_and_in_team_unconfirmed:
|
||||||
|
"User is already a member of sciNote and team {team} as {role} but is not confirmed yet.",
|
||||||
|
user_exists_invited_to_team_unconfirmed:
|
||||||
|
"User is already a member of sciNote but is not confirmed yet - successfully invited to team {team} as {role}.",
|
||||||
|
user_exists_and_in_team:
|
||||||
|
"User is already a member of sciNote and team {team} as {role}.",
|
||||||
|
user_exists_invited_to_team:
|
||||||
|
"User was already a member of sciNote - successfully invited to team {team} as {role}.",
|
||||||
|
user_created: "User succesfully invited to sciNote.",
|
||||||
|
user_created_invited_to_team:
|
||||||
|
"User successfully invited to sciNote and team {team} as {role}.",
|
||||||
|
user_invalid: "Invalid email.",
|
||||||
|
too_many_emails:
|
||||||
|
"Only invited first {nr} emails. To invite more users, fill in another invitation form."
|
||||||
|
}
|
||||||
|
},
|
||||||
settings_page: {
|
settings_page: {
|
||||||
all_teams: "All teams",
|
all_teams: "All teams",
|
||||||
in_team: "You are member of {num} team",
|
in_team: "You are member of {num} team",
|
||||||
|
@ -64,20 +101,28 @@ export default {
|
||||||
yes: "Yes",
|
yes: "Yes",
|
||||||
leave_team_modal: {
|
leave_team_modal: {
|
||||||
title: "Leave team {teamName}",
|
title: "Leave team {teamName}",
|
||||||
subtitle: "Are you sure you wish to leave team My projects? This action is irreversible.",
|
subtitle:
|
||||||
|
"Are you sure you wish to leave team My projects? This action is irreversible.",
|
||||||
warnings: "Leaving team has following consequences:",
|
warnings: "Leaving team has following consequences:",
|
||||||
warning_message_one: "you will lose access to all content belonging to the team (including projects, tasks, protocols and activities);",
|
warning_message_one:
|
||||||
warning_message_two: "all projects in the team where you were the sole <b>Owner</b> will receive a new owner from the team administrators;",
|
"you will lose access to all content belonging to the team (including projects, tasks, protocols and activities);",
|
||||||
warning_message_three: "all repository protocols in the team belonging to you will be reassigned onto a new owner from team administrators.",
|
warning_message_two:
|
||||||
|
"all projects in the team where you were the sole <b>Owner</b> will receive a new owner from the team administrators;",
|
||||||
|
warning_message_three:
|
||||||
|
"all repository protocols in the team belonging to you will be reassigned onto a new owner from team administrators.",
|
||||||
leave_team: "Leave"
|
leave_team: "Leave"
|
||||||
},
|
},
|
||||||
remove_user_modal: {
|
remove_user_modal: {
|
||||||
title: "Remove user {user} from team {team}",
|
title: "Remove user {user} from team {team}",
|
||||||
subtitle: "Are you sure you wish to remove user {user} from team {team}?",
|
subtitle:
|
||||||
|
"Are you sure you wish to remove user {user} from team {team}?",
|
||||||
warnings: "Removing user from team has following consequences:",
|
warnings: "Removing user from team has following consequences:",
|
||||||
warning_message_one: "user will lose access to all content belonging to the team (including projects, tasks, protocols and activities);",
|
warning_message_one:
|
||||||
warning_message_two: "all projects in the team where user was the sole <b>Owner</b> will be reassigned onto you as a new owner;",
|
"user will lose access to all content belonging to the team (including projects, tasks, protocols and activities);",
|
||||||
warning_message_three: "all repository protocols in the team belonging to user will be reassigned onto you.",
|
warning_message_two:
|
||||||
|
"all projects in the team where user was the sole <b>Owner</b> will be reassigned onto you as a new owner;",
|
||||||
|
warning_message_three:
|
||||||
|
"all repository protocols in the team belonging to user will be reassigned onto you.",
|
||||||
remove_user: "Remove user"
|
remove_user: "Remove user"
|
||||||
},
|
},
|
||||||
update_team_description_modal: {
|
update_team_description_modal: {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import axios from "../../../../../config/axios";
|
import axios from "../../../../../config/axios";
|
||||||
|
|
||||||
|
import InviteUsersModal from "../../../../../components/InviteUsersModal";
|
||||||
import RemoveUserModal from "./RemoveUserModal";
|
import RemoveUserModal from "./RemoveUserModal";
|
||||||
import DataTable from "../../../../../components/data_table";
|
import DataTable from "../../../../../components/data_table";
|
||||||
import { UPDATE_USER_TEAM_ROLE_PATH } from "../../../../../config/api_endpoints";
|
import { UPDATE_USER_TEAM_ROLE_PATH } from "../../../../../config/api_endpoints";
|
||||||
|
@ -23,8 +24,18 @@ const initalUserToRemove = {
|
||||||
class TeamsMembers extends Component {
|
class TeamsMembers extends Component {
|
||||||
constructor(params) {
|
constructor(params) {
|
||||||
super(params);
|
super(params);
|
||||||
this.state = { showModal: false, userToRemove: initalUserToRemove };
|
this.state = {
|
||||||
|
showModal: false,
|
||||||
|
showInviteUsersModal: false,
|
||||||
|
userToRemove: initalUserToRemove
|
||||||
|
};
|
||||||
this.memberAction = this.memberAction.bind(this);
|
this.memberAction = this.memberAction.bind(this);
|
||||||
|
this.showInviteUsersModalCallback = this.showInviteUsersModalCallback.bind(
|
||||||
|
this
|
||||||
|
);
|
||||||
|
this.closeInviteUsersModalCallback = this.closeInviteUsersModalCallback.bind(
|
||||||
|
this
|
||||||
|
);
|
||||||
this.hideModal = this.hideModal.bind(this);
|
this.hideModal = this.hideModal.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +56,14 @@ class TeamsMembers extends Component {
|
||||||
.catch(error => console.log(error));
|
.catch(error => console.log(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showInviteUsersModalCallback() {
|
||||||
|
this.setState({ showInviteUsersModal: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
closeInviteUsersModalCallback() {
|
||||||
|
this.setState({ showInviteUsersModal: false });
|
||||||
|
}
|
||||||
|
|
||||||
hideModal() {
|
hideModal() {
|
||||||
this.setState({ showModal: false, userToRemove: initalUserToRemove });
|
this.setState({ showModal: false, userToRemove: initalUserToRemove });
|
||||||
}
|
}
|
||||||
|
@ -169,7 +188,7 @@ class TeamsMembers extends Component {
|
||||||
<FormattedMessage id="settings_page.single_team.members_panel_title" />
|
<FormattedMessage id="settings_page.single_team.members_panel_title" />
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button>
|
<Button bsStyle="primary" onClick={this.showInviteUsersModalCallback}>
|
||||||
<Glyphicon glyph="plus" />
|
<Glyphicon glyph="plus" />
|
||||||
<FormattedMessage id="settings_page.single_team.add_members" />
|
<FormattedMessage id="settings_page.single_team.add_members" />
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -181,6 +200,12 @@ class TeamsMembers extends Component {
|
||||||
updateUsersCallback={this.props.updateUsersCallback}
|
updateUsersCallback={this.props.updateUsersCallback}
|
||||||
userToRemove={this.state.userToRemove}
|
userToRemove={this.state.userToRemove}
|
||||||
/>
|
/>
|
||||||
|
<InviteUsersModal
|
||||||
|
showModal={this.state.showInviteUsersModal}
|
||||||
|
onCloseModal={this.closeInviteUsersModalCallback}
|
||||||
|
team={this.props.team}
|
||||||
|
updateUsersCallback={this.props.updateUsersCallback}
|
||||||
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
@import 'constants';
|
@import 'constants';
|
||||||
@import 'react-bootstrap-timezone-picker/dist/react-bootstrap-timezone-picker.min.css';
|
@import 'react-bootstrap-timezone-picker/dist/react-bootstrap-timezone-picker.min.css';
|
||||||
@import '~react-bootstrap-table/dist/react-bootstrap-table.min';
|
@import '~react-bootstrap-table/dist/react-bootstrap-table.min';
|
||||||
|
@import 'react-tagsinput/react-tagsinput.css';
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: $color-concrete;
|
background-color: $color-concrete;
|
||||||
|
|
160
app/services/client_api/invitations_service.rb
Normal file
160
app/services/client_api/invitations_service.rb
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
module ClientApi
|
||||||
|
class InvitationsService
|
||||||
|
include InputSanitizeHelper
|
||||||
|
include UsersGenerator
|
||||||
|
|
||||||
|
def initialize(args)
|
||||||
|
@user = args[:user]
|
||||||
|
@emails = args[:emails].map(&:downcase)
|
||||||
|
@team = args[:team]
|
||||||
|
@role = args[:role]
|
||||||
|
|
||||||
|
raise ClientApi::CustomInvitationsError unless @team && @role &&
|
||||||
|
@emails && @emails.present?
|
||||||
|
if @role && !UserTeam.roles.keys.include?(@role)
|
||||||
|
raise ClientApi::CustomInvitationsError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def invitation
|
||||||
|
invite_results = []
|
||||||
|
|
||||||
|
@emails.each_with_index do |email, index|
|
||||||
|
result = {}
|
||||||
|
# Check invite users limit
|
||||||
|
if index >= Constants::INVITE_USERS_LIMIT
|
||||||
|
result[:status] = :too_many_emails
|
||||||
|
result[:alert] = :danger
|
||||||
|
result[:invite_user_limit] = Constants::INVITE_USERS_LIMIT
|
||||||
|
invite_results << result
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
result[:email] = email
|
||||||
|
|
||||||
|
# Check if user already exists
|
||||||
|
user = User.find_by_email(email) if User.exists?(email: email)
|
||||||
|
# Handle user invitation
|
||||||
|
result = handle_user(result, email, user)
|
||||||
|
invite_results << result
|
||||||
|
end
|
||||||
|
invite_results
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def handle_user(result, email, user)
|
||||||
|
return handle_new_user(result, email, user) if user.blank?
|
||||||
|
handle_existing_user(result, user)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_new_user(result, email, user)
|
||||||
|
password = generate_user_password
|
||||||
|
# Validate the user data
|
||||||
|
error = (Constants::BASIC_EMAIL_REGEX !~ email)
|
||||||
|
error = validate_user(email, email, password).count > 0 unless error
|
||||||
|
|
||||||
|
if !error
|
||||||
|
# Invite new user
|
||||||
|
user = invite_new_user(email)
|
||||||
|
|
||||||
|
result[:status] = :user_created
|
||||||
|
result[:alert] = :success
|
||||||
|
result[:user] = user
|
||||||
|
|
||||||
|
# Invitation to team
|
||||||
|
if @team.present?
|
||||||
|
user_team = create_user_team_relation_and_notification(user)
|
||||||
|
result[:status] = :user_created_invited_to_team
|
||||||
|
result[:user_team] = user_team
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# Return invalid status
|
||||||
|
result[:status] = :user_invalid
|
||||||
|
result[:alert] = :danger
|
||||||
|
end
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_existing_user(result, user)
|
||||||
|
result[:status] =
|
||||||
|
:"#{:user_exists}#{:_unconfirmed unless user.confirmed?}"
|
||||||
|
result[:alert] = :info
|
||||||
|
result[:user] = user
|
||||||
|
|
||||||
|
# Invitation to team
|
||||||
|
if @team.present?
|
||||||
|
if UserTeam.exists?(user: user, team: @team)
|
||||||
|
user_team = UserTeam.where(user: user, team: @team).first
|
||||||
|
end
|
||||||
|
|
||||||
|
if user_team.present?
|
||||||
|
result[:status] =
|
||||||
|
:"#{:user_exists_and_in_team}#{:_unconfirmed unless user
|
||||||
|
.confirmed?}"
|
||||||
|
else
|
||||||
|
user_team = create_user_team_relation_and_notification(user)
|
||||||
|
result[:status] =
|
||||||
|
:"#{:user_exists_invited_to_team}#{:_unconfirmed unless user
|
||||||
|
.confirmed?}"
|
||||||
|
end
|
||||||
|
result[:user_team] = user_team
|
||||||
|
end
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
def invite_new_user(email)
|
||||||
|
user = User.invite!(
|
||||||
|
full_name: email,
|
||||||
|
email: email,
|
||||||
|
initials: email.upcase[0..1],
|
||||||
|
skip_invitation: true
|
||||||
|
)
|
||||||
|
user.update(invited_by: @user)
|
||||||
|
|
||||||
|
# Sending email invitation is done in background job to prevent
|
||||||
|
# issues with email delivery. Also invite method must be call
|
||||||
|
# with :skip_invitation attribute set to true - see above.
|
||||||
|
user.delay.deliver_invitation
|
||||||
|
user
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_user_team_relation_and_notification(user)
|
||||||
|
user_team = UserTeam.new(
|
||||||
|
user: user,
|
||||||
|
team: @team,
|
||||||
|
role: @role
|
||||||
|
)
|
||||||
|
user_team.save
|
||||||
|
|
||||||
|
generate_notification(
|
||||||
|
@user,
|
||||||
|
user,
|
||||||
|
user_team.role_str,
|
||||||
|
user_team.team
|
||||||
|
)
|
||||||
|
user_team
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_notification(user, target_user, role, team)
|
||||||
|
title = I18n.t('notifications.assign_user_to_team',
|
||||||
|
assigned_user: target_user.name,
|
||||||
|
role: role,
|
||||||
|
team: team.name,
|
||||||
|
assigned_by_user: user.name)
|
||||||
|
|
||||||
|
message = "#{I18n.t('search.index.team')} #{team.name}"
|
||||||
|
notification = Notification.create(
|
||||||
|
type_of: :assignment,
|
||||||
|
title: sanitize_input(title),
|
||||||
|
message: sanitize_input(message)
|
||||||
|
)
|
||||||
|
|
||||||
|
if target_user.settings[:notifications][:assignments]
|
||||||
|
UserNotification.create(notification: notification, user: target_user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
CustomInvitationsError = Class.new(StandardError)
|
||||||
|
end
|
12
app/views/client_api/users/invite_users.json.jbuilder
Normal file
12
app/views/client_api/users/invite_users.json.jbuilder
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
json.invite_results invite_results do |invite_result|
|
||||||
|
json.status invite_result[:status]
|
||||||
|
json.alert invite_result[:alert]
|
||||||
|
json.email invite_result[:email]
|
||||||
|
if invite_result[:user_team].present?
|
||||||
|
json.user_role invite_result[:user_team].role
|
||||||
|
end
|
||||||
|
if invite_result[:invite_user_limit].present?
|
||||||
|
json.invite_limit invite_result[:invite_user_limit]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
json.team_name team.name if team.present?
|
|
@ -83,7 +83,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="col-xs-24 col-sm-12">
|
<div class="col-xs-24 col-sm-12">
|
||||||
<a href="#" class="btn btn-primary pull-right row" data-trigger="invite-users" data-modal-id="team-invite-users-modal">
|
<a href="#" class="btn btn-primary pull-right row" data-trigger="invite-users"
|
||||||
|
data-turbolinks="false" data-modal-id="team-invite-users-modal">
|
||||||
<span class="glyphicon glyphicon-plus"></span>
|
<span class="glyphicon glyphicon-plus"></span>
|
||||||
<%= t("users.settings.teams.edit.add_user") %>
|
<%= t("users.settings.teams.edit.add_user") %>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1825,3 +1825,5 @@ en:
|
||||||
user_teams:
|
user_teams:
|
||||||
leave_team_error: "An error occured."
|
leave_team_error: "An error occured."
|
||||||
leave_flash: "Successfuly left team %{team}."
|
leave_flash: "Successfuly left team %{team}."
|
||||||
|
invite_users:
|
||||||
|
permission_error: "You don't have permission to invite additional users to team. Contact its administrator/s."
|
||||||
|
|
|
@ -54,6 +54,9 @@ Rails.application.routes.draw do
|
||||||
to: 'users#change_recent_notification_email'
|
to: 'users#change_recent_notification_email'
|
||||||
post '/change_system_notification_email',
|
post '/change_system_notification_email',
|
||||||
to: 'users#change_system_notification_email'
|
to: 'users#change_system_notification_email'
|
||||||
|
devise_scope :user do
|
||||||
|
put '/invite_users', to: 'invitations#invite_users'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -68,6 +68,7 @@
|
||||||
"react-bootstrap-table": "^4.0.0",
|
"react-bootstrap-table": "^4.0.0",
|
||||||
"react-bootstrap-timezone-picker": "^1.0.11",
|
"react-bootstrap-timezone-picker": "^1.0.11",
|
||||||
"react-data-grid": "^2.0.2",
|
"react-data-grid": "^2.0.2",
|
||||||
|
"react-tagsinput": "^3.17.0",
|
||||||
"react-dom": "^15.6.1",
|
"react-dom": "^15.6.1",
|
||||||
"react-intl": "^2.3.0",
|
"react-intl": "^2.3.0",
|
||||||
"react-intl-redux": "^0.6.0",
|
"react-intl-redux": "^0.6.0",
|
||||||
|
|
|
@ -4815,6 +4815,10 @@ react-s-alert@^1.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
babel-runtime "^6.23.0"
|
babel-runtime "^6.23.0"
|
||||||
|
|
||||||
|
react-tagsinput@^3.17.0:
|
||||||
|
version "3.18.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-tagsinput/-/react-tagsinput-3.18.0.tgz#40e036fc0f4c3d6b4689858189ab02926717a818"
|
||||||
|
|
||||||
react-timezone@^0.2.0:
|
react-timezone@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-timezone/-/react-timezone-0.2.0.tgz#031b6d700b15eb6dd977e326b05fe9080b8a2b6d"
|
resolved "https://registry.yarnpkg.com/react-timezone/-/react-timezone-0.2.0.tgz#031b6d700b15eb6dd977e326b05fe9080b8a2b6d"
|
||||||
|
|
Loading…
Reference in a new issue