diff --git a/Gemfile b/Gemfile
index e1c48b798..23e6b12d4 100644
--- a/Gemfile
+++ b/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
diff --git a/Gemfile.lock b/Gemfile.lock
index a4c3f3265..f29f17386 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -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
diff --git a/app/controllers/client_api/teams/teams_controller.rb b/app/controllers/client_api/teams/teams_controller.rb
new file mode 100644
index 000000000..e97e03daa
--- /dev/null
+++ b/app/controllers/client_api/teams/teams_controller.rb
@@ -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
diff --git a/app/controllers/client_api/teams_controller.rb b/app/controllers/client_api/teams_controller.rb
deleted file mode 100644
index 0ffba958a..000000000
--- a/app/controllers/client_api/teams_controller.rb
+++ /dev/null
@@ -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
diff --git a/app/controllers/client_api/users/user_teams_controller.rb b/app/controllers/client_api/users/user_teams_controller.rb
index de1a39f8f..bf66fa4ff 100644
--- a/app/controllers/client_api/users/user_teams_controller.rb
+++ b/app/controllers/client_api/users/user_teams_controller.rb
@@ -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
diff --git a/app/javascript/packs/app/action_types.js b/app/javascript/packs/app/action_types.js
index b14c08712..b2d3a7443 100644
--- a/app/javascript/packs/app/action_types.js
+++ b/app/javascript/packs/app/action_types.js
@@ -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";
diff --git a/app/javascript/packs/app/constants/colors.js b/app/javascript/packs/app/constants/colors.js
index 55972ab92..15e7f0471 100644
--- a/app/javascript/packs/app/constants/colors.js
+++ b/app/javascript/packs/app/constants/colors.js
@@ -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";
diff --git a/app/javascript/packs/app/constants/numeric.js b/app/javascript/packs/app/constants/numeric.js
index dc04372b4..1ed90ac4d 100644
--- a/app/javascript/packs/app/constants/numeric.js
+++ b/app/javascript/packs/app/constants/numeric.js
@@ -1 +1,3 @@
export const ENTER_KEY_CODE = 13;
+export const TEXT_MAX_LENGTH = 10000;
+export const NAME_MAX_LENGTH = 255;
diff --git a/app/javascript/packs/app/dom_routes.js b/app/javascript/packs/app/dom_routes.js
new file mode 100644
index 000000000..f04d2ae72
--- /dev/null
+++ b/app/javascript/packs/app/dom_routes.js
@@ -0,0 +1,3 @@
+// Settings page
+export const SETTINGS_TEAMS_ROUTE = "/settings/teams";
+export const SETTINGS_TEAM_ROUTE = "/settings/teams/:id";
diff --git a/app/javascript/packs/app/reducers.js b/app/javascript/packs/app/reducers.js
index 0cfd67fc9..9ae33fb42 100644
--- a/app/javascript/packs/app/reducers.js
+++ b/app/javascript/packs/app/reducers.js
@@ -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,
diff --git a/app/javascript/packs/app/routes.js b/app/javascript/packs/app/routes.js
index 9ab561127..9515ea989 100644
--- a/app/javascript/packs/app/routes.js
+++ b/app/javascript/packs/app/routes.js
@@ -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";
diff --git a/app/javascript/packs/locales/messages.js b/app/javascript/packs/locales/messages.js
index 7932e3533..a2b7ada18 100644
--- a/app/javascript/packs/locales/messages.js
+++ b/app/javascript/packs/locales/messages.js
@@ -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 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"
- },
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 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}?",
+ 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.",
+ 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: {created_at}",
+ created_by: "Created by: {created_by}",
+ space_usage: "Space usage: {space_usage}",
+ no_description: "No description",
+ 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",
diff --git a/app/javascript/packs/shared/actions/LeaveTeamActions.js b/app/javascript/packs/shared/actions/LeaveTeamActions.js
deleted file mode 100644
index ffd489a56..000000000
--- a/app/javascript/packs/shared/actions/LeaveTeamActions.js
+++ /dev/null
@@ -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
- };
-}
diff --git a/app/javascript/packs/shared/actions/TeamsActions.js b/app/javascript/packs/shared/actions/TeamsActions.js
index 40f4b7295..3dd85ab17 100644
--- a/app/javascript/packs/shared/actions/TeamsActions.js
+++ b/app/javascript/packs/shared/actions/TeamsActions.js
@@ -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));
diff --git a/app/javascript/packs/shared/modals_container/modals/LeaveTeamModal.jsx b/app/javascript/packs/shared/modals_container/modals/LeaveTeamModal.jsx
index 790ba3a59..4de293378 100644
--- a/app/javascript/packs/shared/modals_container/modals/LeaveTeamModal.jsx
+++ b/app/javascript/packs/shared/modals_container/modals/LeaveTeamModal.jsx
@@ -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 {
@@ -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, {
diff --git a/app/javascript/packs/shared/reducers/LeaveTeamReducer.js b/app/javascript/packs/shared/reducers/LeaveTeamReducer.js
deleted file mode 100644
index a784bc582..000000000
--- a/app/javascript/packs/shared/reducers/LeaveTeamReducer.js
+++ /dev/null
@@ -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;
-}
diff --git a/app/javascript/packs/shared/reducers/TeamReducers.js b/app/javascript/packs/shared/reducers/TeamReducers.js
index a3ca8a7c5..0df2a6417 100644
--- a/app/javascript/packs/shared/reducers/TeamReducers.js
+++ b/app/javascript/packs/shared/reducers/TeamReducers.js
@@ -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;
+};
diff --git a/app/javascript/packs/src/settings/components/MainNav.jsx b/app/javascript/packs/src/settings/components/MainNav.jsx
index 5efd85c92..9a00647d8 100644
--- a/app/javascript/packs/src/settings/components/MainNav.jsx
+++ b/app/javascript/packs/src/settings/components/MainNav.jsx
@@ -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={() => }
/>
-
-
+
+
diff --git a/app/javascript/packs/src/settings/components/team/SettingsTeamPageContainer.jsx b/app/javascript/packs/src/settings/components/team/SettingsTeamPageContainer.jsx
new file mode 100644
index 000000000..e54e8f08c
--- /dev/null
+++ b/app/javascript/packs/src/settings/components/team/SettingsTeamPageContainer.jsx
@@ -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 (
+
+ );
+ }
+
+ renderEditNameModel() {
+ if (this.state.showNameModal) {
+ return(
+
+ );
+ }
+ }
+
+ render() {
+ return (
+
+
+
+
+
+
+
+
+ {this.state.team.name}
+
+
+
+
+ {this.state.team.name}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {this.renderDescription()}
+
+
+
+
+
+
+ {this.renderEditNameModel()}
+
+ );
+ }
+}
+
+SettingsTeamPageContainer.PropTypes = {
+ match: ReactRouterPropTypes.match.isRequired
+};
+
+export default SettingsTeamPageContainer;
diff --git a/app/javascript/packs/src/settings/components/team/actions/UserTeamsActions.js b/app/javascript/packs/src/settings/components/team/actions/UserTeamsActions.js
deleted file mode 100644
index 33f07eb5a..000000000
--- a/app/javascript/packs/src/settings/components/team/actions/UserTeamsActions.js
+++ /dev/null
@@ -1,2 +0,0 @@
-
-import { LEAVE_TEAM_PATH } from '../../../../../app/routes'
diff --git a/app/javascript/packs/src/settings/components/team/components/RemoveUserModal.jsx b/app/javascript/packs/src/settings/components/team/components/RemoveUserModal.jsx
new file mode 100644
index 000000000..a6670dd0f
--- /dev/null
+++ b/app/javascript/packs/src/settings/components/team/components/RemoveUserModal.jsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+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;
diff --git a/app/javascript/packs/src/settings/components/team/components/TeamsMembers.jsx b/app/javascript/packs/src/settings/components/team/components/TeamsMembers.jsx
new file mode 100644
index 000000000..510067d8b
--- /dev/null
+++ b/app/javascript/packs/src/settings/components/team/components/TeamsMembers.jsx
@@ -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 ? : " ";
+ }
+
+ 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 (
+
+
+
+ }
+ id="actions-dropdown"
+ >
+
+
+
+
+
+
+
+ );
+ }
+
+ 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 (
+
+ }
+ >
+
+
+
+
+
+ );
+ }
+}
+
+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;
diff --git a/app/javascript/packs/src/settings/components/team/components/UpdateTeamDescriptionModal.jsx b/app/javascript/packs/src/settings/components/team/components/UpdateTeamDescriptionModal.jsx
new file mode 100644
index 000000000..5ffb57a2a
--- /dev/null
+++ b/app/javascript/packs/src/settings/components/team/components/UpdateTeamDescriptionModal.jsx
@@ -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: (
+
+ )
+ });
+ } 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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {this.state.errorMessage}
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+UpdateTeamDescriptionModal.propTypes = {
+ showModal: bool.isRequired,
+ hideModal: func.isRequired,
+ team: PropTypes.shape({
+ id: number.isRequired,
+ description: string
+ }).isRequired,
+ updateTeamCallback: func.isRequired
+};
+
+export default UpdateTeamDescriptionModal;
diff --git a/app/javascript/packs/src/settings/components/team/components/UpdateTeamNameModal.jsx b/app/javascript/packs/src/settings/components/team/components/UpdateTeamNameModal.jsx
new file mode 100644
index 000000000..47005bef7
--- /dev/null
+++ b/app/javascript/packs/src/settings/components/team/components/UpdateTeamNameModal.jsx
@@ -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: (
+
+ )
+ });
+ } 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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {this.state.errorMessage}
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+UpdateTeamNameModal.propTypes = {
+ showModal: bool.isRequired,
+ hideModal: func.isRequired,
+ team: PropTypes.shape({
+ id: number.isRequired,
+ name: string
+ }).isRequired,
+ updateTeamCallback: func.isRequired
+};
+
+export default UpdateTeamNameModal;
diff --git a/app/javascript/packs/src/settings/components/team/SettingsTeams.jsx b/app/javascript/packs/src/settings/components/teams/SettingsTeams.jsx
similarity index 100%
rename from app/javascript/packs/src/settings/components/team/SettingsTeams.jsx
rename to app/javascript/packs/src/settings/components/teams/SettingsTeams.jsx
diff --git a/app/javascript/packs/src/settings/components/team/components/TeamsDataTable.jsx b/app/javascript/packs/src/settings/components/teams/components/TeamsDataTable.jsx
similarity index 81%
rename from app/javascript/packs/src/settings/components/team/components/TeamsDataTable.jsx
rename to app/javascript/packs/src/settings/components/teams/components/TeamsDataTable.jsx
index e638f9516..2e97cb7e1 100644
--- a/app/javascript/packs/src/settings/components/team/components/TeamsDataTable.jsx
+++ b/app/javascript/packs/src/settings/components/teams/components/TeamsDataTable.jsx
@@ -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 (
-