From 48c89226043b8b3f1b9e9870416152c51cbc36a5 Mon Sep 17 00:00:00 2001 From: mlorb Date: Wed, 20 Sep 2017 11:31:07 +0200 Subject: [PATCH 1/5] adds invite users modal --- Gemfile | 5 +- Gemfile.lock | 12 +- .../users/invitations_controller.rb | 34 +++++ .../components/InviteUsersButton.jsx | 27 ++++ .../components/InviteUsersForm.jsx | 38 +++++ .../components/InviteUsersResults.jsx | 30 ++++ .../src/components/InviteUsersModal/index.jsx | 107 +++++++++++++ app/javascript/src/config/api_endpoints.js | 1 + .../src/config/constants/numeric.js | 1 + app/javascript/src/config/locales/messages.js | 95 ++++++++---- .../scenes/team/components/TeamsMembers.jsx | 25 ++- app/javascript/src/styles/main.scss | 23 +-- .../client_api/invitations_service.rb | 143 ++++++++++++++++++ .../users/invite_users.json.jbuilder | 8 + app/views/users/settings/teams/show.html.erb | 2 +- config/routes.rb | 3 + package.json | 1 + yarn.lock | 4 + 18 files changed, 510 insertions(+), 49 deletions(-) create mode 100644 app/controllers/client_api/users/invitations_controller.rb create mode 100644 app/javascript/src/components/InviteUsersModal/components/InviteUsersButton.jsx create mode 100644 app/javascript/src/components/InviteUsersModal/components/InviteUsersForm.jsx create mode 100644 app/javascript/src/components/InviteUsersModal/components/InviteUsersResults.jsx create mode 100644 app/javascript/src/components/InviteUsersModal/index.jsx create mode 100644 app/services/client_api/invitations_service.rb create mode 100644 app/views/client_api/users/invite_users.json.jbuilder diff --git a/Gemfile b/Gemfile index d487bbb7b..14a76b849 100644 --- a/Gemfile +++ b/Gemfile @@ -70,7 +70,10 @@ gem 'paperclip', '~> 5.1' # File attachment, image attachment library gem 'aws-sdk', '~> 2' 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 'tinymce-rails', '~> 4.6.4' # Rich text editor diff --git a/Gemfile.lock b/Gemfile.lock index a5f40ce5a..ec7e69440 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -21,6 +21,14 @@ GIT activejob (>= 4.2) 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 remote: https://github.com/phatworx/devise_security_extension.git revision: b2ee978af7d49f0fb0e7271c6ac074dfb4d39353 @@ -175,8 +183,6 @@ GEM railties (>= 4.1.0, < 5.2) responders warden (~> 1.2.3) - devise-async (0.7.0) - devise (>= 2.2) devise_invitable (1.7.2) actionmailer (>= 4.1.0) devise (>= 4.0.0) @@ -487,7 +493,7 @@ DEPENDENCIES delayed_job_active_record delayed_paperclip! devise (~> 4.3.0) - devise-async + devise-async! devise_invitable devise_security_extension! factory_girl_rails diff --git a/app/controllers/client_api/users/invitations_controller.rb b/app/controllers/client_api/users/invitations_controller.rb new file mode 100644 index 000000000..27a5fa6a3 --- /dev/null +++ b/app/controllers/client_api/users/invitations_controller.rb @@ -0,0 +1,34 @@ +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]) + render_403 if @team && !is_admin_of_team(@team) + end + end +end +end \ No newline at end of file diff --git a/app/javascript/src/components/InviteUsersModal/components/InviteUsersButton.jsx b/app/javascript/src/components/InviteUsersModal/components/InviteUsersButton.jsx new file mode 100644 index 000000000..0ddd97ccf --- /dev/null +++ b/app/javascript/src/components/InviteUsersModal/components/InviteUsersButton.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { DropdownButton, MenuItem } from 'react-bootstrap'; + +const InviteUsersButton = props => + } + id="invite_users.submit_button" + > + props.handleClick('guest')}> + + + props.handleClick('normal_user')}> + + + props.handleClick('admin')}> + + + ; + +InviteUsersButton.propTypes = { + handleClick: PropTypes.func.isRequired +}; + +export default InviteUsersButton; diff --git a/app/javascript/src/components/InviteUsersModal/components/InviteUsersForm.jsx b/app/javascript/src/components/InviteUsersModal/components/InviteUsersForm.jsx new file mode 100644 index 000000000..6e8576360 --- /dev/null +++ b/app/javascript/src/components/InviteUsersModal/components/InviteUsersForm.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import PropTypes 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 => + +

+ +

+ + + + + + +
; + +InviteUsersForm.propTypes = { + tags: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, + handleChange: PropTypes.func.isRequired, + teamName: PropTypes.string.isRequired +}; + +export default InviteUsersForm; diff --git a/app/javascript/src/components/InviteUsersModal/components/InviteUsersResults.jsx b/app/javascript/src/components/InviteUsersModal/components/InviteUsersResults.jsx new file mode 100644 index 000000000..87d2ff153 --- /dev/null +++ b/app/javascript/src/components/InviteUsersModal/components/InviteUsersResults.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Alert } from 'react-bootstrap'; +import { FormattedMessage } from 'react-intl'; + +const InviteUsersResults = props => +
+
+ +
+
+ {props.results.invite_results.map(result => + + {result.email} +  -  + , + nr: result.invite_limit + }} /> + + )} +
; + +InviteUsersResults.propTypes = { + results: PropTypes.object.isRequired +}; + +export default InviteUsersResults; + diff --git a/app/javascript/src/components/InviteUsersModal/index.jsx b/app/javascript/src/components/InviteUsersModal/index.jsx new file mode 100644 index 000000000..b4ab3b8be --- /dev/null +++ b/app/javascript/src/components/InviteUsersModal/index.jsx @@ -0,0 +1,107 @@ +import React, { Component } from 'react'; +import PropTypes 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() { + super(); + 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 }); + this.setState({ showInviteUsersResults: true }); + }) + .catch(error => {}); + } + + render() { + let modalBody = null; + let inviteButton = null; + if (this.state.showInviteUsersResults) { + modalBody = ; + inviteButton = null; + } else { + modalBody = ; + inviteButton = ; + } + + return ( + + + + + + + + {modalBody} + + + + + {inviteButton} + + + + ); + } +} + +InviteUsersModal.propTypes = { + showModal: PropTypes.bool.isRequired, + onCloseModal: PropTypes.func.isRequired, + team: PropTypes.shape({ + id: PropTypes.number.isRequired, + name: PropTypes.string.isRequired + }).isRequired, + updateUsersCallback: PropTypes.func.isRequired +}; + +export default InviteUsersModal; diff --git a/app/javascript/src/config/api_endpoints.js b/app/javascript/src/config/api_endpoints.js index ae5c3693e..9e1fa0172 100644 --- a/app/javascript/src/config/api_endpoints.js +++ b/app/javascript/src/config/api_endpoints.js @@ -39,6 +39,7 @@ export const CHANGE_USER_RECENT_NOTIFICATION_EMAIL_PATH = "/client_api/users/change_recent_notification_email"; export const CHANGE_USER_SYSTEM_MESSAGE_NOTIFICATION_EMAIL_PATH = "/client_api/users/change_system_notification_email"; +export const INVITE_USERS_PATH = '/client_api/users/invite_users'; // info dropdown_title export const CUSTOMER_SUPPORT_LINK = "http://scinote.net/support"; diff --git a/app/javascript/src/config/constants/numeric.js b/app/javascript/src/config/constants/numeric.js index 601d72a10..1e917b60a 100644 --- a/app/javascript/src/config/constants/numeric.js +++ b/app/javascript/src/config/constants/numeric.js @@ -2,3 +2,4 @@ export const ENTER_KEY_CODE = 13; export const NAME_MIN_LENGTH = 2; export const NAME_MAX_LENGTH = 255; export const TEXT_MAX_LENGTH = 10000; +export const INVITE_USERS_LIMIT = 20; diff --git a/app/javascript/src/config/locales/messages.js b/app/javascript/src/config/locales/messages.js index 798c7bebd..037c2fb8e 100644 --- a/app/javascript/src/config/locales/messages.js +++ b/app/javascript/src/config/locales/messages.js @@ -1,25 +1,25 @@ export default { - "en-US": { + 'en-US': { general: { - close: "Close", - cancel: "Cancel", - update: "Update", - edit: "Edit", - loading: "Loading ..." + close: 'Close', + cancel: 'Cancel', + update: 'Update', + edit: 'Edit', + loading: 'Loading ...' }, error_messages: { text_too_short: "is too short (minimum is {min_length} characters)", text_too_long: "is too long (maximum is {max_length} characters)" }, navbar: { - page_title: "sciNote", - home_label: "Home", - protocols_label: "Protocols", - repositories_label: "Repositories", - activities_label: "Activities", - search_label: "Search", - notifications_label: "Notifications", - info_label: "Info" + page_title: 'sciNote', + home_label: 'Home', + protocols_label: 'Protocols', + repositories_label: 'Repositories', + activities_label: 'Activities', + search_label: 'Search', + notifications_label: 'Notifications', + info_label: 'Info' }, settings_page: { all_teams: "All teams", @@ -54,8 +54,8 @@ export default { "Assignment notifications appear whenever you get assigned to a team, project, task.", recent_changes: "Recent changes", recent_changes_msg: - "Recent changes notifications appear whenever there is a change on a task you are assigned to.", - system_message: "System message", + 'Recent changes notifications appear whenever there is a change on a task you are assigned to.', + system_message: 'System message', system_message_msg: "System message notifications are specifically sent by site maintainers to notify all users about a system update.", show_in_scinote: "Show in sciNote", @@ -114,29 +114,62 @@ export default { } }, activities: { - modal_title: "Activities", - no_data: "No Data", - more_activities: "More Activities" + modal_title: 'Activities', + no_data: 'No Data', + more_activities: 'More Activities' }, global_team_switch: { - new_team: "New team" + new_team: 'New team' }, notifications: { - dropdown_title: "Notifications", - dropdown_settings_link: "Settings", - dropdown_show_all: "Show all notifications" + dropdown_title: 'Notifications', + dropdown_settings_link: 'Settings', + dropdown_show_all: 'Show all notifications' }, info_dropdown: { - customer_support: "Customer support", - tutorials: "Tutorials", - release_notes: "Release notes", - premium: "Premium", - contact_us: "Contact us" + customer_support: 'Customer support', + tutorials: 'Tutorials', + release_notes: 'Release notes', + premium: 'Premium', + contact_us: 'Contact us' }, user_account_dropdown: { - greeting: "Hi, {name}", - settings: "Settings", - log_out: "Log out" + greeting: 'Hi, {name}', + settings: 'Settings', + log_out: 'Log out' + }, + 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.' + } } } }; diff --git a/app/javascript/src/scenes/SettingsPage/scenes/team/components/TeamsMembers.jsx b/app/javascript/src/scenes/SettingsPage/scenes/team/components/TeamsMembers.jsx index 35a027e81..bed7cf04b 100644 --- a/app/javascript/src/scenes/SettingsPage/scenes/team/components/TeamsMembers.jsx +++ b/app/javascript/src/scenes/SettingsPage/scenes/team/components/TeamsMembers.jsx @@ -10,6 +10,7 @@ import { import { FormattedMessage } from "react-intl"; import axios from "../../../../../config/axios"; +import InviteUsersModal from '../../../../../components/InviteUsersModal'; import RemoveUserModal from "./RemoveUserModal"; import DataTable from "../../../../../components/data_table"; import { UPDATE_USER_TEAM_ROLE_PATH } from "../../../../../config/api_endpoints"; @@ -23,8 +24,14 @@ const initalUserToRemove = { class TeamsMembers extends Component { constructor(params) { super(params); - this.state = { showModal: false, userToRemove: initalUserToRemove }; + this.state = { + showModal: false, + showInviteUsersModal: false, + userToRemove: initalUserToRemove + }; this.memberAction = this.memberAction.bind(this); + this.showInviteUsersModalCallback = this.showInviteUsersModalCallback.bind(this); + this.closeInviteUsersModalCallback = this.closeInviteUsersModalCallback.bind(this); this.hideModal = this.hideModal.bind(this); } @@ -45,6 +52,14 @@ class TeamsMembers extends Component { .catch(error => console.log(error)); } + showInviteUsersModalCallback() { + this.setState({ showInviteUsersModal: true }); + } + + closeInviteUsersModalCallback() { + this.setState({ showInviteUsersModal: false }); + } + hideModal() { this.setState({ showModal: false, userToRemove: initalUserToRemove }); } @@ -169,7 +184,7 @@ class TeamsMembers extends Component { } > - @@ -181,6 +196,12 @@ class TeamsMembers extends Component { updateUsersCallback={this.props.updateUsersCallback} userToRemove={this.state.userToRemove} /> + ); } diff --git a/app/javascript/src/styles/main.scss b/app/javascript/src/styles/main.scss index d24494f08..5c5a8f1fb 100644 --- a/app/javascript/src/styles/main.scss +++ b/app/javascript/src/styles/main.scss @@ -1,25 +1,26 @@ @import 'constants'; @import 'react-bootstrap-timezone-picker/dist/react-bootstrap-timezone-picker.min.css'; @import '~react-bootstrap-table/dist/react-bootstrap-table.min'; +@import 'react-tagsinput/react-tagsinput.css'; body { - background-color: $color-concrete; - color: $color-emperor; - font-family: "Open Sans", Arial, Helvetica, sans-serif; - font-size: 13px; + background-color: $color-concrete; + color: $color-emperor; + font-family: "Open Sans", Arial, Helvetica, sans-serif; + font-size: 13px; } .label-primary { - background-color: $color-theme-primary; + background-color: $color-theme-primary; } .btn-primary { - background-color: $color-theme-secondary; - border-color: $primary-hover-color; - margin-right: 7px; - &:hover { - background-color: $primary-hover-color; - } + background-color: $color-theme-secondary; + border-color: $primary-hover-color; + margin-right: 7px; + &:hover { + background-color: $primary-hover-color; + } } // // fixes issue with dropdown in datatable diff --git a/app/services/client_api/invitations_service.rb b/app/services/client_api/invitations_service.rb new file mode 100644 index 000000000..9971eb275 --- /dev/null +++ b/app/services/client_api/invitations_service.rb @@ -0,0 +1,143 @@ +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 @emails && @team && @role + @emails && @emails.empty? { raise ClientApi::CustomInvitationsError } + @role && !UserTeam.roles.keys.include?(@role) { raise ClientApi::CustomInvitationsError } + 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) + + # User does not exist + if user.blank? + 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 + 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 + # User exists + else + result[:status] = :"#{:user_exists}#{:_unconfirmed if !user.confirmed?}" + result[:alert] = :info + result[:user] = user + + # Invitation to team + if @team.present? + user_team = + UserTeam.where(user: user, team: @team).first if UserTeam.exists?(user: user, team: @team) + + if user_team.present? + result[:status] = :"#{:user_exists_and_in_team}#{:_unconfirmed if !user.confirmed?}" + + else + user_team = create_user_team_relation_and_notification(user) + result[:status] = :"#{:user_exists_invited_to_team}#{:_unconfirmed if !user.confirmed?}" + end + result[:user_team] = user_team + end + end + invite_results << result + end + invite_results + end + + private + + 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.assignments_notification + UserNotification.create(notification: notification, user: target_user) + end + end + end + + CustomInvitationsError = Class.new(StandardError) +end \ No newline at end of file diff --git a/app/views/client_api/users/invite_users.json.jbuilder b/app/views/client_api/users/invite_users.json.jbuilder new file mode 100644 index 000000000..95da57caf --- /dev/null +++ b/app/views/client_api/users/invite_users.json.jbuilder @@ -0,0 +1,8 @@ +json.invite_results invite_results do |invite_result| + json.status invite_result[:status] + json.alert invite_result[:alert] + json.email invite_result[:email] + json.user_role invite_result[:user_team].role if invite_result[:user_team].present? + json.invite_limit invite_result[:invite_user_limit] if invite_result[:invite_user_limit].present? +end +json.team_name team.name if team.present? \ No newline at end of file diff --git a/app/views/users/settings/teams/show.html.erb b/app/views/users/settings/teams/show.html.erb index 9dc61983b..13b420746 100644 --- a/app/views/users/settings/teams/show.html.erb +++ b/app/views/users/settings/teams/show.html.erb @@ -83,7 +83,7 @@
- + <%= t("users.settings.teams.edit.add_user") %> diff --git a/config/routes.rb b/config/routes.rb index 398e374e7..81f0d7304 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -54,6 +54,9 @@ Rails.application.routes.draw do to: 'users#change_recent_notification_email' post '/change_system_notification_email', to: 'users#change_system_notification_email' + devise_scope :user do + put '/invite_users', to: 'invitations#invite_users' + end end end diff --git a/package.json b/package.json index 0371cd7c3..8f15e51df 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "react-bootstrap-table": "^4.0.0", "react-bootstrap-timezone-picker": "^1.0.11", "react-data-grid": "^2.0.2", + "react-tagsinput": "^3.17.0", "react-dom": "^15.6.1", "react-intl": "^2.3.0", "react-intl-redux": "^0.6.0", diff --git a/yarn.lock b/yarn.lock index c0ce92ca1..fe7343bfb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4815,6 +4815,10 @@ react-s-alert@^1.3.0: dependencies: 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: version "0.2.0" resolved "https://registry.yarnpkg.com/react-timezone/-/react-timezone-0.2.0.tgz#031b6d700b15eb6dd977e326b05fe9080b8a2b6d" From 55772a80abf94feac07240e4ef0d2bcb99505753 Mon Sep 17 00:00:00 2001 From: mlorb Date: Thu, 21 Sep 2017 10:36:09 +0200 Subject: [PATCH 2/5] fix hound and refactor --- .../users/invitations_controller.rb | 48 ++--- .../components/InviteUsersButton.jsx | 43 ++-- .../components/InviteUsersForm.jsx | 66 +++--- .../components/InviteUsersResults.jsx | 56 ++--- .../src/components/InviteUsersModal/index.jsx | 192 +++++++++--------- app/javascript/src/config/api_endpoints.js | 2 +- app/javascript/src/config/locales/messages.js | 132 ++++++------ .../scenes/team/components/TeamsMembers.jsx | 12 +- app/javascript/src/styles/main.scss | 30 +-- .../client_api/invitations_service.rb | 116 ++++++----- .../users/invite_users.json.jbuilder | 10 +- app/views/users/settings/teams/show.html.erb | 3 +- 12 files changed, 385 insertions(+), 325 deletions(-) diff --git a/app/controllers/client_api/users/invitations_controller.rb b/app/controllers/client_api/users/invitations_controller.rb index 27a5fa6a3..87789769e 100644 --- a/app/controllers/client_api/users/invitations_controller.rb +++ b/app/controllers/client_api/users/invitations_controller.rb @@ -1,34 +1,34 @@ module ClientApi -module Users - class InvitationsController < Devise::InvitationsController + module Users + class InvitationsController < Devise::InvitationsController + before_action :check_invite_users_permission, only: :invite_users - 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 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} + 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 - end - private + private - def check_invite_users_permission - @team = Team.find_by_id(params[:team_id]) - render_403 if @team && !is_admin_of_team(@team) + def check_invite_users_permission + @team = Team.find_by_id(params[:team_id]) + render_403 if @team && !is_admin_of_team(@team) + end end end end -end \ No newline at end of file diff --git a/app/javascript/src/components/InviteUsersModal/components/InviteUsersButton.jsx b/app/javascript/src/components/InviteUsersModal/components/InviteUsersButton.jsx index 0ddd97ccf..4b38f0092 100644 --- a/app/javascript/src/components/InviteUsersModal/components/InviteUsersButton.jsx +++ b/app/javascript/src/components/InviteUsersModal/components/InviteUsersButton.jsx @@ -1,27 +1,28 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; -import { DropdownButton, MenuItem } from 'react-bootstrap'; +import React from "react"; +import { func } from "prop-types"; +import { FormattedMessage } from "react-intl"; +import { DropdownButton, MenuItem } from "react-bootstrap"; -const InviteUsersButton = props => - } - id="invite_users.submit_button" - > - props.handleClick('guest')}> - - - props.handleClick('normal_user')}> - - - props.handleClick('admin')}> - - - ; +const InviteUsersButton = props => ( + } + id="invite_users.submit_button" + > + props.handleClick("guest")}> + + + props.handleClick("normal_user")}> + + + props.handleClick("admin")}> + + + +); InviteUsersButton.propTypes = { - handleClick: PropTypes.func.isRequired + handleClick: func.isRequired }; export default InviteUsersButton; diff --git a/app/javascript/src/components/InviteUsersModal/components/InviteUsersForm.jsx b/app/javascript/src/components/InviteUsersModal/components/InviteUsersForm.jsx index 6e8576360..3f0805b24 100644 --- a/app/javascript/src/components/InviteUsersModal/components/InviteUsersForm.jsx +++ b/app/javascript/src/components/InviteUsersModal/components/InviteUsersForm.jsx @@ -1,38 +1,42 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; -import { FormGroup, HelpBlock } from 'react-bootstrap'; -import TagsInput from 'react-tagsinput'; +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'; +import { INVITE_USERS_LIMIT } from "../../../config/constants/numeric"; -const InviteUsersForm = props => - -

- -

- - - - - - -
; +const InviteUsersForm = props => ( + +

+ +

+ + + + + + +
+); InviteUsersForm.propTypes = { - tags: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, - handleChange: PropTypes.func.isRequired, - teamName: PropTypes.string.isRequired + tags: arrayOf(string.isRequired).isRequired, + handleChange: func.isRequired, + teamName: string.isRequired }; export default InviteUsersForm; diff --git a/app/javascript/src/components/InviteUsersModal/components/InviteUsersResults.jsx b/app/javascript/src/components/InviteUsersModal/components/InviteUsersResults.jsx index 87d2ff153..6c27fb14c 100644 --- a/app/javascript/src/components/InviteUsersModal/components/InviteUsersResults.jsx +++ b/app/javascript/src/components/InviteUsersModal/components/InviteUsersResults.jsx @@ -1,30 +1,38 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Alert } from 'react-bootstrap'; -import { FormattedMessage } from 'react-intl'; +import React from "react"; +import { shape, arrayOf, string } from "prop-types"; +import { Alert } from "react-bootstrap"; +import { FormattedMessage } from "react-intl"; -const InviteUsersResults = props => -
-
- -
-
- {props.results.invite_results.map(result => - - {result.email} -  -  - , - nr: result.invite_limit - }} /> - - )} -
; +const InviteUsersResults = props => ( +
+
+ +
+
+ {props.results.invite_results.map(result => ( + + {result.email} +  -  + + ), + nr: result.invite_limit + }} + /> + + ))} +
+); InviteUsersResults.propTypes = { - results: PropTypes.object.isRequired + results: shape({ + invite_results: arrayOf.isRequired, + team_name: string.isRequired + }).isRequired }; export default InviteUsersResults; - diff --git a/app/javascript/src/components/InviteUsersModal/index.jsx b/app/javascript/src/components/InviteUsersModal/index.jsx index b4ab3b8be..8d702cfa2 100644 --- a/app/javascript/src/components/InviteUsersModal/index.jsx +++ b/app/javascript/src/components/InviteUsersModal/index.jsx @@ -1,107 +1,115 @@ -import React, { Component } from 'react'; -import PropTypes 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 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'; +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; -`; +const StyledButtonToolbar = styled(ButtonToolbar)`float: right;`; class InviteUsersModal extends Component { - constructor() { - super(); - this.state = { - showInviteUsersResults: false, - inputTags: [], - inviteResults: [] - }; - this.handleInputChange = this.handleInputChange.bind(this); - this.inviteAs = this.inviteAs.bind(this); - this.handleCloseModal = this.handleCloseModal.bind(this) - } + 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); - }) - } + 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 }); - } + 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 }); - this.setState({ showInviteUsersResults: true }); - }) - .catch(error => {}); - } + 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 }); + this.setState({ showInviteUsersResults: true }); + }) + .catch(error => {}); + } - render() { - let modalBody = null; - let inviteButton = null; - if (this.state.showInviteUsersResults) { - modalBody = ; - inviteButton = null; - } else { - modalBody = ; - inviteButton = ; - } + render() { + let modalBody = null; + let inviteButton = null; + if (this.state.showInviteUsersResults) { + modalBody = ; + inviteButton = null; + } else { + modalBody = ( + + ); + inviteButton = ; + } - return ( - - - - - - - - {modalBody} - - - - - {inviteButton} - - - - ); - } + return ( + + + + + + + {modalBody} + + + + {inviteButton} + + + + ); + } } InviteUsersModal.propTypes = { - showModal: PropTypes.bool.isRequired, - onCloseModal: PropTypes.func.isRequired, - team: PropTypes.shape({ - id: PropTypes.number.isRequired, - name: PropTypes.string.isRequired - }).isRequired, - updateUsersCallback: PropTypes.func.isRequired + showModal: bool.isRequired, + onCloseModal: func.isRequired, + team: shape({ + id: number.isRequired, + name: string.isRequired + }).isRequired, + updateUsersCallback: func.isRequired }; export default InviteUsersModal; diff --git a/app/javascript/src/config/api_endpoints.js b/app/javascript/src/config/api_endpoints.js index 9e1fa0172..9c066088b 100644 --- a/app/javascript/src/config/api_endpoints.js +++ b/app/javascript/src/config/api_endpoints.js @@ -39,7 +39,7 @@ export const CHANGE_USER_RECENT_NOTIFICATION_EMAIL_PATH = "/client_api/users/change_recent_notification_email"; export const CHANGE_USER_SYSTEM_MESSAGE_NOTIFICATION_EMAIL_PATH = "/client_api/users/change_system_notification_email"; -export const INVITE_USERS_PATH = '/client_api/users/invite_users'; +export const INVITE_USERS_PATH = "/client_api/users/invite_users"; // info dropdown_title export const CUSTOMER_SUPPORT_LINK = "http://scinote.net/support"; diff --git a/app/javascript/src/config/locales/messages.js b/app/javascript/src/config/locales/messages.js index 037c2fb8e..b1f64bd33 100644 --- a/app/javascript/src/config/locales/messages.js +++ b/app/javascript/src/config/locales/messages.js @@ -1,25 +1,25 @@ export default { - 'en-US': { + "en-US": { general: { - close: 'Close', - cancel: 'Cancel', - update: 'Update', - edit: 'Edit', - loading: 'Loading ...' + close: "Close", + cancel: "Cancel", + update: "Update", + edit: "Edit", + loading: "Loading ..." }, error_messages: { text_too_short: "is too short (minimum is {min_length} characters)", text_too_long: "is too long (maximum is {max_length} characters)" }, navbar: { - page_title: 'sciNote', - home_label: 'Home', - protocols_label: 'Protocols', - repositories_label: 'Repositories', - activities_label: 'Activities', - search_label: 'Search', - notifications_label: 'Notifications', - info_label: 'Info' + page_title: "sciNote", + home_label: "Home", + protocols_label: "Protocols", + repositories_label: "Repositories", + activities_label: "Activities", + search_label: "Search", + notifications_label: "Notifications", + info_label: "Info" }, settings_page: { all_teams: "All teams", @@ -54,8 +54,8 @@ export default { "Assignment notifications appear whenever you get assigned to a team, project, task.", recent_changes: "Recent changes", recent_changes_msg: - 'Recent changes notifications appear whenever there is a change on a task you are assigned to.', - system_message: 'System message', + "Recent changes notifications appear whenever there is a change on a task you are assigned to.", + system_message: "System message", system_message_msg: "System message notifications are specifically sent by site maintainers to notify all users about a system update.", show_in_scinote: "Show in sciNote", @@ -64,20 +64,28 @@ export default { yes: "Yes", leave_team_modal: { 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:", - warning_message_one: "you will lose access to all content belonging to the team (including projects, tasks, protocols and activities);", - warning_message_two: "all projects in the team where you were the sole Owner 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.", + warning_message_one: + "you will lose access to all content belonging to the team (including projects, tasks, protocols and activities);", + warning_message_two: + "all projects in the team where you were the sole Owner 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" }, remove_user_modal: { 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:", - warning_message_one: "user will lose access to all content belonging to the team (including projects, tasks, protocols and activities);", - warning_message_two: "all projects in the team where user was the sole Owner 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.", + warning_message_one: + "user will lose access to all content belonging to the team (including projects, tasks, protocols and activities);", + warning_message_two: + "all projects in the team where user was the sole Owner 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" }, update_team_description_modal: { @@ -114,61 +122,65 @@ export default { } }, activities: { - modal_title: 'Activities', - no_data: 'No Data', - more_activities: 'More Activities' + modal_title: "Activities", + no_data: "No Data", + more_activities: "More Activities" }, global_team_switch: { - new_team: 'New team' + new_team: "New team" }, notifications: { - dropdown_title: 'Notifications', - dropdown_settings_link: 'Settings', - dropdown_show_all: 'Show all notifications' + dropdown_title: "Notifications", + dropdown_settings_link: "Settings", + dropdown_show_all: "Show all notifications" }, info_dropdown: { - customer_support: 'Customer support', - tutorials: 'Tutorials', - release_notes: 'Release notes', - premium: 'Premium', - contact_us: 'Contact us' + customer_support: "Customer support", + tutorials: "Tutorials", + release_notes: "Release notes", + premium: "Premium", + contact_us: "Contact us" }, user_account_dropdown: { - greeting: 'Hi, {name}', - settings: 'Settings', - log_out: 'Log out' + greeting: "Hi, {name}", + settings: "Settings", + log_out: "Log out" }, 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.', + 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' + invite: "Invite user/s", + guest: "as Guest/s", + normal_user: "as Normal user/s", + admin: "as Administrator/s" }, - results_title: 'Invitation results:', + results_title: "Invitation results:", roles: { - guest: 'Guest', - normal_user: 'Normal user', - admin: 'Administrator' + guest: "Guest", + normal_user: "Normal user", + admin: "Administrator" }, results_msg: { - user_exists: 'User is already a member of sciNote.', + 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 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 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 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.' + "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." } } } diff --git a/app/javascript/src/scenes/SettingsPage/scenes/team/components/TeamsMembers.jsx b/app/javascript/src/scenes/SettingsPage/scenes/team/components/TeamsMembers.jsx index bed7cf04b..8f703018b 100644 --- a/app/javascript/src/scenes/SettingsPage/scenes/team/components/TeamsMembers.jsx +++ b/app/javascript/src/scenes/SettingsPage/scenes/team/components/TeamsMembers.jsx @@ -10,7 +10,7 @@ import { import { FormattedMessage } from "react-intl"; import axios from "../../../../../config/axios"; -import InviteUsersModal from '../../../../../components/InviteUsersModal'; +import InviteUsersModal from "../../../../../components/InviteUsersModal"; import RemoveUserModal from "./RemoveUserModal"; import DataTable from "../../../../../components/data_table"; import { UPDATE_USER_TEAM_ROLE_PATH } from "../../../../../config/api_endpoints"; @@ -30,8 +30,12 @@ class TeamsMembers extends Component { userToRemove: initalUserToRemove }; this.memberAction = this.memberAction.bind(this); - this.showInviteUsersModalCallback = this.showInviteUsersModalCallback.bind(this); - this.closeInviteUsersModalCallback = this.closeInviteUsersModalCallback.bind(this); + this.showInviteUsersModalCallback = this.showInviteUsersModalCallback.bind( + this + ); + this.closeInviteUsersModalCallback = this.closeInviteUsersModalCallback.bind( + this + ); this.hideModal = this.hideModal.bind(this); } @@ -184,7 +188,7 @@ class TeamsMembers extends Component { } > - diff --git a/app/javascript/src/styles/main.scss b/app/javascript/src/styles/main.scss index 5c5a8f1fb..54cb15064 100644 --- a/app/javascript/src/styles/main.scss +++ b/app/javascript/src/styles/main.scss @@ -1,26 +1,26 @@ -@import 'constants'; -@import 'react-bootstrap-timezone-picker/dist/react-bootstrap-timezone-picker.min.css'; -@import '~react-bootstrap-table/dist/react-bootstrap-table.min'; -@import 'react-tagsinput/react-tagsinput.css'; +@import "constants"; +@import "react-bootstrap-timezone-picker/dist/react-bootstrap-timezone-picker.min.css"; +@import "~react-bootstrap-table/dist/react-bootstrap-table.min"; +@import "react-tagsinput/react-tagsinput.css"; body { - background-color: $color-concrete; - color: $color-emperor; - font-family: "Open Sans", Arial, Helvetica, sans-serif; - font-size: 13px; + background-color: $color-concrete; + color: $color-emperor; + font-family: "Open Sans", Arial, Helvetica, sans-serif; + font-size: 13px; } .label-primary { - background-color: $color-theme-primary; + background-color: $color-theme-primary; } .btn-primary { - background-color: $color-theme-secondary; - border-color: $primary-hover-color; - margin-right: 7px; - &:hover { - background-color: $primary-hover-color; - } + background-color: $color-theme-secondary; + border-color: $primary-hover-color; + margin-right: 7px; + &:hover { + background-color: $primary-hover-color; + } } // // fixes issue with dropdown in datatable diff --git a/app/services/client_api/invitations_service.rb b/app/services/client_api/invitations_service.rb index 9971eb275..4abb0e04f 100644 --- a/app/services/client_api/invitations_service.rb +++ b/app/services/client_api/invitations_service.rb @@ -11,12 +11,14 @@ module ClientApi raise ClientApi::CustomInvitationsError unless @emails && @team && @role @emails && @emails.empty? { raise ClientApi::CustomInvitationsError } - @role && !UserTeam.roles.keys.include?(@role) { raise ClientApi::CustomInvitationsError } + 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 @@ -33,52 +35,13 @@ module ClientApi # Check if user already exists user = User.find_by_email(email) if User.exists?(email: email) - # User does not exist - if user.blank? - 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 - 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 - # User exists - else - result[:status] = :"#{:user_exists}#{:_unconfirmed if !user.confirmed?}" - result[:alert] = :info - result[:user] = user - - # Invitation to team - if @team.present? - user_team = - UserTeam.where(user: user, team: @team).first if UserTeam.exists?(user: user, team: @team) - - if user_team.present? - result[:status] = :"#{:user_exists_and_in_team}#{:_unconfirmed if !user.confirmed?}" - - else - user_team = create_user_team_relation_and_notification(user) - result[:status] = :"#{:user_exists_invited_to_team}#{:_unconfirmed if !user.confirmed?}" - end - result[:user_team] = user_team - end - end + result = if user.blank? + # User does not exist + handle_new_user(result, email, user) + else + # User exists + handle_existing_user(result, user) + end invite_results << result end invite_results @@ -86,6 +49,61 @@ module ClientApi private + 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, @@ -140,4 +158,4 @@ module ClientApi end CustomInvitationsError = Class.new(StandardError) -end \ No newline at end of file +end diff --git a/app/views/client_api/users/invite_users.json.jbuilder b/app/views/client_api/users/invite_users.json.jbuilder index 95da57caf..d0c4d71c2 100644 --- a/app/views/client_api/users/invite_users.json.jbuilder +++ b/app/views/client_api/users/invite_users.json.jbuilder @@ -2,7 +2,11 @@ json.invite_results invite_results do |invite_result| json.status invite_result[:status] json.alert invite_result[:alert] json.email invite_result[:email] - json.user_role invite_result[:user_team].role if invite_result[:user_team].present? - json.invite_limit invite_result[:invite_user_limit] if invite_result[:invite_user_limit].present? + 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? \ No newline at end of file +json.team_name team.name if team.present? diff --git a/app/views/users/settings/teams/show.html.erb b/app/views/users/settings/teams/show.html.erb index 13b420746..a432bd8ec 100644 --- a/app/views/users/settings/teams/show.html.erb +++ b/app/views/users/settings/teams/show.html.erb @@ -83,7 +83,8 @@
- + <%= t("users.settings.teams.edit.add_user") %> From ac3601bd2ef712c3cf601c967f42b05fbe7ba292 Mon Sep 17 00:00:00 2001 From: mlorb Date: Thu, 21 Sep 2017 10:56:17 +0200 Subject: [PATCH 3/5] hound --- app/javascript/src/styles/main.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/javascript/src/styles/main.scss b/app/javascript/src/styles/main.scss index 54cb15064..434b3392b 100644 --- a/app/javascript/src/styles/main.scss +++ b/app/javascript/src/styles/main.scss @@ -1,7 +1,7 @@ -@import "constants"; -@import "react-bootstrap-timezone-picker/dist/react-bootstrap-timezone-picker.min.css"; -@import "~react-bootstrap-table/dist/react-bootstrap-table.min"; -@import "react-tagsinput/react-tagsinput.css"; +@import 'constants'; +@import 'react-bootstrap-timezone-picker/dist/react-bootstrap-timezone-picker.min.css'; +@import '~react-bootstrap-table/dist/react-bootstrap-table.min'; +@import 'react-tagsinput/react-tagsinput.css'; body { background-color: $color-concrete; From 9d96bf668a1764e1b163f9536d61693f75617f8c Mon Sep 17 00:00:00 2001 From: mlorb Date: Thu, 21 Sep 2017 16:42:56 +0200 Subject: [PATCH 4/5] fix error handeling, refactoring --- .../users/invitations_controller.rb | 9 ++- .../src/components/InviteUsersModal/index.jsx | 11 ++- app/javascript/src/config/locales/messages.js | 74 +++++++++---------- .../client_api/invitations_service.rb | 17 ++--- config/locales/en.yml | 2 + 5 files changed, 63 insertions(+), 50 deletions(-) diff --git a/app/controllers/client_api/users/invitations_controller.rb b/app/controllers/client_api/users/invitations_controller.rb index 87789769e..7765cf609 100644 --- a/app/controllers/client_api/users/invitations_controller.rb +++ b/app/controllers/client_api/users/invitations_controller.rb @@ -27,7 +27,14 @@ module ClientApi def check_invite_users_permission @team = Team.find_by_id(params[:team_id]) - render_403 if @team && !is_admin_of_team(@team) + 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 diff --git a/app/javascript/src/components/InviteUsersModal/index.jsx b/app/javascript/src/components/InviteUsersModal/index.jsx index 8d702cfa2..a6a82a0d2 100644 --- a/app/javascript/src/components/InviteUsersModal/index.jsx +++ b/app/javascript/src/components/InviteUsersModal/index.jsx @@ -55,10 +55,15 @@ class InviteUsersModal extends Component { team_id: this.props.team.id }) .then(({ data }) => { - this.setState({ inviteResults: data }); - this.setState({ showInviteUsersResults: true }); + this.setState({ inviteResults: data, showInviteUsersResults: true}); }) - .catch(error => {}); + .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() { diff --git a/app/javascript/src/config/locales/messages.js b/app/javascript/src/config/locales/messages.js index b1f64bd33..07c3295eb 100644 --- a/app/javascript/src/config/locales/messages.js +++ b/app/javascript/src/config/locales/messages.js @@ -21,6 +21,43 @@ export default { notifications_label: "Notifications", 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: { all_teams: "All teams", in_team: "You are member of {num} team", @@ -145,43 +182,6 @@ export default { greeting: "Hi, {name}", settings: "Settings", log_out: "Log out" - }, - 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." - } } } }; diff --git a/app/services/client_api/invitations_service.rb b/app/services/client_api/invitations_service.rb index 4abb0e04f..a605b55f0 100644 --- a/app/services/client_api/invitations_service.rb +++ b/app/services/client_api/invitations_service.rb @@ -34,14 +34,8 @@ module ClientApi # Check if user already exists user = User.find_by_email(email) if User.exists?(email: email) - - result = if user.blank? - # User does not exist - handle_new_user(result, email, user) - else - # User exists - handle_existing_user(result, user) - end + # Handle user invitation + result = handle_user(result, email, user) invite_results << result end invite_results @@ -49,6 +43,11 @@ module ClientApi 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 @@ -151,7 +150,7 @@ module ClientApi message: sanitize_input(message) ) - if target_user.assignments_notification + if target_user.settings[:notifications][:assignments] UserNotification.create(notification: notification, user: target_user) end end diff --git a/config/locales/en.yml b/config/locales/en.yml index c266d562c..e5112418b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1825,3 +1825,5 @@ en: user_teams: leave_team_error: "An error occured." 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." From 4ef7b57f315792a2f500a4b3306e913b3bdc5bef Mon Sep 17 00:00:00 2001 From: mlorb Date: Fri, 22 Sep 2017 09:57:41 +0200 Subject: [PATCH 5/5] fix error handeling --- app/services/client_api/invitations_service.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/client_api/invitations_service.rb b/app/services/client_api/invitations_service.rb index a605b55f0..3c710fbd4 100644 --- a/app/services/client_api/invitations_service.rb +++ b/app/services/client_api/invitations_service.rb @@ -9,8 +9,8 @@ module ClientApi @team = args[:team] @role = args[:role] - raise ClientApi::CustomInvitationsError unless @emails && @team && @role - @emails && @emails.empty? { raise ClientApi::CustomInvitationsError } + raise ClientApi::CustomInvitationsError unless @team && @role && + @emails && @emails.present? if @role && !UserTeam.roles.keys.include?(@role) raise ClientApi::CustomInvitationsError end