mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-09-08 14:15:35 +08:00
Merge pull request #781 from ZmagoD/zd_SCI_1499
Write single team page in React.js
This commit is contained in:
commit
4e8381d9ba
46 changed files with 1696 additions and 205 deletions
1
Gemfile
1
Gemfile
|
@ -92,6 +92,7 @@ group :development, :test do
|
|||
gem 'rubocop', require: false
|
||||
gem 'scss_lint', require: false
|
||||
gem 'starscope', require: false
|
||||
gem 'bullet'
|
||||
end
|
||||
|
||||
group :test do
|
||||
|
|
|
@ -105,6 +105,9 @@ GEM
|
|||
momentjs-rails (>= 2.8.1)
|
||||
bootstrap_form (2.7.0)
|
||||
builder (3.2.3)
|
||||
bullet (5.6.1)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.10.0)
|
||||
byebug (9.0.6)
|
||||
capybara (2.15.1)
|
||||
addressable
|
||||
|
@ -431,6 +434,7 @@ GEM
|
|||
execjs (>= 0.3.0, < 3)
|
||||
underscore-rails (1.8.3)
|
||||
unicode-display_width (1.3.0)
|
||||
uniform_notifier (1.10.0)
|
||||
warden (1.2.7)
|
||||
rack (>= 1.0)
|
||||
webpacker (2.0)
|
||||
|
@ -467,6 +471,7 @@ DEPENDENCIES
|
|||
bootstrap-select-rails
|
||||
bootstrap3-datetimepicker-rails (~> 4.15.35)
|
||||
bootstrap_form
|
||||
bullet
|
||||
byebug
|
||||
capybara
|
||||
commit_param_routing
|
||||
|
@ -543,4 +548,4 @@ RUBY VERSION
|
|||
ruby 2.4.1p111
|
||||
|
||||
BUNDLED WITH
|
||||
1.15.3
|
||||
1.15.4
|
||||
|
|
65
app/controllers/client_api/teams/teams_controller.rb
Normal file
65
app/controllers/client_api/teams/teams_controller.rb
Normal file
|
@ -0,0 +1,65 @@
|
|||
module ClientApi
|
||||
module Teams
|
||||
class TeamsController < ApplicationController
|
||||
include ClientApi::Users::UserTeamsHelper
|
||||
|
||||
def index
|
||||
success_response('/client_api/teams/index',
|
||||
teams: current_user.teams_data)
|
||||
end
|
||||
|
||||
def details
|
||||
team_service = ClientApi::TeamsService.new(team_id: params[:team_id],
|
||||
current_user: current_user)
|
||||
success_response('/client_api/teams/details',
|
||||
team_service.team_page_details_data)
|
||||
rescue ClientApi::CustomTeamError
|
||||
error_response
|
||||
end
|
||||
|
||||
def change_team
|
||||
team_service = ClientApi::TeamsService.new(team_id: params[:team_id],
|
||||
current_user: current_user)
|
||||
team_service.change_current_team!
|
||||
success_response('/client_api/teams/index', team_service.teams_data)
|
||||
rescue ClientApi::CustomTeamError
|
||||
error_response
|
||||
end
|
||||
|
||||
def update
|
||||
team_service = ClientApi::TeamsService.new(team_id: params[:team_id],
|
||||
current_user: current_user,
|
||||
params: team_params)
|
||||
team_service.update_team!
|
||||
success_response('/client_api/teams/update_details',
|
||||
team_service.single_team_details_data)
|
||||
rescue ClientApi::CustomTeamError => error
|
||||
error_response(error.to_s)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def team_params
|
||||
params.require(:team).permit(:description, :name)
|
||||
end
|
||||
|
||||
def success_response(template, locals)
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render template: template,
|
||||
status: :ok,
|
||||
locals: locals
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def error_response(message = t('client_api.generic_error_message'))
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: { message: message }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,50 +0,0 @@
|
|||
module ClientApi
|
||||
class TeamsController < ApplicationController
|
||||
include ClientApi::Users::UserTeamsHelper
|
||||
MissingTeamError = Class.new(StandardError)
|
||||
|
||||
def index
|
||||
success_response
|
||||
end
|
||||
|
||||
def change_team
|
||||
change_current_team
|
||||
success_response
|
||||
rescue MissingTeamError
|
||||
error_response
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def success_response
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render template: '/client_api/teams/index',
|
||||
status: :ok,
|
||||
locals: teams
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def error_response
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: { message: 'Bad boy!' }, status: :bad_request
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def teams
|
||||
{ teams: current_user.teams_data }
|
||||
end
|
||||
|
||||
def change_current_team
|
||||
team_id = params.fetch(:team_id) { raise MissingTeamError }
|
||||
unless current_user.teams.pluck(:id).include? team_id
|
||||
raise MissingTeamError
|
||||
end
|
||||
current_user.update_attribute(:current_team_id, team_id)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -1,86 +1,67 @@
|
|||
module ClientApi
|
||||
module Users
|
||||
class UserTeamsController < ApplicationController
|
||||
include NotificationsHelper
|
||||
include InputSanitizeHelper
|
||||
include ClientApi::Users::UserTeamsHelper
|
||||
|
||||
before_action :find_user_team, only: :leave_team
|
||||
|
||||
def leave_team
|
||||
if user_cant_leave?
|
||||
unsuccess_response
|
||||
else
|
||||
begin
|
||||
assign_new_team_owner
|
||||
generate_new_notification
|
||||
success_response
|
||||
rescue
|
||||
unsuccess_response
|
||||
end
|
||||
end
|
||||
ut_service = ClientApi::UserTeamService.new(
|
||||
user: current_user,
|
||||
team_id: params[:team],
|
||||
user_team_id: params[:user_team]
|
||||
)
|
||||
ut_service.destroy_user_team_and_assign_new_team_owner!
|
||||
success_response('/client_api/teams/index', ut_service.teams_data)
|
||||
rescue ClientApi::CustomUserTeamError
|
||||
unsuccess_response(t('client_api.user_teams.leave_team_error'))
|
||||
end
|
||||
|
||||
def update_role
|
||||
ut_service = ClientApi::UserTeamService.new(
|
||||
user: current_user,
|
||||
team_id: params[:team],
|
||||
user_team_id: params[:user_team],
|
||||
role: params[:role]
|
||||
)
|
||||
ut_service.update_role!
|
||||
success_response('/client_api/teams/team_users',
|
||||
ut_service.team_users_data)
|
||||
rescue ClientApi::CustomUserTeamError => error
|
||||
unsuccess_response(error.to_s)
|
||||
end
|
||||
|
||||
def remove_user
|
||||
ut_service = ClientApi::UserTeamService.new(
|
||||
user: current_user,
|
||||
team_id: params[:team],
|
||||
user_team_id: params[:user_team]
|
||||
)
|
||||
ut_service.destroy_user_team_and_assign_new_team_owner!
|
||||
success_response('/client_api/teams/team_users',
|
||||
ut_service.team_users_data)
|
||||
rescue ClientApi::CustomUserTeamError => error
|
||||
unsuccess_response(error.to_s)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_user_team
|
||||
@team = Team.find_by_id(params[:team])
|
||||
@user_team = UserTeam.where(team: @team, user: current_user).first
|
||||
end
|
||||
|
||||
def user_cant_leave?
|
||||
return unless @user_team && @team
|
||||
@user_team.admin? &&
|
||||
@team.user_teams.where(role: 2).count <= 1
|
||||
end
|
||||
|
||||
def success_response
|
||||
def success_response(template, locals)
|
||||
respond_to do |format|
|
||||
# return a list of teams
|
||||
format.json do
|
||||
render template: '/client_api/teams/index',
|
||||
render template: template,
|
||||
status: :ok,
|
||||
locals: {
|
||||
teams: current_user.teams_data,
|
||||
flash_message: t('client_api.user_teams.leave_flash',
|
||||
team: @team.name)
|
||||
}
|
||||
locals: locals
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def unsuccess_response
|
||||
def unsuccess_response(message)
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: { message: t(
|
||||
'client_api.user_teams.leave_team_error'
|
||||
) },
|
||||
render json: { message: message },
|
||||
status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def assign_new_team_owner
|
||||
new_owner = @team.user_teams
|
||||
.where(role: 2)
|
||||
.where.not(id: @user_team.id)
|
||||
.first.user
|
||||
new_owner ||= current_user
|
||||
reset_user_current_team(@user_team)
|
||||
@user_team.destroy(new_owner)
|
||||
end
|
||||
|
||||
def reset_user_current_team(user_team)
|
||||
ids = user_team.user.teams_ids
|
||||
ids -= [user_team.team.id]
|
||||
user_team.user.current_team_id = ids.first
|
||||
user_team.user.save
|
||||
end
|
||||
|
||||
def generate_new_notification
|
||||
generate_notification(@user_team.user, @user_team.user, @user_team.team,
|
||||
false, false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// teams
|
||||
export const SET_CURRENT_TEAM = "SET_CURRENT_TEAM";
|
||||
export const GET_LIST_OF_TEAMS = "GET_LIST_OF_TEAMS";
|
||||
export const SET_TEAM_DETAILS = "SET_TEAM_DETAILS";
|
||||
|
||||
// activities
|
||||
export const GLOBAL_ACTIVITIES_DATA = "GLOBAL_ACTIVITIES_DATA";
|
||||
|
@ -26,7 +27,10 @@ export const CHANGE_SYSTEM_MESSAGE_NOTIFICATION_EMAIL =
|
|||
|
||||
// user teams
|
||||
export const LEAVE_TEAM = "LEAVE_TEAM";
|
||||
|
||||
// modals
|
||||
export const SHOW_LEAVE_TEAM_MODAL = "SHOW_LEAVE_TEAM_MODAL";
|
||||
export const UPDATE_TEAM_DESCRIPTION_MODAL = "UPDATE_TEAM_DESCRIPTION_MODAL";
|
||||
|
||||
// spinner
|
||||
export const SPINNER_ON = "SPINNER_ON";
|
||||
|
|
|
@ -7,7 +7,7 @@ export const BORDER_LIGHT_COLOR = "#e3e3e3";
|
|||
export const WILD_SAND_COLOR = "#f5f5f5";
|
||||
export const MYSTIC_COLOR = "#eaeff2";
|
||||
export const COLOR_CONCRETE = "#f2f2f2";
|
||||
export const COLOR_MINE_SHAFT = "#333"
|
||||
export const COLOR_MINE_SHAFT = "#333";
|
||||
export const COLOR_BLACK = "#000";
|
||||
export const COLOR_GRAY_LIGHT_YADCF = "#ccc";
|
||||
export const ICON_GREEN_COLOR = "#8fd13f";
|
||||
|
@ -18,3 +18,4 @@ export const SIDEBAR_HOVER_GRAY_COLOR = "#D2D2D2";
|
|||
export const COLOR_ALTO = "#dddddd";
|
||||
export const COLOR_GRAY = "#909088";
|
||||
export const COLOR_ALABASTER = "#fcfcfc";
|
||||
export const COLOR_APPLE_BLOSSOM = "#a94442";
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
export const ENTER_KEY_CODE = 13;
|
||||
export const TEXT_MAX_LENGTH = 10000;
|
||||
export const NAME_MAX_LENGTH = 255;
|
||||
|
|
3
app/javascript/packs/app/dom_routes.js
Normal file
3
app/javascript/packs/app/dom_routes.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
// Settings page
|
||||
export const SETTINGS_TEAMS_ROUTE = "/settings/teams";
|
||||
export const SETTINGS_TEAM_ROUTE = "/settings/teams/:id";
|
|
@ -1,11 +1,11 @@
|
|||
import { combineReducers } from "redux";
|
||||
import {
|
||||
setCurrentTeam,
|
||||
getListOfTeams
|
||||
getListOfTeams,
|
||||
showLeaveTeamModal,
|
||||
} from "../shared/reducers/TeamReducers";
|
||||
import { globalActivities } from "../shared/reducers/ActivitiesReducers";
|
||||
import { currentUser } from "../shared/reducers/UsersReducer";
|
||||
import { showLeaveTeamModal } from "../shared/reducers/LeaveTeamReducer";
|
||||
|
||||
export default combineReducers({
|
||||
current_team: setCurrentTeam,
|
||||
|
|
|
@ -13,7 +13,9 @@ export const SETTINGS_ACCOUNT_PREFERENCES_PATH =
|
|||
|
||||
// teams
|
||||
export const TEAMS_PATH = "/client_api/teams";
|
||||
export const CHANGE_TEAM_PATH = "/client_api/change_team";
|
||||
export const CHANGE_TEAM_PATH = "/client_api/teams/change_team";
|
||||
export const TEAM_DETAILS_PATH = "/client_api/teams/:team_id/details";
|
||||
export const TEAM_UPDATE_PATH = "/client_api/teams/update";
|
||||
|
||||
// search
|
||||
export const SEARCH_PATH = "/search";
|
||||
|
@ -49,6 +51,8 @@ export const CONTACT_US_LINK =
|
|||
|
||||
// user teams
|
||||
export const LEAVE_TEAM_PATH = "/client_api/users/leave_team";
|
||||
export const UPDATE_USER_TEAM_ROLE_PATH = "/client_api/users/update_role";
|
||||
export const REMOVE_USER_FROM_TEAM_PATH = "/client_api/users/remove_user";
|
||||
|
||||
// settings
|
||||
export const SETTINGS_ACCOUNT_PROFILE = "/settings/account/profile";
|
||||
|
|
|
@ -7,6 +7,9 @@ export default {
|
|||
edit: "Edit",
|
||||
loading: "Loading ..."
|
||||
},
|
||||
error_messages: {
|
||||
text_too_long: "is too long (maximum is {max_length} characters)"
|
||||
},
|
||||
navbar: {
|
||||
page_title: "sciNote",
|
||||
home_label: "Home",
|
||||
|
@ -22,15 +25,6 @@ export default {
|
|||
in_team: "You are member of {num} team",
|
||||
in_teams: "You are member of {num} team",
|
||||
leave_team: "Leave team",
|
||||
leave_team_modal: {
|
||||
title: "Leave team {teamName}",
|
||||
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 <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"
|
||||
},
|
||||
account: "Account",
|
||||
team: "Team",
|
||||
avatar: "Avatar",
|
||||
|
@ -66,7 +60,48 @@ export default {
|
|||
show_in_scinote: "Show in sciNote",
|
||||
notify_me_via_email: "Notify me via email",
|
||||
no: "No",
|
||||
yes: "Yes"
|
||||
yes: "Yes",
|
||||
leave_team_modal: {
|
||||
title: "Leave team {teamName}",
|
||||
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 <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"
|
||||
},
|
||||
remove_user_modal: {
|
||||
title: "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 <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"
|
||||
},
|
||||
update_team_description_modal: {
|
||||
title: "Edit team description",
|
||||
label: "Description"
|
||||
},
|
||||
update_team_name_modal: {
|
||||
title: "Edit team name",
|
||||
label: "Name"
|
||||
},
|
||||
single_team: {
|
||||
created_on: "Created on: <strong>{created_at}</strong>",
|
||||
created_by: "Created by: <strong>{created_by}</strong>",
|
||||
space_usage: "Space usage: <strong>{space_usage}</strong>",
|
||||
no_description: "<i>No description</i>",
|
||||
members_panel_title: "Team members",
|
||||
add_members: "Add team members",
|
||||
actions: {
|
||||
user_role: "User role",
|
||||
guest: "Guest",
|
||||
normal_user: "Normal user",
|
||||
administrator: "Administrator",
|
||||
remove_user: "Remove"
|
||||
}
|
||||
}
|
||||
},
|
||||
activities: {
|
||||
modal_title: "Activities",
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import { SHOW_LEAVE_TEAM_MODAL } from "../../app/action_types";
|
||||
|
||||
export function leaveTeamModalShow(show = false, id = 0, teamName = "") {
|
||||
return {
|
||||
payload: { show, id, teamName },
|
||||
type: SHOW_LEAVE_TEAM_MODAL
|
||||
};
|
||||
}
|
|
@ -1,7 +1,18 @@
|
|||
import axios from "../../app/axios";
|
||||
import _ from "lodash";
|
||||
import { TEAMS_PATH, CHANGE_TEAM_PATH } from "../../app/routes";
|
||||
import { GET_LIST_OF_TEAMS, SET_CURRENT_TEAM } from "../../app/action_types";
|
||||
import {
|
||||
GET_LIST_OF_TEAMS,
|
||||
SET_CURRENT_TEAM,
|
||||
SHOW_LEAVE_TEAM_MODAL
|
||||
} from "../../app/action_types";
|
||||
|
||||
export function leaveTeamModalShow(show = false, team = {}) {
|
||||
return {
|
||||
payload: { team, show },
|
||||
type: SHOW_LEAVE_TEAM_MODAL
|
||||
};
|
||||
}
|
||||
|
||||
export function addTeamsData(data) {
|
||||
return {
|
||||
|
@ -33,10 +44,10 @@ export function getTeamsList() {
|
|||
};
|
||||
}
|
||||
|
||||
export function changeTeam(teamId) {
|
||||
export function changeTeam(team_id) {
|
||||
return dispatch => {
|
||||
axios
|
||||
.post(CHANGE_TEAM_PATH, { teamId }, { withCredentials: true })
|
||||
.post(CHANGE_TEAM_PATH, { team_id }, { withCredentials: true })
|
||||
.then(response => {
|
||||
const teams = response.data.teams.collection;
|
||||
dispatch(addTeamsData(teams));
|
||||
|
|
|
@ -6,8 +6,7 @@ import { connect } from "react-redux";
|
|||
import axios from "../../../app/axios";
|
||||
|
||||
import { LEAVE_TEAM_PATH } from "../../../app/routes";
|
||||
import { leaveTeamModalShow } from "../../actions/LeaveTeamActions";
|
||||
import { addTeamsData, setCurrentTeam } from "../../actions/TeamsActions";
|
||||
import { addTeamsData, setCurrentTeam, leaveTeamModalShow } from "../../actions/TeamsActions";
|
||||
|
||||
class LeaveTeamModal extends Component {
|
||||
constructor(props) {
|
||||
|
@ -21,13 +20,13 @@ class LeaveTeamModal extends Component {
|
|||
}
|
||||
|
||||
leaveTeam() {
|
||||
const teamUrl = `${LEAVE_TEAM_PATH}?team=${this.props.teamId}`;
|
||||
const teamUrl = `${LEAVE_TEAM_PATH}?team=${this.props.team
|
||||
.id}&user_team=${this.props.team.user_team_id}`;
|
||||
axios
|
||||
.delete(teamUrl, {
|
||||
withCredentials: true
|
||||
})
|
||||
.then(response => {
|
||||
console.log(response);
|
||||
const teams = response.data.teams.collection;
|
||||
this.props.addTeamsData(teams);
|
||||
const currentTeam = _.find(teams, team => team.current_team);
|
||||
|
@ -46,7 +45,7 @@ class LeaveTeamModal extends Component {
|
|||
<Modal.Title>
|
||||
<FormattedMessage
|
||||
id="settings_page.leave_team_modal.title"
|
||||
values={{ teamName: this.props.teamName }}
|
||||
values={{ teamName: this.props.team.name }}
|
||||
/>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
@ -85,15 +84,19 @@ class LeaveTeamModal extends Component {
|
|||
|
||||
LeaveTeamModal.propTypes = {
|
||||
showModal: bool.isRequired,
|
||||
teamId: number.isRequired,
|
||||
teamName: string.isRequired,
|
||||
team: PropTypes.shape({
|
||||
id: number.isRequired,
|
||||
name: string.isRequired,
|
||||
user_team_id: number.isRequired
|
||||
}).isRequired,
|
||||
addTeamsData: func.isRequired,
|
||||
leaveTeamModalShow: func.isRequired
|
||||
leaveTeamModalShow: func.isRequired,
|
||||
setCurrentTeam: func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = ({ showLeaveTeamModal }) => ({
|
||||
showModal: showLeaveTeamModal.show,
|
||||
teamId: showLeaveTeamModal.id,
|
||||
teamName: showLeaveTeamModal.teamName
|
||||
team: showLeaveTeamModal.team
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
import { SHOW_LEAVE_TEAM_MODAL } from "../../app/action_types";
|
||||
|
||||
export function showLeaveTeamModal(
|
||||
state = { show: false, id: 0, teamName: "" },
|
||||
action
|
||||
) {
|
||||
if (action.type === SHOW_LEAVE_TEAM_MODAL) {
|
||||
return { ...state, ...action.payload };
|
||||
}
|
||||
return state;
|
||||
}
|
|
@ -1,7 +1,13 @@
|
|||
import { SET_CURRENT_TEAM, GET_LIST_OF_TEAMS } from "../../app/action_types";
|
||||
import {
|
||||
SET_CURRENT_TEAM,
|
||||
GET_LIST_OF_TEAMS,
|
||||
SHOW_LEAVE_TEAM_MODAL
|
||||
} from "../../app/action_types";
|
||||
|
||||
const initialState = { name: "", id: 0, current_team: true };
|
||||
export const setCurrentTeam = (state = initialState, action) => {
|
||||
export const setCurrentTeam = (
|
||||
state = { name: "", id: 0, current_team: true },
|
||||
action
|
||||
) => {
|
||||
if (action.type === SET_CURRENT_TEAM) {
|
||||
return Object.assign({}, state, action.team);
|
||||
}
|
||||
|
@ -17,3 +23,13 @@ export const getListOfTeams = (state = { collection: [] }, action) => {
|
|||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export const showLeaveTeamModal = (
|
||||
state = { show: false, team: { id: 0, name: "", user_team_id: 0 } },
|
||||
action
|
||||
) => {
|
||||
if (action.type === SHOW_LEAVE_TEAM_MODAL) {
|
||||
return { ...state, ...action.payload };
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
|
|
@ -5,6 +5,11 @@ import { Nav, NavItem } from "react-bootstrap";
|
|||
import { FormattedMessage } from "react-intl";
|
||||
import Navigation from "../../../shared/navigation";
|
||||
|
||||
import {
|
||||
SETTINGS_TEAMS_ROUTE,
|
||||
SETTINGS_TEAM_ROUTE
|
||||
} from "../../../app/dom_routes";
|
||||
|
||||
import {
|
||||
ROOT_PATH,
|
||||
SETTINGS_PATH,
|
||||
|
@ -16,8 +21,9 @@ import {
|
|||
} from "../../../app/routes";
|
||||
|
||||
import NotFound from "../../../shared/404/NotFound";
|
||||
import SettingsAccount from ".././components/account/SettingsAccount";
|
||||
import SettingsTeams from ".././components/team/SettingsTeams";
|
||||
import SettingsAccount from "./account/SettingsAccount";
|
||||
import SettingsTeams from "./teams/SettingsTeams";
|
||||
import SettingsTeamPageContainer from "./team/SettingsTeamPageContainer";
|
||||
|
||||
export default class MainNav extends Component {
|
||||
constructor(props) {
|
||||
|
@ -64,8 +70,11 @@ export default class MainNav extends Component {
|
|||
path={SETTINGS_PATH}
|
||||
render={() => <Redirect to={SETTINGS_ACCOUNT_PROFILE_PATH} />}
|
||||
/>
|
||||
<Route path={SETTINGS_ACCOUNT_PATH} component={SettingsAccount} />
|
||||
<Route path={SETTINGS_TEAMS_PATH} component={SettingsTeams} />
|
||||
<Route
|
||||
path={SETTINGS_TEAM_ROUTE}
|
||||
component={SettingsTeamPageContainer}
|
||||
/>
|
||||
<Route path={SETTINGS_TEAMS_ROUTE} component={SettingsTeams} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,237 @@
|
|||
import React, { Component } from "react";
|
||||
import ReactRouterPropTypes from "react-router-prop-types";
|
||||
import { Link } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { Row, Col, Glyphicon, Well } from "react-bootstrap";
|
||||
import { FormattedHTMLMessage, FormattedMessage } from "react-intl";
|
||||
import moment from "moment";
|
||||
import prettysize from "prettysize";
|
||||
import axios from "../../../../app/axios";
|
||||
|
||||
import { TEAM_DETAILS_PATH, SETTINGS_TEAMS } from "../../../../app/routes";
|
||||
import { BORDER_LIGHT_COLOR } from "../../../../app/constants/colors";
|
||||
|
||||
import TeamsMembers from "./components/TeamsMembers";
|
||||
import UpdateTeamDescriptionModal from "./components/UpdateTeamDescriptionModal";
|
||||
import UpdateTeamNameModal from "./components/UpdateTeamNameModal";
|
||||
|
||||
const Wrapper = styled.div`
|
||||
background: white;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid ${BORDER_LIGHT_COLOR};
|
||||
border-top: none;
|
||||
margin: 0;
|
||||
padding: 16px 15px 50px 15px;
|
||||
`;
|
||||
|
||||
const TabTitle = styled.div`padding: 15px;`;
|
||||
|
||||
const BadgeWrapper = styled.div`
|
||||
font-size: 1.4em;
|
||||
float: left;
|
||||
padding: 6px 10px;
|
||||
background-color: #37a0d9;
|
||||
color: #fff;
|
||||
`;
|
||||
|
||||
const StyledWell = styled(Well)`
|
||||
padding: 9px;
|
||||
& > span {
|
||||
padding-left: 5px;
|
||||
}`;
|
||||
|
||||
const StyledDescriptionWell = styled(Well)`
|
||||
padding: 9px;
|
||||
& > span {
|
||||
padding-left: 5px;
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledH3 = styled.h3`
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledOl = styled.ol`padding: 15px;`;
|
||||
|
||||
class SettingsTeamPageContainer extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showDescriptionModal: false,
|
||||
showNameModal: false,
|
||||
users: [],
|
||||
team: {
|
||||
id: 0,
|
||||
name: "",
|
||||
description: "",
|
||||
created_by: "",
|
||||
created_at: "",
|
||||
space_taken: 0
|
||||
}
|
||||
};
|
||||
this.showDescriptionModal = this.showDescriptionModal.bind(this);
|
||||
this.hideDescriptionModalCallback = this.hideDescriptionModalCallback.bind(
|
||||
this
|
||||
);
|
||||
this.updateTeamCallback = this.updateTeamCallback.bind(this);
|
||||
this.updateUsersCallback = this.updateUsersCallback.bind(this);
|
||||
this.showNameModal = this.showNameModal.bind(this);
|
||||
this.hideNameModalCallback = this.hideNameModalCallback.bind(this);
|
||||
this.renderEditNameModel = this.renderEditNameModel.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { id } = this.props.match.params;
|
||||
const path = TEAM_DETAILS_PATH.replace(":team_id", id);
|
||||
axios.get(path).then(response => {
|
||||
const { team, users } = response.data.team_details;
|
||||
this.setState({ users, team });
|
||||
});
|
||||
}
|
||||
|
||||
showDescriptionModal() {
|
||||
this.setState({ showDescriptionModal: true });
|
||||
}
|
||||
|
||||
hideDescriptionModalCallback() {
|
||||
this.setState({ showDescriptionModal: false });
|
||||
}
|
||||
|
||||
showNameModal() {
|
||||
this.setState({ showNameModal: true });
|
||||
}
|
||||
|
||||
hideNameModalCallback() {
|
||||
this.setState({ showNameModal: false });
|
||||
}
|
||||
|
||||
updateTeamCallback(team) {
|
||||
this.setState({ team });
|
||||
}
|
||||
|
||||
updateUsersCallback(users) {
|
||||
this.setState({ users });
|
||||
}
|
||||
|
||||
renderDescription() {
|
||||
if (this.state.team.description) {
|
||||
return this.state.team.description;
|
||||
}
|
||||
return (
|
||||
<FormattedHTMLMessage id="settings_page.single_team.no_description" />
|
||||
);
|
||||
}
|
||||
|
||||
renderEditNameModel() {
|
||||
if (this.state.showNameModal) {
|
||||
return(
|
||||
<UpdateTeamNameModal
|
||||
showModal={this.state.showNameModal}
|
||||
hideModal={this.hideNameModalCallback}
|
||||
team={this.state.team}
|
||||
updateTeamCallback={this.updateTeamCallback}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Wrapper>
|
||||
<StyledOl className="breadcrumb">
|
||||
<li>
|
||||
<Link to={SETTINGS_TEAMS}>
|
||||
<FormattedMessage id="settings_page.all_teams" />
|
||||
</Link>
|
||||
</li>
|
||||
<li className="active">
|
||||
{this.state.team.name}
|
||||
</li>
|
||||
</StyledOl>
|
||||
<TabTitle>
|
||||
<StyledH3 onClick={this.showNameModal}>
|
||||
{this.state.team.name}
|
||||
</StyledH3>
|
||||
</TabTitle>
|
||||
<Row>
|
||||
<Col xs={6} sm={3}>
|
||||
<BadgeWrapper>
|
||||
<Glyphicon glyph="calendar" />
|
||||
</BadgeWrapper>
|
||||
<StyledWell>
|
||||
<FormattedHTMLMessage
|
||||
id="settings_page.single_team.created_on"
|
||||
values={{
|
||||
created_at: moment(this.state.team.created_at).format(
|
||||
"DD.MM.YYYY"
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</StyledWell>
|
||||
</Col>
|
||||
<Col xs={10} sm={5}>
|
||||
<BadgeWrapper>
|
||||
<Glyphicon glyph="user" />
|
||||
</BadgeWrapper>
|
||||
<StyledWell>
|
||||
<FormattedHTMLMessage
|
||||
id="settings_page.single_team.created_by"
|
||||
values={{ created_by: this.state.team.created_by }}
|
||||
/>
|
||||
</StyledWell>
|
||||
</Col>
|
||||
<Col xs={8} sm={4}>
|
||||
<BadgeWrapper>
|
||||
<Glyphicon glyph="hdd" />
|
||||
</BadgeWrapper>
|
||||
<StyledWell>
|
||||
<FormattedHTMLMessage
|
||||
id="settings_page.single_team.space_usage"
|
||||
values={{
|
||||
space_usage: prettysize(this.state.team.space_taken)
|
||||
}}
|
||||
/>
|
||||
</StyledWell>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col sm={12} onClick={this.showDescriptionModal}>
|
||||
<BadgeWrapper>
|
||||
<Glyphicon glyph="info-sign" />
|
||||
</BadgeWrapper>
|
||||
<StyledDescriptionWell>
|
||||
<span>
|
||||
{this.renderDescription()}
|
||||
</span>
|
||||
</StyledDescriptionWell>
|
||||
</Col>
|
||||
</Row>
|
||||
<TeamsMembers
|
||||
members={this.state.users}
|
||||
updateUsersCallback={this.updateUsersCallback}
|
||||
team={this.state.team}
|
||||
/>
|
||||
<UpdateTeamDescriptionModal
|
||||
showModal={this.state.showDescriptionModal}
|
||||
hideModal={this.hideDescriptionModalCallback}
|
||||
team={this.state.team}
|
||||
updateTeamCallback={this.updateTeamCallback}
|
||||
/>
|
||||
{this.renderEditNameModel()}
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsTeamPageContainer.PropTypes = {
|
||||
match: ReactRouterPropTypes.match.isRequired
|
||||
};
|
||||
|
||||
export default SettingsTeamPageContainer;
|
|
@ -1,2 +0,0 @@
|
|||
|
||||
import { LEAVE_TEAM_PATH } from '../../../../../app/routes'
|
|
@ -0,0 +1,99 @@
|
|||
import React, { Component } from "react";
|
||||
import { bool, number, string, func, shape } from "prop-types";
|
||||
import { Modal, Button, Alert, Glyphicon } from "react-bootstrap";
|
||||
import { FormattedMessage, FormattedHTMLMessage } from "react-intl";
|
||||
|
||||
import axios from "../../../../../app/axios";
|
||||
|
||||
import { REMOVE_USER_FROM_TEAM_PATH } from "../../../../../app/routes";
|
||||
|
||||
class RemoveUserModal extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onCloseModal = this.onCloseModal.bind(this);
|
||||
this.removeUser = this.removeUser.bind(this);
|
||||
}
|
||||
|
||||
onCloseModal() {
|
||||
this.props.hideModal();
|
||||
}
|
||||
|
||||
removeUser() {
|
||||
const { team_id, team_user_id } = this.props.userToRemove;
|
||||
axios({
|
||||
method: "DELETE",
|
||||
url: REMOVE_USER_FROM_TEAM_PATH,
|
||||
withCredentials: true,
|
||||
data: {
|
||||
team: team_id,
|
||||
user_team: team_user_id
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
this.props.updateUsersCallback(response.data.team_users);
|
||||
this.props.hideModal();
|
||||
})
|
||||
.catch(error => console.log(error));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { teamName, userName } = this.props.userToRemove;
|
||||
return (
|
||||
<Modal show={this.props.showModal} onHide={this.onCloseModal}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
<FormattedMessage
|
||||
id="settings_page.remove_user_modal.title"
|
||||
values={{ user: userName, team: teamName }}
|
||||
/>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="settings_page.remove_user_modal.subtitle"
|
||||
values={{ user: userName, team: teamName }}
|
||||
/>
|
||||
</p>
|
||||
<Alert bsStyle="danger">
|
||||
<Glyphicon glyph="exclamation-sign" />
|
||||
<FormattedMessage id="settings_page.remove_user_modal.warnings" />
|
||||
<ul>
|
||||
<li>
|
||||
<FormattedMessage id="settings_page.remove_user_modal.warning_message_one" />
|
||||
</li>
|
||||
<li>
|
||||
<FormattedHTMLMessage id="settings_page.remove_user_modal.warning_message_two" />
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage id="settings_page.remove_user_modal.warning_message_three" />
|
||||
</li>
|
||||
</ul>
|
||||
</Alert>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={this.onCloseModal}>
|
||||
<FormattedMessage id="general.close" />
|
||||
</Button>
|
||||
<Button bsStyle="success" onClick={this.removeUser}>
|
||||
<FormattedMessage id="settings_page.remove_user_modal.remove_user" />
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveUserModal.propTypes = {
|
||||
showModal: bool.isRequired,
|
||||
hideModal: func.isRequired,
|
||||
userToRemove: shape({
|
||||
userName: string.isRequired,
|
||||
team_user_id: number.isRequired,
|
||||
teamName: string.isRequired,
|
||||
team_id: number.isRequired
|
||||
}).isRequired,
|
||||
updateUsersCallback: func.isRequired
|
||||
};
|
||||
|
||||
export default RemoveUserModal;
|
|
@ -0,0 +1,212 @@
|
|||
import React, { Component } from "react";
|
||||
import PropTypes, { number, func, string, bool } from "prop-types";
|
||||
import {
|
||||
Panel,
|
||||
Button,
|
||||
Glyphicon,
|
||||
DropdownButton,
|
||||
MenuItem
|
||||
} from "react-bootstrap";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import axios from "../../../../../app/axios";
|
||||
|
||||
import RemoveUserModal from "./RemoveUserModal";
|
||||
import DataTable from "../../../../../shared/data_table";
|
||||
import { UPDATE_USER_TEAM_ROLE_PATH } from "../../../../../app/routes";
|
||||
|
||||
const initalUserToRemove = {
|
||||
userName: "",
|
||||
team_user_id: 0,
|
||||
teamName: "",
|
||||
team_id: 0
|
||||
};
|
||||
class TeamsMembers extends Component {
|
||||
constructor(params) {
|
||||
super(params);
|
||||
this.state = { showModal: false, userToRemove: initalUserToRemove };
|
||||
this.memberAction = this.memberAction.bind(this);
|
||||
this.hideModal = this.hideModal.bind(this);
|
||||
}
|
||||
|
||||
currentRole(memberRole, role) {
|
||||
return memberRole === role ? <Glyphicon glyph="ok" /> : " ";
|
||||
}
|
||||
|
||||
updateRole(userTeamId, role) {
|
||||
axios
|
||||
.put(UPDATE_USER_TEAM_ROLE_PATH, {
|
||||
team: this.props.team.id,
|
||||
user_team: userTeamId,
|
||||
role
|
||||
})
|
||||
.then(response => {
|
||||
this.props.updateUsersCallback(response.data.team_users);
|
||||
})
|
||||
.catch(error => console.log(error));
|
||||
}
|
||||
|
||||
hideModal() {
|
||||
this.setState({ showModal: false, userToRemove: initalUserToRemove });
|
||||
}
|
||||
|
||||
userToRemove(userToRemove) {
|
||||
this.setState({ showModal: true, userToRemove });
|
||||
}
|
||||
|
||||
memberAction(data, row) {
|
||||
return (
|
||||
<DropdownButton
|
||||
bsStyle="default"
|
||||
disabled={data.disable}
|
||||
title={
|
||||
<span>
|
||||
<Glyphicon glyph="cog" />
|
||||
</span>
|
||||
}
|
||||
id="actions-dropdown"
|
||||
>
|
||||
<MenuItem header>
|
||||
<FormattedMessage id="settings_page.single_team.actions.user_role" />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onSelect={() => {
|
||||
// 0 => Guest
|
||||
this.updateRole(data.team_user_id, 0);
|
||||
}}
|
||||
>
|
||||
{this.currentRole(data.current_role, "Guest")}
|
||||
<FormattedMessage id="settings_page.single_team.actions.guest" />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onSelect={() => {
|
||||
// 1 => Normal user
|
||||
this.updateRole(data.team_user_id, 1);
|
||||
}}
|
||||
>
|
||||
{this.currentRole(data.current_role, "Normal user")}
|
||||
<FormattedMessage id="settings_page.single_team.actions.normal_user" />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onSelect={() => {
|
||||
// 2 => Administrator
|
||||
this.updateRole(data.team_user_id, 2);
|
||||
}}
|
||||
>
|
||||
{this.currentRole(data.current_role, "Administrator")}
|
||||
<FormattedMessage id="settings_page.single_team.actions.administrator" />
|
||||
</MenuItem>
|
||||
<MenuItem divider />
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
this.userToRemove({
|
||||
userName: row.name,
|
||||
team_user_id: data.team_user_id,
|
||||
teamName: this.props.team.name,
|
||||
team_id: this.props.team.id
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FormattedMessage id="settings_page.single_team.actions.remove_user" />
|
||||
</MenuItem>
|
||||
</DropdownButton>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const columns = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Name",
|
||||
isKey: false,
|
||||
textId: "name",
|
||||
position: 0,
|
||||
dataSort: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Email",
|
||||
isKey: true,
|
||||
textId: "email",
|
||||
position: 1,
|
||||
dataSort: true
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Role",
|
||||
isKey: false,
|
||||
textId: "role",
|
||||
position: 2,
|
||||
dataSort: true
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Joined on",
|
||||
isKey: false,
|
||||
textId: "created_at",
|
||||
position: 3
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Status",
|
||||
isKey: false,
|
||||
textId: "status",
|
||||
position: 3
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Actions",
|
||||
isKey: false,
|
||||
textId: "actions",
|
||||
columnClassName: "react-bootstrap-table-dropdown-fix",
|
||||
dataFormat: this.memberAction,
|
||||
position: 3
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Panel
|
||||
header={
|
||||
<FormattedMessage id="settings_page.single_team.members_panel_title" />
|
||||
}
|
||||
>
|
||||
<Button>
|
||||
<Glyphicon glyph="plus" />
|
||||
<FormattedMessage id="settings_page.single_team.add_members" />
|
||||
</Button>
|
||||
|
||||
<DataTable data={this.props.members} columns={columns} />
|
||||
<RemoveUserModal
|
||||
showModal={this.state.showModal}
|
||||
hideModal={this.hideModal}
|
||||
updateUsersCallback={this.props.updateUsersCallback}
|
||||
userToRemove={this.state.userToRemove}
|
||||
/>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TeamsMembers.propTypes = {
|
||||
updateUsersCallback: func.isRequired,
|
||||
team: PropTypes.shape({
|
||||
id: number.isRequired,
|
||||
name: string.isRequired
|
||||
}).isRequired,
|
||||
members: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: number.isRequired,
|
||||
name: string.isRequired,
|
||||
email: string.isRequired,
|
||||
role: string.isRequired,
|
||||
created_at: string.isRequired,
|
||||
status: string.isRequired,
|
||||
actions: PropTypes.shape({
|
||||
current_role: string.isRequired,
|
||||
team_user_id: number.isRequired,
|
||||
disable: bool.isRequired
|
||||
})
|
||||
}).isRequired
|
||||
).isRequired
|
||||
};
|
||||
|
||||
export default TeamsMembers;
|
|
@ -0,0 +1,130 @@
|
|||
import React, { Component } from "react";
|
||||
import PropTypes, { bool, number, string, func } from "prop-types";
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
FormGroup,
|
||||
ControlLabel,
|
||||
FormControl,
|
||||
HelpBlock
|
||||
} from "react-bootstrap";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import _ from "lodash";
|
||||
import styled from "styled-components";
|
||||
import axios from "../../../../../app/axios";
|
||||
|
||||
import { TEXT_MAX_LENGTH } from "../../../../../app/constants/numeric";
|
||||
import { TEAM_UPDATE_PATH } from "../../../../../app/routes";
|
||||
import { COLOR_APPLE_BLOSSOM } from "../../../../../app/constants/colors";
|
||||
|
||||
const StyledHelpBlock = styled(HelpBlock)`
|
||||
color: ${COLOR_APPLE_BLOSSOM}
|
||||
`;
|
||||
|
||||
class UpdateTeamDescriptionModal extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { errorMessage: "", description: "" };
|
||||
this.onCloseModal = this.onCloseModal.bind(this);
|
||||
this.updateDescription = this.updateDescription.bind(this);
|
||||
this.handleDescription = this.handleDescription.bind(this);
|
||||
this.getValidationState = this.getValidationState.bind(this);
|
||||
}
|
||||
|
||||
onCloseModal() {
|
||||
this.setState({ errorMessage: "", description: "" });
|
||||
this.props.hideModal();
|
||||
}
|
||||
|
||||
getValidationState() {
|
||||
return this.state.errorMessage.length > 0 ? "error" : null;
|
||||
}
|
||||
|
||||
handleDescription(el) {
|
||||
const { value } = el.target;
|
||||
if (value.length > TEXT_MAX_LENGTH) {
|
||||
this.setState({
|
||||
errorMessage: (
|
||||
<FormattedMessage
|
||||
id="error_messages.text_too_long"
|
||||
values={{ max_length: TEXT_MAX_LENGTH }}
|
||||
/>
|
||||
)
|
||||
});
|
||||
} else {
|
||||
this.setState({ errorMessage: "", description: value });
|
||||
}
|
||||
}
|
||||
|
||||
updateDescription() {
|
||||
axios({
|
||||
method: "post",
|
||||
url: TEAM_UPDATE_PATH,
|
||||
withCredentials: true,
|
||||
data: {
|
||||
team_id: this.props.team.id,
|
||||
team: { description: this.state.description }
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
this.props.updateTeamCallback(response.data.team);
|
||||
this.onCloseModal();
|
||||
})
|
||||
.catch(error => this.setState({ errorMessage: error.message }));
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal show={this.props.showModal} onHide={this.onCloseModal}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
<FormattedMessage id="settings_page.update_team_description_modal.title" />
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<FormGroup
|
||||
controlId="teamDescription"
|
||||
validationState={this.getValidationState()}
|
||||
>
|
||||
<ControlLabel>
|
||||
<FormattedMessage id="settings_page.update_team_description_modal.label" />
|
||||
</ControlLabel>
|
||||
<FormControl
|
||||
componentClass="textarea"
|
||||
defaultValue={this.props.team.description}
|
||||
onChange={this.handleDescription}
|
||||
/>
|
||||
<FormControl.Feedback />
|
||||
<StyledHelpBlock>
|
||||
{this.state.errorMessage}
|
||||
</StyledHelpBlock>
|
||||
</FormGroup>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={this.onCloseModal}>
|
||||
<FormattedMessage id="general.close" />
|
||||
</Button>
|
||||
<Button
|
||||
bsStyle="success"
|
||||
onClick={this.updateDescription}
|
||||
disabled={!_.isEmpty(this.state.errorMessage)}
|
||||
>
|
||||
<FormattedMessage id="general.update" />
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateTeamDescriptionModal.propTypes = {
|
||||
showModal: bool.isRequired,
|
||||
hideModal: func.isRequired,
|
||||
team: PropTypes.shape({
|
||||
id: number.isRequired,
|
||||
description: string
|
||||
}).isRequired,
|
||||
updateTeamCallback: func.isRequired
|
||||
};
|
||||
|
||||
export default UpdateTeamDescriptionModal;
|
|
@ -0,0 +1,130 @@
|
|||
import React, { Component } from "react";
|
||||
import PropTypes, { bool, number, string, func } from "prop-types";
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
FormGroup,
|
||||
ControlLabel,
|
||||
FormControl,
|
||||
HelpBlock
|
||||
} from "react-bootstrap";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import _ from "lodash";
|
||||
import styled from "styled-components";
|
||||
import axios from "../../../../../app/axios";
|
||||
|
||||
import { NAME_MAX_LENGTH } from "../../../../../app/constants/numeric";
|
||||
import { TEAM_UPDATE_PATH } from "../../../../../app/routes";
|
||||
import { COLOR_APPLE_BLOSSOM } from "../../../../../app/constants/colors";
|
||||
|
||||
const StyledHelpBlock = styled(HelpBlock)`
|
||||
color: ${COLOR_APPLE_BLOSSOM}
|
||||
`;
|
||||
|
||||
class UpdateTeamNameModal extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { errorMessage: "", name: props.team.name };
|
||||
this.onCloseModal = this.onCloseModal.bind(this);
|
||||
this.updateName = this.updateName.bind(this);
|
||||
this.handleName = this.handleName.bind(this);
|
||||
this.getValidationState = this.getValidationState.bind(this);
|
||||
}
|
||||
|
||||
onCloseModal() {
|
||||
this.setState({ errorMessage: "", name: "" });
|
||||
this.props.hideModal();
|
||||
}
|
||||
|
||||
getValidationState() {
|
||||
return this.state.errorMessage.length > 0 ? "error" : null;
|
||||
}
|
||||
|
||||
handleName(el) {
|
||||
const { value } = el.target;
|
||||
if (value.length > NAME_MAX_LENGTH) {
|
||||
this.setState({
|
||||
errorMessage: (
|
||||
<FormattedMessage
|
||||
id="error_messages.text_too_long"
|
||||
values={{ max_length: NAME_MAX_LENGTH }}
|
||||
/>
|
||||
)
|
||||
});
|
||||
} else {
|
||||
this.setState({ errorMessage: "", name: value });
|
||||
}
|
||||
}
|
||||
|
||||
updateName() {
|
||||
axios({
|
||||
method: "post",
|
||||
url: TEAM_UPDATE_PATH,
|
||||
withCredentials: true,
|
||||
data: {
|
||||
team_id: this.props.team.id,
|
||||
team: { name: this.state.name }
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
this.props.updateTeamCallback(response.data.team);
|
||||
this.onCloseModal();
|
||||
})
|
||||
.catch(error => this.setState({ errorMessage: error.message }));
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal show={this.props.showModal} onHide={this.onCloseModal}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
<FormattedMessage id="settings_page.update_team_name_modal.title" />
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<FormGroup
|
||||
controlId="teamName"
|
||||
validationState={this.getValidationState()}
|
||||
>
|
||||
<ControlLabel>
|
||||
<FormattedMessage id="settings_page.update_team_name_modal.label" />
|
||||
</ControlLabel>
|
||||
<FormControl
|
||||
type="text"
|
||||
onChange={this.handleName}
|
||||
value={this.state.name}
|
||||
/>
|
||||
<FormControl.Feedback />
|
||||
<StyledHelpBlock>
|
||||
{this.state.errorMessage}
|
||||
</StyledHelpBlock>
|
||||
</FormGroup>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={this.onCloseModal}>
|
||||
<FormattedMessage id="general.close" />
|
||||
</Button>
|
||||
<Button
|
||||
bsStyle="success"
|
||||
onClick={this.updateName}
|
||||
disabled={!_.isEmpty(this.state.errorMessage)}
|
||||
>
|
||||
<FormattedMessage id="general.update" />
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateTeamNameModal.propTypes = {
|
||||
showModal: bool.isRequired,
|
||||
hideModal: func.isRequired,
|
||||
team: PropTypes.shape({
|
||||
id: number.isRequired,
|
||||
name: string
|
||||
}).isRequired,
|
||||
updateTeamCallback: func.isRequired
|
||||
};
|
||||
|
||||
export default UpdateTeamNameModal;
|
|
@ -2,10 +2,11 @@ import React, { Component } from "react";
|
|||
import PropTypes, { func, number, string, bool } from "prop-types";
|
||||
import { connect } from "react-redux";
|
||||
import { Button } from "react-bootstrap";
|
||||
import _ from "lodash";
|
||||
import { Link } from "react-router-dom";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { leaveTeamModalShow } from "../../../../../shared/actions/LeaveTeamActions";
|
||||
import { leaveTeamModalShow } from "../../../../../shared/actions/TeamsActions";
|
||||
import DataTable from "../../../../../shared/data_table";
|
||||
import { SETTINGS_TEAMS_ROUTE } from "../../../../../app/dom_routes";
|
||||
|
||||
class TeamsDataTable extends Component {
|
||||
constructor(props) {
|
||||
|
@ -13,18 +14,17 @@ class TeamsDataTable extends Component {
|
|||
|
||||
this.leaveTeamModal = this.leaveTeamModal.bind(this);
|
||||
this.leaveTeamButton = this.leaveTeamButton.bind(this);
|
||||
this.linkToTeam = this.linkToTeam.bind(this);
|
||||
}
|
||||
|
||||
leaveTeamModal(e, id) {
|
||||
const team = _.find(this.props.teams, el => el.id === id);
|
||||
this.props.leaveTeamModalShow(true, id, team.name);
|
||||
leaveTeamModal(e, team) {
|
||||
this.props.leaveTeamModalShow(true, team);
|
||||
}
|
||||
|
||||
leaveTeamButton(id) {
|
||||
const team = _.find(this.props.teams, el => el.id === id);
|
||||
leaveTeamButton(id, team) {
|
||||
if (team.can_be_leaved) {
|
||||
return (
|
||||
<Button onClick={e => this.leaveTeamModal(e, id)}>
|
||||
<Button onClick={e => this.leaveTeamModal(e, team)}>
|
||||
<FormattedMessage id="settings_page.leave_team" />
|
||||
</Button>
|
||||
);
|
||||
|
@ -36,6 +36,14 @@ class TeamsDataTable extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
linkToTeam(name, row) {
|
||||
return (
|
||||
<Link to={`${SETTINGS_TEAMS_ROUTE}/${row.id}`}>
|
||||
{name}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const options = {
|
||||
defaultSortName: "name",
|
||||
|
@ -50,6 +58,7 @@ class TeamsDataTable extends Component {
|
|||
name: "Name",
|
||||
isKey: false,
|
||||
textId: "name",
|
||||
dataFormat: this.linkToTeam,
|
||||
position: 0,
|
||||
dataSort: true
|
||||
},
|
|
@ -21,3 +21,11 @@ body {
|
|||
background-color: $primary-hover-color;
|
||||
}
|
||||
}
|
||||
|
||||
// // fixes issue with dropdown in datatable
|
||||
.react-bootstrap-table-dropdown-fix {
|
||||
overflow: inherit !important;
|
||||
& .open > .dropdown-menu {
|
||||
position: relative !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -215,7 +215,8 @@ class User < ApplicationRecord
|
|||
'CASE WHEN teams.id=? THEN true ELSE false END AS current_team, ' \
|
||||
'CASE WHEN (SELECT COUNT(*) FROM user_teams WHERE ' \
|
||||
'user_teams.team_id=teams.id AND role=2) >= 2 THEN true ELSE false ' \
|
||||
'END AS can_be_leaved FROM teams INNER JOIN user_teams ON ' \
|
||||
'END AS can_be_leaved, user_teams.id AS user_team_id ' \
|
||||
'FROM teams INNER JOIN user_teams ON ' \
|
||||
'teams.id=user_teams.team_id WHERE user_teams.user_id=?',
|
||||
self.current_team_id, self.id]
|
||||
)
|
||||
|
|
38
app/services/client_api/teams_service.rb
Normal file
38
app/services/client_api/teams_service.rb
Normal file
|
@ -0,0 +1,38 @@
|
|||
module ClientApi
|
||||
class TeamsService
|
||||
def initialize(arg)
|
||||
team_id = arg.fetch(:team_id) { raise ClientApi::CustomTeamError }
|
||||
@params = arg.fetch(:params) { false }
|
||||
@team = Team.find_by_id(team_id)
|
||||
@user = arg.fetch(:current_user) { raise ClientApi::CustomTeamError }
|
||||
raise ClientApi::CustomTeamError unless @user.teams.include? @team
|
||||
end
|
||||
|
||||
def change_current_team!
|
||||
@user.update_attribute(:current_team_id, @team.id)
|
||||
end
|
||||
|
||||
def team_page_details_data
|
||||
team_users = UserTeam.includes(:user)
|
||||
.references(:user)
|
||||
.where(team: @team)
|
||||
.distinct
|
||||
{ team: @team, team_users: team_users }
|
||||
end
|
||||
|
||||
def single_team_details_data
|
||||
{ team: @team }
|
||||
end
|
||||
|
||||
def update_team!
|
||||
raise ClientApi::CustomTeamError unless @params
|
||||
return if @team.update_attributes(@params)
|
||||
raise ClientApi::CustomTeamError, @team.errors.full_messages
|
||||
end
|
||||
|
||||
def teams_data
|
||||
{ teams: @user.teams_data }
|
||||
end
|
||||
end
|
||||
CustomTeamError = Class.new(StandardError)
|
||||
end
|
87
app/services/client_api/user_team_service.rb
Normal file
87
app/services/client_api/user_team_service.rb
Normal file
|
@ -0,0 +1,87 @@
|
|||
module ClientApi
|
||||
class UserTeamService
|
||||
include NotificationsHelper
|
||||
include InputSanitizeHelper
|
||||
|
||||
def initialize(args)
|
||||
parsed_args = validate_params(args)
|
||||
@team = Team.find_by_id(parsed_args.fetch(:team_id))
|
||||
@user = parsed_args.fetch(:user)
|
||||
@user_team = UserTeam.find_by_id(parsed_args.fetch(:user_team_id).to_i)
|
||||
@role = args.fetch(:role) { false }
|
||||
end
|
||||
|
||||
def destroy_user_team_and_assign_new_team_owner!
|
||||
raise ClientApi::CustomUserTeamError if user_cant_leave?
|
||||
new_owner = @team.user_teams
|
||||
.where(role: 2)
|
||||
.where.not(id: @user_team.id)
|
||||
.first.user
|
||||
new_owner ||= @user
|
||||
reset_user_current_team(@user_team)
|
||||
@user_team.destroy(new_owner)
|
||||
generate_new_notification
|
||||
end
|
||||
|
||||
def update_role!
|
||||
raise ClientApi::CustomUserTeamError if user_cant_leave?
|
||||
unless @role
|
||||
raise ClientApi::CustomUserTeamError,
|
||||
I18n.t('client_api.generic_error_message')
|
||||
end
|
||||
return if @user_team.update_attribute(:role, @role)
|
||||
raise ClientApi::CustomUserTeamError, @user_team.errors.full_messages
|
||||
end
|
||||
|
||||
def team_users_data
|
||||
team_users = UserTeam.includes(:user)
|
||||
.references(:user)
|
||||
.where(team: @team)
|
||||
.distinct
|
||||
{ team_users: team_users }
|
||||
end
|
||||
|
||||
def teams_data
|
||||
{
|
||||
teams: @user.teams_data,
|
||||
flash_message: I18n.t('client_api.user_teams.leave_flash',
|
||||
team: @team.name)
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reset_user_current_team(user_team)
|
||||
ids = user_team.user.teams_ids
|
||||
ids -= [user_team.team.id]
|
||||
user_team.user.current_team_id = ids.first
|
||||
user_team.user.save
|
||||
end
|
||||
|
||||
def user_cant_leave?
|
||||
@user.teams.include?(@team) &&
|
||||
@user_team.admin? &&
|
||||
@team.user_teams.where(role: 2).count <= 1
|
||||
end
|
||||
|
||||
def generate_new_notification
|
||||
user = @user_team.user
|
||||
generate_notification(user,
|
||||
user,
|
||||
@user_team.team,
|
||||
false,
|
||||
false)
|
||||
end
|
||||
|
||||
def validate_params(args)
|
||||
keys = %i(team_id user_team_id user)
|
||||
raise ClientApi::CustomUserTeamError unless keys.all? { |s| args.key? s }
|
||||
raise ClientApi::CustomUserTeamError if args.values.any? &:nil?
|
||||
team_id = args.fetch(:team_id)
|
||||
user_team_id = args.fetch(:user_team_id)
|
||||
user = args.fetch(:user)
|
||||
{ user: user, user_team_id: user_team_id, team_id: team_id }
|
||||
end
|
||||
end
|
||||
CustomUserTeamError = Class.new(StandardError)
|
||||
end
|
23
app/views/client_api/teams/details.json.jbuilder
Normal file
23
app/views/client_api/teams/details.json.jbuilder
Normal file
|
@ -0,0 +1,23 @@
|
|||
json.team_details do
|
||||
json.team do
|
||||
json.id team.id
|
||||
json.name team.name
|
||||
json.created_at team.created_at
|
||||
json.created_by "#{team.created_by.full_name} (#{team.created_by.email})"
|
||||
json.space_taken team.space_taken
|
||||
json.description team.description
|
||||
end
|
||||
json.users team_users do |team_user|
|
||||
json.id team_user.user.id
|
||||
json.name team_user.user.full_name
|
||||
json.email team_user.user.email
|
||||
json.role team_user.role_str
|
||||
json.created_at I18n.l(team_user.created_at, format: :full_date)
|
||||
json.status team_user.user.active_status_str
|
||||
json.actions do
|
||||
json.current_role team_user.role_str
|
||||
json.team_user_id team_user.id
|
||||
json.disable team_user.user == current_user
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,5 +6,6 @@ json.teams do
|
|||
json.role retrive_role_name(team.fetch('role') { nil })
|
||||
json.current_team team.fetch('current_team')
|
||||
json.can_be_leaved team.fetch('can_be_leaved')
|
||||
json.user_team_id team.fetch('user_team_id')
|
||||
end
|
||||
end
|
||||
|
|
13
app/views/client_api/teams/team_users.json.jbuilder
Normal file
13
app/views/client_api/teams/team_users.json.jbuilder
Normal file
|
@ -0,0 +1,13 @@
|
|||
json.team_users team_users do |team_user|
|
||||
json.id team_user.user.id
|
||||
json.name team_user.user.full_name
|
||||
json.email team_user.user.email
|
||||
json.role team_user.role_str
|
||||
json.created_at I18n.l(team_user.created_at, format: :full_date)
|
||||
json.status team_user.user.active_status_str
|
||||
json.actions do
|
||||
json.current_role team_user.role_str
|
||||
json.team_user_id team_user.id
|
||||
json.disable team_user.user == current_user
|
||||
end
|
||||
end
|
8
app/views/client_api/teams/update_details.json.jbuilder
Normal file
8
app/views/client_api/teams/update_details.json.jbuilder
Normal file
|
@ -0,0 +1,8 @@
|
|||
json.team do
|
||||
json.id team.id
|
||||
json.name team.name
|
||||
json.created_at team.created_at
|
||||
json.created_by "#{team.created_by.full_name} (#{team.created_by.email})"
|
||||
json.space_taken team.space_taken
|
||||
json.description team.description
|
||||
end
|
|
@ -1,5 +1,11 @@
|
|||
Rails.application.configure do
|
||||
|
||||
# Settings specified here will take precedence over those in config/application.rb.
|
||||
config.after_initialize do
|
||||
Bullet.enable = true
|
||||
Bullet.bullet_logger = true
|
||||
Bullet.raise = true # raise an error if n+1 query occurs
|
||||
end
|
||||
|
||||
# Configure public file server for tests with Cache-Control for performance.
|
||||
config.public_file_server.enabled = true
|
||||
|
|
|
@ -1821,6 +1821,7 @@ en:
|
|||
by: 'by'
|
||||
|
||||
client_api:
|
||||
generic_error_message: "Something went wrong! Please try again later."
|
||||
user_teams:
|
||||
leave_team_error: "An error occured."
|
||||
leave_flash: "Successfuly left team %{team}."
|
||||
|
|
|
@ -17,9 +17,12 @@ Rails.application.routes.draw do
|
|||
get '/activities', to: 'activities#index'
|
||||
|
||||
# teams
|
||||
get '/teams', to: 'teams#index'
|
||||
post '/change_team', to: 'teams#change_team'
|
||||
|
||||
get '/teams', to: 'teams/teams#index'
|
||||
namespace :teams do
|
||||
get '/:team_id/details', to: 'teams#details'
|
||||
post '/change_team', to: 'teams#change_team'
|
||||
post '/update', to: 'teams#update'
|
||||
end
|
||||
# notifications
|
||||
get '/recent_notifications', to: 'notifications#recent_notifications'
|
||||
|
||||
|
@ -27,6 +30,9 @@ Rails.application.routes.draw do
|
|||
get '/current_user_info', to: 'users/users#current_user_info'
|
||||
|
||||
namespace :users do
|
||||
delete '/remove_user', to: 'user_teams#remove_user'
|
||||
delete '/leave_team', to: 'user_teams#leave_team'
|
||||
put '/update_role', to: 'user_teams#update_role'
|
||||
get '/profile_info', to: 'users#profile_info'
|
||||
get '/statistics_info', to: 'users#statistics_info'
|
||||
get '/preferences_info', to: 'users#preferences_info'
|
||||
|
|
|
@ -59,11 +59,14 @@
|
|||
"postcss-loader": "^2.0.6",
|
||||
"postcss-smart-import": "^0.7.5",
|
||||
"precss": "^2.0.0",
|
||||
"prettysize": "^0.1.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"rails-erb-loader": "^5.0.2",
|
||||
"react": "^15.6.1",
|
||||
"react-bootstrap": "^0.31.1",
|
||||
"react-bootstrap-table": "^4.0.0",
|
||||
"react-bootstrap-timezone-picker": "^1.0.11",
|
||||
"react-data-grid": "^2.0.2",
|
||||
"react-dom": "^15.6.1",
|
||||
"react-intl": "^2.3.0",
|
||||
"react-intl-redux": "^0.6.0",
|
||||
|
@ -71,6 +74,7 @@
|
|||
"react-redux": "^5.0.5",
|
||||
"react-router-bootstrap": "^0.24.2",
|
||||
"react-router-dom": "^4.1.2",
|
||||
"react-router-prop-types": "^0.0.1",
|
||||
"react-timezone": "^0.2.0",
|
||||
"redux": "^3.7.2",
|
||||
"redux-thunk": "^2.2.0",
|
||||
|
@ -80,8 +84,6 @@
|
|||
"styled-components": "^2.1.1",
|
||||
"webpack": "^3.2.0",
|
||||
"webpack-manifest-plugin": "^1.1.2",
|
||||
"webpack-merge": "^4.1.0",
|
||||
"react-bootstrap-table": "^4.0.0",
|
||||
"react-data-grid": "^2.0.2"
|
||||
"webpack-merge": "^4.1.0"
|
||||
}
|
||||
}
|
||||
|
|
90
spec/controllers/client_api/teams/teams_controller_spec.rb
Normal file
90
spec/controllers/client_api/teams/teams_controller_spec.rb
Normal file
|
@ -0,0 +1,90 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe ClientApi::Teams::TeamsController, type: :controller do
|
||||
login_user
|
||||
|
||||
before do
|
||||
@user_one = User.first
|
||||
@user_two = FactoryGirl.create :user, email: 'sec_user@asdf.com'
|
||||
@team_one = FactoryGirl.create :team
|
||||
@team_two = FactoryGirl.create :team, name: 'Team two'
|
||||
FactoryGirl.create :user_team, team: @team_one, user: @user_one, role: 2
|
||||
end
|
||||
|
||||
describe 'GET #index' do
|
||||
it 'should return HTTP success response' do
|
||||
get :index, format: :json
|
||||
expect(response).to be_success
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #change_team' do
|
||||
it 'should return HTTP success response' do
|
||||
FactoryGirl.create :user_team, team: @team_two, user: @user_one, role: 2
|
||||
@user_one.update_attribute(:current_team_id, @team_one.id)
|
||||
post :change_team, params: { team_id: @team_two.id }, as: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'should return HTTP unprocessable_entity response if user not in team' do
|
||||
@user_one.update_attribute(:current_team_id, @team_one.id)
|
||||
post :change_team, params: { team_id: @team_two.id }, as: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it 'should return HTTP success response if same team as current' do
|
||||
@user_one.update_attribute(:current_team_id, @team_one.id)
|
||||
post :change_team, params: { team_id: @team_one.id }, as: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #details' do
|
||||
it 'should return HTTP success response' do
|
||||
FactoryGirl.create :user_team, team: @team_two, user: @user_one, role: 2
|
||||
@user_one.update_attribute(:current_team_id, @team_one.id)
|
||||
get :details, params: { team_id: @team_two.id }, as: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'should return HTTP unprocessable_entity response if user not in team' do
|
||||
@user_one.update_attribute(:current_team_id, @team_one.id)
|
||||
get :details, params: { team_id: @team_two.id }, as: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it 'should return HTTP unprocessable_entity response if team_id not valid' do
|
||||
get :details, params: { team_id: 'banana' }, as: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #update' do
|
||||
let(:user_team) do
|
||||
create :user_team, team: @team_two, user: @user_one, role: 2
|
||||
end
|
||||
|
||||
it 'should return HTTP success response' do
|
||||
user_team
|
||||
post :update,
|
||||
params: { team_id: @team_two.id,
|
||||
team: { description: 'My description' } },
|
||||
as: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'should return HTTP unprocessable_entity response iput not valid' do
|
||||
user_team
|
||||
post :update,
|
||||
params: {
|
||||
team_id: @team_two.id,
|
||||
team: {
|
||||
description: "super long: #{'a' * Constants::TEXT_MAX_LENGTH}"
|
||||
}
|
||||
},
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,32 +1,82 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe ClientApi::Users::UserTeamsController, type: :controller do
|
||||
login_user
|
||||
let(:user_one) { User.first }
|
||||
let(:user_two) { create :user, email: Faker::Internet.email }
|
||||
let(:team) { create :team }
|
||||
let(:user_team) { create :user_team, team: team, user: user_one }
|
||||
|
||||
describe 'DELETE #leave_team' do
|
||||
login_user
|
||||
before do
|
||||
@user_one = User.first
|
||||
@user_two = FactoryGirl.create(:user, email: 'sec_user@asdf.com')
|
||||
@team = FactoryGirl.create :team
|
||||
FactoryGirl.create :user_team, team: @team, user: @user_one, role: 2
|
||||
end
|
||||
|
||||
it 'Returns HTTP success if user can leave the team' do
|
||||
FactoryGirl.create :user_team, team: @team, user: @user_two, role: 2
|
||||
delete :leave_team, params: { team: @team.id }, format: :json
|
||||
it 'should return HTTP success if user can leave the team' do
|
||||
create :user_team, team: team, user: user_two
|
||||
delete :leave_team,
|
||||
params: { team: team.id, user_team: user_team.id },
|
||||
format: :json
|
||||
expect(response).to be_success
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'Returns HTTP unprocessable_entity if user can\'t leave the team' do
|
||||
delete :leave_team, params: { team: @team.id }, format: :json
|
||||
it 'should return HTTP unprocessable_entity if user can\'t ' \
|
||||
'leave the team' do
|
||||
delete :leave_team,
|
||||
params: { team: team.id, user_team: user_team.id },
|
||||
format: :json
|
||||
expect(response).to_not be_success
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it 'Returns HTTP unprocessable_entity if no params given' do
|
||||
it 'should return HTTP unprocessable_entity if no params given' do
|
||||
delete :leave_team, format: :json
|
||||
expect(response).to_not be_success
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #update_role' do
|
||||
it 'should return HTTP success if user can leave the team' do
|
||||
user_team_two = create :user_team, team: team, user: user_two, role: 2
|
||||
post :update_role,
|
||||
params: { team: team.id,
|
||||
user_team: user_team_two.id,
|
||||
role: 'normal_user' },
|
||||
format: :json
|
||||
expect(response).to be_success
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'should return HTTP unprocessable_entity if user can\'t ' \
|
||||
'leave the team' do
|
||||
post :update_role,
|
||||
params: { team: team.id,
|
||||
user_team: user_team.id,
|
||||
role: 'normal_user' },
|
||||
format: :json
|
||||
expect(response).to_not be_success
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE #remove_user' do
|
||||
it 'should return HTTP success if user can be removed' do
|
||||
user_team
|
||||
user_team_two = create :user_team, team: team, user: user_two
|
||||
post :remove_user,
|
||||
params: { team: team.id, user_team: user_team_two.id },
|
||||
format: :json
|
||||
expect(response).to be_success
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'should return HTTP unprocessable_entity if user can\'t ' \
|
||||
'be removed' do
|
||||
post :remove_user,
|
||||
params: { team: team.id,
|
||||
user_team: user_team.id,
|
||||
role: 'normal_user' },
|
||||
format: :json
|
||||
expect(response).to_not be_success
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -160,14 +160,15 @@ describe User, type: :model do
|
|||
|
||||
it 'in a specific format: {id: .., name: .., members: .., role: ' \
|
||||
'.., current_team: .., can_be_leaved: ..}' do
|
||||
create :user_team, team: team, user: user_one
|
||||
user_team = create :user_team, team: team, user: user_one
|
||||
expected_result = {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
members: 1,
|
||||
role: 2,
|
||||
current_team: true,
|
||||
can_be_leaved: false
|
||||
can_be_leaved: false,
|
||||
user_team_id: user_team.id
|
||||
}
|
||||
|
||||
user_one.teams_data.first.each do |k, v|
|
||||
|
@ -176,7 +177,7 @@ describe User, type: :model do
|
|||
end
|
||||
|
||||
it 'should return correct number of team members' do
|
||||
create :user_team, team: team, user: user_one
|
||||
user_team = create :user_team, team: team, user: user_one
|
||||
create :user_team, team: team, user: user_two
|
||||
expected_result = {
|
||||
id: team.id,
|
||||
|
@ -184,7 +185,8 @@ describe User, type: :model do
|
|||
members: 2,
|
||||
role: 2,
|
||||
current_team: true,
|
||||
can_be_leaved: true
|
||||
can_be_leaved: true,
|
||||
user_team_id: user_team.id
|
||||
}
|
||||
|
||||
user_one.teams_data.first.each do |k, v|
|
||||
|
|
109
spec/services/client_api/teams_service_spec.rb
Normal file
109
spec/services/client_api/teams_service_spec.rb
Normal file
|
@ -0,0 +1,109 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe ClientApi::TeamsService do
|
||||
let(:team_one) { create :team }
|
||||
let(:user_one) { create :user }
|
||||
|
||||
it 'should raise an ClientApi::CustomTeamError if user is not assigned' do
|
||||
expect {
|
||||
ClientApi::TeamsService.new(team_id: team_one.id)
|
||||
}.to raise_error(ClientApi::CustomTeamError)
|
||||
end
|
||||
|
||||
it 'should raise an ClientApi::CustomTeamError if team is not assigned' do
|
||||
expect {
|
||||
ClientApi::TeamsService.new(current_user: user_one)
|
||||
}.to raise_error(ClientApi::CustomTeamError)
|
||||
end
|
||||
|
||||
it 'should raise an ClientApi::CustomTeamError if team is not user team' do
|
||||
expect {
|
||||
ClientApi::TeamsService.new(current_user: user_one, team_id: team_one.id)
|
||||
}.to raise_error(ClientApi::CustomTeamError)
|
||||
end
|
||||
|
||||
describe '#change_current_team!' do
|
||||
let(:team_two) { create :team, name: 'team two' }
|
||||
let(:user_two) do
|
||||
create :user, current_team_id: team_one.id, email: 'user_two@test.com'
|
||||
end
|
||||
|
||||
it 'should change user current team' do
|
||||
create :user_team, user: user_two, team: team_two
|
||||
teams_service = ClientApi::TeamsService.new(current_user: user_two,
|
||||
team_id: team_two.id)
|
||||
teams_service.change_current_team!
|
||||
expect(user_two.current_team_id).to eq team_two.id
|
||||
end
|
||||
end
|
||||
|
||||
describe '#team_page_details_data' do
|
||||
let(:team_service) do
|
||||
ClientApi::TeamsService.new(current_user: user_one, team_id: team_one.id)
|
||||
end
|
||||
|
||||
it 'should return team page data' do
|
||||
user_team = create :user_team, user: user_one, team: team_one
|
||||
data = team_service.team_page_details_data
|
||||
expect(data.fetch(:team).name).to eq team_one.name
|
||||
expect(data.fetch(:team_users).first).to eq user_team
|
||||
end
|
||||
end
|
||||
|
||||
describe '#teams_data' do
|
||||
let(:team_service) do
|
||||
ClientApi::TeamsService.new(current_user: user_one, team_id: team_one.id)
|
||||
end
|
||||
|
||||
it 'should return user teams' do
|
||||
create :user_team, user: user_one, team: team_one
|
||||
data = team_service.teams_data.fetch(:teams)
|
||||
expect(data.first.fetch('name')).to eq team_one.name
|
||||
end
|
||||
end
|
||||
|
||||
describe '#update_team!' do
|
||||
let(:team_two) { create :team, name: 'Banana', created_by: user_one }
|
||||
|
||||
it 'should raise an error if input invalid' do
|
||||
create :user_team, user: user_one, team: team_one
|
||||
team_service = ClientApi::TeamsService.new(
|
||||
current_user: user_one,
|
||||
team_id: team_one.id,
|
||||
params: {
|
||||
description: "super long: #{'a' * Constants::TEXT_MAX_LENGTH}"
|
||||
}
|
||||
)
|
||||
expect {
|
||||
team_service.update_team!
|
||||
}.to raise_error(ClientApi::CustomTeamError)
|
||||
end
|
||||
|
||||
it 'should update the team description if the input is valid' do
|
||||
create :user_team, user: user_one, team: team_two
|
||||
desc = 'Banana Team description'
|
||||
team_service = ClientApi::TeamsService.new(
|
||||
current_user: user_one,
|
||||
team_id: team_two.id,
|
||||
params: {
|
||||
description: desc
|
||||
}
|
||||
)
|
||||
team_service.update_team!
|
||||
# load values from db
|
||||
team_two.reload
|
||||
expect(team_two.description).to eq desc
|
||||
end
|
||||
end
|
||||
|
||||
describe '#single_team_details_data' do
|
||||
let(:team_service) do
|
||||
ClientApi::TeamsService.new(current_user: user_one, team_id: team_one.id)
|
||||
end
|
||||
|
||||
it 'should return a team object' do
|
||||
create :user_team, user: user_one, team: team_one
|
||||
expect(team_service.single_team_details_data.fetch(:team)).to eq team_one
|
||||
end
|
||||
end
|
||||
end
|
137
spec/services/client_api/user_team_service_spec.rb
Normal file
137
spec/services/client_api/user_team_service_spec.rb
Normal file
|
@ -0,0 +1,137 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe ClientApi::UserTeamService do
|
||||
let(:team_one) { create :team }
|
||||
let(:user_one) { create :user, email: Faker::Internet.email }
|
||||
let(:user_two) { create :user, email: Faker::Internet.email }
|
||||
let(:user_team) { create :user_team, user: user_one, team: team_one }
|
||||
|
||||
it 'should raise ClientApi::CustomUserTeamError if user is not assigned' do
|
||||
expect {
|
||||
ClientApi::UserTeamService.new(
|
||||
team_id: team_one.id,
|
||||
user_team_id: user_team.id
|
||||
)
|
||||
}.to raise_error(ClientApi::CustomUserTeamError)
|
||||
end
|
||||
|
||||
it 'should raise ClientApi::CustomUserTeamError if team is not assigned' do
|
||||
expect {
|
||||
ClientApi::UserTeamService.new(user: user_one, user_team_id: user_team.id)
|
||||
}.to raise_error(ClientApi::CustomUserTeamError)
|
||||
end
|
||||
|
||||
it 'should raise ClientApi::CustomUserTeamError if ' \
|
||||
'user_team is not assigned' do
|
||||
expect {
|
||||
ClientApi::UserTeamService.new(user: user_one, team_id: team_one.id)
|
||||
}.to raise_error(ClientApi::CustomUserTeamError)
|
||||
end
|
||||
|
||||
describe '#destroy_user_team_and_assign_new_team_owner!' do
|
||||
it 'should raise ClientApi::CustomUserTeamError if user ' \
|
||||
'can\'t leave the team' do
|
||||
ut_service = ClientApi::UserTeamService.new(
|
||||
team_id: team_one.id,
|
||||
user_team_id: user_team.id,
|
||||
user: user_one
|
||||
)
|
||||
expect {
|
||||
ut_service.destroy_user_team_and_assign_new_team_owner!
|
||||
}.to raise_error(ClientApi::CustomUserTeamError)
|
||||
end
|
||||
|
||||
it 'should destroy the user_team relation' do
|
||||
create :user_team, team: team_one, user: user_one
|
||||
new_user_team = create :user_team, team: team_one, user: user_two
|
||||
ut_service = ClientApi::UserTeamService.new(
|
||||
team_id: team_one.id,
|
||||
user_team_id: new_user_team.id,
|
||||
user: user_one
|
||||
)
|
||||
ut_service.destroy_user_team_and_assign_new_team_owner!
|
||||
expect(team_one.users).to_not include user_two
|
||||
end
|
||||
|
||||
it 'should assign a new owner to the team' do
|
||||
user_team_one = create :user_team, team: team_one, user: user_one
|
||||
create :user_team, team: team_one, user: user_two
|
||||
ut_service = ClientApi::UserTeamService.new(
|
||||
team_id: team_one.id,
|
||||
user_team_id: user_team_one.id,
|
||||
user: user_one
|
||||
)
|
||||
ut_service.destroy_user_team_and_assign_new_team_owner!
|
||||
expect(team_one.users).to include user_two
|
||||
end
|
||||
end
|
||||
|
||||
describe '#update_role!' do
|
||||
it 'should raise ClientApi::CustomUserTeamError if no role is set' do
|
||||
create :user_team, team: team_one, user: user_one
|
||||
ut_service = ClientApi::UserTeamService.new(
|
||||
user: user_one,
|
||||
team_id: team_one.id,
|
||||
user_team_id: user_team.id
|
||||
)
|
||||
expect {
|
||||
ut_service.update_role!
|
||||
}.to raise_error(ClientApi::CustomUserTeamError)
|
||||
end
|
||||
|
||||
it 'should update user role' do
|
||||
create :user_team, team: team_one, user: user_two
|
||||
user_team = create :user_team, team: team_one, user: user_one
|
||||
ut_service = ClientApi::UserTeamService.new(
|
||||
user: user_one,
|
||||
team_id: team_one.id,
|
||||
user_team_id: user_team.id,
|
||||
role: 1
|
||||
)
|
||||
ut_service.update_role!
|
||||
user_team.reload
|
||||
expect(user_team.role).to eq 'normal_user'
|
||||
end
|
||||
|
||||
it 'should raise ClientApi::CustomUserTeamError if is the last ' \
|
||||
'admin on the team' do
|
||||
user_team = create :user_team, team: team_one, user: user_one
|
||||
ut_service = ClientApi::UserTeamService.new(
|
||||
user: user_one,
|
||||
team_id: team_one.id,
|
||||
user_team_id: user_team.id,
|
||||
role: 1
|
||||
)
|
||||
expect {
|
||||
ut_service.update_role!
|
||||
}.to raise_error(ClientApi::CustomUserTeamError)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#team_users_data' do
|
||||
it 'should return a hash of team members' do
|
||||
user_team = create :user_team, team: team_one, user: user_one
|
||||
ut_service = ClientApi::UserTeamService.new(
|
||||
user: user_one,
|
||||
team_id: team_one.id,
|
||||
user_team_id: user_team.id,
|
||||
role: 1
|
||||
)
|
||||
expect(ut_service.team_users_data.fetch(:team_users)).to include user_team
|
||||
end
|
||||
end
|
||||
|
||||
describe '#teams_data' do
|
||||
it 'should return a list of teams where user is a member' do
|
||||
user_team = create :user_team, team: team_one, user: user_one
|
||||
ut_service = ClientApi::UserTeamService.new(
|
||||
user: user_one,
|
||||
team_id: team_one.id,
|
||||
user_team_id: user_team.id,
|
||||
role: 1
|
||||
)
|
||||
team_id = ut_service.teams_data[:teams].first.fetch('id')
|
||||
expect(team_id).to eq team_one.id
|
||||
end
|
||||
end
|
||||
end
|
|
@ -16,6 +16,8 @@
|
|||
require 'capybara/rspec'
|
||||
require 'simplecov'
|
||||
require 'faker'
|
||||
require 'active_record'
|
||||
require 'bullet'
|
||||
|
||||
RSpec.configure do |config|
|
||||
# rspec-expectations config goes here. You can use an alternate
|
||||
|
@ -97,4 +99,15 @@ RSpec.configure do |config|
|
|||
# as the one that triggered the failure.
|
||||
Kernel.srand config.seed
|
||||
=end
|
||||
# Enable bullet gem in tests
|
||||
if Bullet.enable?
|
||||
config.before(:each) do
|
||||
Bullet.start_request
|
||||
end
|
||||
|
||||
config.after(:each) do
|
||||
Bullet.perform_out_of_channel_notifications if Bullet.notification?
|
||||
Bullet.end_request
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
20
yarn.lock
20
yarn.lock
|
@ -4516,6 +4516,10 @@ prettier@^1.5.3:
|
|||
version "1.5.3"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.5.3.tgz#59dadc683345ec6b88f88b94ed4ae7e1da394bfe"
|
||||
|
||||
prettysize@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/prettysize/-/prettysize-0.1.0.tgz#38ee534e2d298bc945fb7243203dd873cefc9679"
|
||||
|
||||
private@^0.1.6, private@^0.1.7:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1"
|
||||
|
@ -4550,7 +4554,7 @@ prop-types-extra@^1.0.1:
|
|||
dependencies:
|
||||
warning "^3.0.0"
|
||||
|
||||
prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.7, prop-types@^15.5.8:
|
||||
prop-types@15.5.10, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.7, prop-types@^15.5.8:
|
||||
version "15.5.10"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154"
|
||||
dependencies:
|
||||
|
@ -4723,10 +4727,6 @@ react-intl@^2.3.0:
|
|||
intl-relativeformat "^1.3.0"
|
||||
invariant "^2.1.1"
|
||||
|
||||
react-moment@^0.6.4:
|
||||
version "0.6.4"
|
||||
resolved "https://registry.yarnpkg.com/react-moment/-/react-moment-0.6.4.tgz#5e531d47ad7b0bff6f6b7175093e98659f5e667b"
|
||||
|
||||
react-modal@^1.4.0:
|
||||
version "1.9.7"
|
||||
resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-1.9.7.tgz#07ef56790b953e3b98ef1e2989e347983c72871d"
|
||||
|
@ -4738,6 +4738,10 @@ react-modal@^1.4.0:
|
|||
prop-types "^15.5.7"
|
||||
react-dom-factories "^1.0.0"
|
||||
|
||||
react-moment@^0.6.4:
|
||||
version "0.6.4"
|
||||
resolved "https://registry.yarnpkg.com/react-moment/-/react-moment-0.6.4.tgz#5e531d47ad7b0bff6f6b7175093e98659f5e667b"
|
||||
|
||||
react-overlays@^0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-0.7.0.tgz#531898ff566c7e5c7226ead2863b8cf9fbb5a981"
|
||||
|
@ -4781,6 +4785,12 @@ react-router-dom@^4.1.2:
|
|||
prop-types "^15.5.4"
|
||||
react-router "^4.2.0"
|
||||
|
||||
react-router-prop-types@^0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-router-prop-types/-/react-router-prop-types-0.0.1.tgz#fd7fe4431f8ee104cdb250c738f410eb85847377"
|
||||
dependencies:
|
||||
prop-types "15.5.10"
|
||||
|
||||
react-router@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.2.0.tgz#61f7b3e3770daeb24062dae3eedef1b054155986"
|
||||
|
|
Loading…
Add table
Reference in a new issue