mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-09-11 23:54:43 +08:00
Merge pull request #778 from ZmagoD/zd_SCI_1497
Rewrite all teams page into React.js
This commit is contained in:
commit
6159b18533
30 changed files with 626 additions and 32 deletions
|
@ -1,5 +1,6 @@
|
||||||
module ClientApi
|
module ClientApi
|
||||||
class TeamsController < ApplicationController
|
class TeamsController < ApplicationController
|
||||||
|
include ClientApi::Users::UserTeamsHelper
|
||||||
MissingTeamError = Class.new(StandardError)
|
MissingTeamError = Class.new(StandardError)
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@ -34,11 +35,11 @@ module ClientApi
|
||||||
end
|
end
|
||||||
|
|
||||||
def teams
|
def teams
|
||||||
{ teams: current_user.teams }
|
{ teams: current_user.teams_data }
|
||||||
end
|
end
|
||||||
|
|
||||||
def change_current_team
|
def change_current_team
|
||||||
team_id = params.fetch(:team_id) { raise MissingTeamError }
|
team_id = params.fetch(:team_id) { raise MissingTeamError }
|
||||||
unless current_user.teams.pluck(:id).include? team_id
|
unless current_user.teams.pluck(:id).include? team_id
|
||||||
raise MissingTeamError
|
raise MissingTeamError
|
||||||
end
|
end
|
||||||
|
|
86
app/controllers/client_api/users/user_teams_controller.rb
Normal file
86
app/controllers/client_api/users/user_teams_controller.rb
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
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
|
||||||
|
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
|
||||||
|
respond_to do |format|
|
||||||
|
# return a list of teams
|
||||||
|
format.json do
|
||||||
|
render template: '/client_api/teams/index',
|
||||||
|
status: :ok,
|
||||||
|
locals: {
|
||||||
|
teams: current_user.teams_data,
|
||||||
|
flash_message: t('client_api.user_teams.leave_flash',
|
||||||
|
team: @team.name)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def unsuccess_response
|
||||||
|
respond_to do |format|
|
||||||
|
format.json do
|
||||||
|
render json: { message: t(
|
||||||
|
'client_api.user_teams.leave_team_error'
|
||||||
|
) },
|
||||||
|
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
|
10
app/helpers/client_api/users/user_teams_helper.rb
Normal file
10
app/helpers/client_api/users/user_teams_helper.rb
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
module ClientApi
|
||||||
|
module Users
|
||||||
|
module UserTeamsHelper
|
||||||
|
def retrive_role_name(index)
|
||||||
|
return unless index
|
||||||
|
['Guest', 'Normal user', 'Administrator'].at(index)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -23,3 +23,7 @@ export const CHANGE_RECENT_NOTIFICATION_EMAIL =
|
||||||
"CHANGE_RECENT_NOTIFICATION_EMAIL";
|
"CHANGE_RECENT_NOTIFICATION_EMAIL";
|
||||||
export const CHANGE_SYSTEM_MESSAGE_NOTIFICATION_EMAIL =
|
export const CHANGE_SYSTEM_MESSAGE_NOTIFICATION_EMAIL =
|
||||||
"CHANGE_SYSTEM_MESSAGE_NOTIFICATION_EMAIL";
|
"CHANGE_SYSTEM_MESSAGE_NOTIFICATION_EMAIL";
|
||||||
|
|
||||||
|
// user teams
|
||||||
|
export const LEAVE_TEAM = "LEAVE_TEAM"
|
||||||
|
export const SHOW_LEAVE_TEAM_MODAL = "SHOW_LEAVE_TEAM_MODAL"
|
||||||
|
|
|
@ -5,10 +5,12 @@ import {
|
||||||
} from "../shared/reducers/TeamReducers";
|
} from "../shared/reducers/TeamReducers";
|
||||||
import { globalActivities } from "../shared/reducers/ActivitiesReducers";
|
import { globalActivities } from "../shared/reducers/ActivitiesReducers";
|
||||||
import { currentUser } from "../shared/reducers/UsersReducer";
|
import { currentUser } from "../shared/reducers/UsersReducer";
|
||||||
|
import { showLeaveTeamModal } from "../shared/reducers/LeaveTeamReducer";
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
current_team: setCurrentTeam,
|
current_team: setCurrentTeam,
|
||||||
all_teams: getListOfTeams,
|
all_teams: getListOfTeams,
|
||||||
global_activities: globalActivities,
|
global_activities: globalActivities,
|
||||||
current_user: currentUser
|
current_user: currentUser,
|
||||||
|
showLeaveTeamModal
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,6 +22,9 @@ export const PREMIUM_LINK = "http://scinote.net/premium/";
|
||||||
export const CONTACT_US_LINK =
|
export const CONTACT_US_LINK =
|
||||||
"http://scinote.net/story-of-scinote/#contact-scinote";
|
"http://scinote.net/story-of-scinote/#contact-scinote";
|
||||||
|
|
||||||
|
// user teams
|
||||||
|
export const LEAVE_TEAM_PATH = "/client_api/users/leave_team"
|
||||||
|
|
||||||
// settings
|
// settings
|
||||||
export const SETTINGS_ACCOUNT_PROFILE = "/settings/account/profile";
|
export const SETTINGS_ACCOUNT_PROFILE = "/settings/account/profile";
|
||||||
export const SETTINGS_ACCOUNT_PREFERENCES = "/settings/account/preferences";
|
export const SETTINGS_ACCOUNT_PREFERENCES = "/settings/account/preferences";
|
||||||
|
|
|
@ -18,6 +18,19 @@ export default {
|
||||||
info_label: "Info"
|
info_label: "Info"
|
||||||
},
|
},
|
||||||
settings_page: {
|
settings_page: {
|
||||||
|
all_teams: "All teams",
|
||||||
|
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",
|
account: "Account",
|
||||||
team: "Team",
|
team: "Team",
|
||||||
avatar: "Avatar",
|
avatar: "Avatar",
|
||||||
|
|
8
app/javascript/packs/shared/actions/LeaveTeamActions.js
Normal file
8
app/javascript/packs/shared/actions/LeaveTeamActions.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
|
@ -3,16 +3,16 @@ import _ from "lodash";
|
||||||
import { TEAMS_PATH, CHANGE_TEAM_PATH } from "../../app/routes";
|
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 } from "../../app/action_types";
|
||||||
|
|
||||||
function addTeamsData(data) {
|
export function addTeamsData(data) {
|
||||||
return {
|
return {
|
||||||
type: GET_LIST_OF_TEAMS,
|
type: GET_LIST_OF_TEAMS,
|
||||||
payload: data
|
payload: data
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setCurrentUser(user) {
|
export function setCurrentTeam(team) {
|
||||||
return {
|
return {
|
||||||
user,
|
team,
|
||||||
type: SET_CURRENT_TEAM
|
type: SET_CURRENT_TEAM
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -22,10 +22,10 @@ export function getTeamsList() {
|
||||||
axios
|
axios
|
||||||
.get(TEAMS_PATH, { withCredentials: true })
|
.get(TEAMS_PATH, { withCredentials: true })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
let teams = _.values(response.data);
|
const teams = response.data.teams.collection;
|
||||||
dispatch(addTeamsData(teams));
|
dispatch(addTeamsData(teams));
|
||||||
let current_team = _.find(teams, team => team.current_team);
|
const currentTeam = _.find(teams, team => team.current_team);
|
||||||
dispatch(setCurrentUser(current_team));
|
dispatch(setCurrentTeam(currentTeam));
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.log("get Teams Error: ", error);
|
console.log("get Teams Error: ", error);
|
||||||
|
@ -33,15 +33,15 @@ export function getTeamsList() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function changeTeam(team_id) {
|
export function changeTeam(teamId) {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
axios
|
axios
|
||||||
.post(CHANGE_TEAM_PATH, { team_id }, { withCredentials: true })
|
.post(CHANGE_TEAM_PATH, { teamId }, { withCredentials: true })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
let teams = _.values(response.data);
|
const teams = response.data.teams.collection;
|
||||||
dispatch(addTeamsData(teams));
|
dispatch(addTeamsData(teams));
|
||||||
let current_team = _.find(teams, team => team.current_team);
|
const currentTeam = _.find(teams, team => team.current_team);
|
||||||
dispatch(setCurrentUser(current_team));
|
dispatch(setCurrentTeam(currentTeam));
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.log("get Teams Error: ", error);
|
console.log("get Teams Error: ", error);
|
||||||
|
|
|
@ -108,4 +108,4 @@ DataTable.propTypes = {
|
||||||
data: PropTypes.arrayOf(PropTypes.object).isRequired
|
data: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DataTable;
|
export default DataTable;
|
||||||
|
|
7
app/javascript/packs/shared/modals_container/index.jsx
Normal file
7
app/javascript/packs/shared/modals_container/index.jsx
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import React from "react";
|
||||||
|
import LeaveTeamModal from "./modals/LeaveTeamModal";
|
||||||
|
|
||||||
|
export default () =>
|
||||||
|
<div>
|
||||||
|
<LeaveTeamModal />
|
||||||
|
</div>;
|
|
@ -0,0 +1,103 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import PropTypes, { bool, number, string, func } from "prop-types";
|
||||||
|
import { Modal, Button, Alert, Glyphicon } from "react-bootstrap";
|
||||||
|
import { FormattedMessage, FormattedHTMLMessage } from "react-intl";
|
||||||
|
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";
|
||||||
|
|
||||||
|
class LeaveTeamModal extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.onCloseModal = this.onCloseModal.bind(this);
|
||||||
|
this.leaveTeam = this.leaveTeam.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onCloseModal() {
|
||||||
|
this.props.leaveTeamModalShow(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
leaveTeam() {
|
||||||
|
const teamUrl = `${LEAVE_TEAM_PATH}?team=${this.props.teamId}`;
|
||||||
|
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);
|
||||||
|
this.props.setCurrentTeam(currentTeam);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log("error: ", error.response.data.message);
|
||||||
|
});
|
||||||
|
this.props.leaveTeamModalShow(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Modal show={this.props.showModal} onHide={this.onCloseModal}>
|
||||||
|
<Modal.Header closeButton>
|
||||||
|
<Modal.Title>
|
||||||
|
<FormattedMessage
|
||||||
|
id="settings_page.leave_team_modal.title"
|
||||||
|
values={{ teamName: this.props.teamName }}
|
||||||
|
/>
|
||||||
|
</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage id="settings_page.leave_team_modal.subtitle" />
|
||||||
|
</p>
|
||||||
|
<Alert bsStyle="danger">
|
||||||
|
<Glyphicon glyph="exclamation-sign" />
|
||||||
|
<FormattedMessage id="settings_page.leave_team_modal.warnings" />
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<FormattedMessage id="settings_page.leave_team_modal.warning_message_one" />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<FormattedHTMLMessage id="settings_page.leave_team_modal.warning_message_two" />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<FormattedMessage id="settings_page.leave_team_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.leaveTeam}>
|
||||||
|
<FormattedMessage id="settings_page.leave_team_modal.leave_team" />
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LeaveTeamModal.propTypes = {
|
||||||
|
showModal: bool.isRequired,
|
||||||
|
teamId: number.isRequired,
|
||||||
|
teamName: string.isRequired,
|
||||||
|
addTeamsData: func.isRequired,
|
||||||
|
leaveTeamModalShow: func.isRequired
|
||||||
|
};
|
||||||
|
const mapStateToProps = ({ showLeaveTeamModal }) => ({
|
||||||
|
showModal: showLeaveTeamModal.show,
|
||||||
|
teamId: showLeaveTeamModal.id,
|
||||||
|
teamName: showLeaveTeamModal.teamName
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, {
|
||||||
|
leaveTeamModalShow,
|
||||||
|
addTeamsData,
|
||||||
|
setCurrentTeam
|
||||||
|
})(LeaveTeamModal);
|
|
@ -7,7 +7,7 @@ import styled from "styled-components";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
|
||||||
import { BORDER_GRAY_COLOR } from "../../../app/constants/colors";
|
import { BORDER_GRAY_COLOR } from "../../../app/constants/colors";
|
||||||
import { setCurrentUser, changeTeam } from "../../actions/TeamsActions";
|
import { changeTeam } from "../../actions/TeamsActions";
|
||||||
import { getTeamsList } from "../../actions/TeamsActions";
|
import { getTeamsList } from "../../actions/TeamsActions";
|
||||||
|
|
||||||
const StyledNavDropdown = styled(NavDropdown)`
|
const StyledNavDropdown = styled(NavDropdown)`
|
||||||
|
@ -89,14 +89,11 @@ TeamSwitch.propTypes = {
|
||||||
// Map the states from store to component
|
// Map the states from store to component
|
||||||
const mapStateToProps = ({ all_teams, current_team }) => ({
|
const mapStateToProps = ({ all_teams, current_team }) => ({
|
||||||
current_team,
|
current_team,
|
||||||
all_teams: _.values(all_teams)
|
all_teams: all_teams.collection
|
||||||
});
|
});
|
||||||
|
|
||||||
// Map the fetch activity action to component
|
// Map the fetch activity action to component
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
setCurrentUser() {
|
|
||||||
dispatch(setCurrentUser());
|
|
||||||
},
|
|
||||||
changeTeam(teamId) {
|
changeTeam(teamId) {
|
||||||
dispatch(changeTeam(teamId));
|
dispatch(changeTeam(teamId));
|
||||||
},
|
},
|
||||||
|
|
11
app/javascript/packs/shared/reducers/LeaveTeamReducer.js
Normal file
11
app/javascript/packs/shared/reducers/LeaveTeamReducer.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
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;
|
||||||
|
}
|
|
@ -3,14 +3,17 @@ import { SET_CURRENT_TEAM, GET_LIST_OF_TEAMS } from "../../app/action_types";
|
||||||
const initialState = { name: "", id: 0, current_team: true };
|
const initialState = { name: "", id: 0, current_team: true };
|
||||||
export const setCurrentTeam = (state = initialState, action) => {
|
export const setCurrentTeam = (state = initialState, action) => {
|
||||||
if (action.type === SET_CURRENT_TEAM) {
|
if (action.type === SET_CURRENT_TEAM) {
|
||||||
return Object.assign({}, state, action.user);
|
return Object.assign({}, state, action.team);
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getListOfTeams = (state = [], action) => {
|
export const getListOfTeams = (state = { collection: [] }, action) => {
|
||||||
if (action.type === GET_LIST_OF_TEAMS) {
|
if (action.type === GET_LIST_OF_TEAMS) {
|
||||||
return Object.assign({}, state, action.payload);
|
return {
|
||||||
|
...state,
|
||||||
|
collection: action.payload
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,6 +10,7 @@ import store from "../../app/store";
|
||||||
import messages from "../../locales/messages";
|
import messages from "../../locales/messages";
|
||||||
|
|
||||||
import MainNav from "./components/MainNav";
|
import MainNav from "./components/MainNav";
|
||||||
|
import ModalsContainer from "../../shared/modals_container";
|
||||||
|
|
||||||
addLocaleData([...enLocaleData]);
|
addLocaleData([...enLocaleData]);
|
||||||
const locale = "en-US";
|
const locale = "en-US";
|
||||||
|
@ -17,6 +18,7 @@ const locale = "en-US";
|
||||||
const SettingsPage = () =>
|
const SettingsPage = () =>
|
||||||
<div>
|
<div>
|
||||||
<MainNav />
|
<MainNav />
|
||||||
|
<ModalsContainer />
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import PropTypes, { number, string, bool } from "prop-types";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import { BORDER_LIGHT_COLOR } from "../../../../app/constants/colors";
|
import {
|
||||||
|
BORDER_LIGHT_COLOR,
|
||||||
|
COLOR_CONCRETE
|
||||||
|
} from "../../../../app/constants/colors";
|
||||||
|
|
||||||
|
import TeamsPageDetails from "./components/TeamsPageDetails";
|
||||||
|
import TeamsDataTable from "./components/TeamsDataTable";
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
background: white;
|
background: white;
|
||||||
|
@ -9,12 +18,42 @@ const Wrapper = styled.div`
|
||||||
border: 1px solid ${BORDER_LIGHT_COLOR};
|
border: 1px solid ${BORDER_LIGHT_COLOR};
|
||||||
border-top: none;
|
border-top: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 16px 0 50px 0;
|
padding: 16px 15px 50px 15px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SettingsTeams = () =>
|
const TabTitle = styled.div`
|
||||||
|
background-color: ${COLOR_CONCRETE};
|
||||||
|
padding: 15px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SettingsTeams = ({ teams }) =>
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<h1 className="text-center">Settings Teams</h1>
|
<TabTitle>
|
||||||
|
<FormattedMessage id="settings_page.all_teams" />
|
||||||
|
</TabTitle>
|
||||||
|
<TeamsPageDetails teams={teams} />
|
||||||
|
<TeamsDataTable teams={teams} />
|
||||||
</Wrapper>;
|
</Wrapper>;
|
||||||
|
|
||||||
export default SettingsTeams;
|
SettingsTeams.propTypes = {
|
||||||
|
teams: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
id: number.isRequired,
|
||||||
|
name: string.isRequired,
|
||||||
|
current_team: bool.isRequired,
|
||||||
|
role: string.isRequired,
|
||||||
|
members: number.isRequired,
|
||||||
|
can_be_leaved: bool.isRequired
|
||||||
|
}).isRequired
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
SettingsTeams.defaultProps = {
|
||||||
|
teams: [{id: 0, name: "", current_team: "", role: "", members: 0}]
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = ({ all_teams }) => ({
|
||||||
|
teams: all_teams.collection
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(SettingsTeams);
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
import { LEAVE_TEAM_PATH } from '../../../../../app/routes'
|
|
@ -0,0 +1,106 @@
|
||||||
|
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 { FormattedMessage } from "react-intl";
|
||||||
|
import { leaveTeamModalShow } from "../../../../../shared/actions/LeaveTeamActions";
|
||||||
|
import DataTable from "../../../../../shared/data_table";
|
||||||
|
|
||||||
|
class TeamsDataTable extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.leaveTeamModal = this.leaveTeamModal.bind(this);
|
||||||
|
this.leaveTeamButton = this.leaveTeamButton.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
leaveTeamModal(e, id) {
|
||||||
|
const team = _.find(this.props.teams, el => el.id === id);
|
||||||
|
this.props.leaveTeamModalShow(true, id, team.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
leaveTeamButton(id) {
|
||||||
|
const team = _.find(this.props.teams, el => el.id === id);
|
||||||
|
if (team.can_be_leaved) {
|
||||||
|
return (
|
||||||
|
<Button onClick={e => this.leaveTeamModal(e, id)}>
|
||||||
|
<FormattedMessage id="settings_page.leave_team" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button disabled>
|
||||||
|
<FormattedMessage id="settings_page.leave_team" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const options = {
|
||||||
|
defaultSortName: "name",
|
||||||
|
defaultSortOrder: "desc",
|
||||||
|
sizePerPageList: [10, 25, 50, 100],
|
||||||
|
paginationPosition: "top",
|
||||||
|
alwaysShowAllBtns: false
|
||||||
|
};
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "Name",
|
||||||
|
isKey: false,
|
||||||
|
textId: "name",
|
||||||
|
position: 0,
|
||||||
|
dataSort: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "Role",
|
||||||
|
isKey: false,
|
||||||
|
textId: "role",
|
||||||
|
position: 1,
|
||||||
|
dataSort: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "Members",
|
||||||
|
isKey: false,
|
||||||
|
textId: "members",
|
||||||
|
position: 2,
|
||||||
|
dataSort: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: "",
|
||||||
|
isKey: true,
|
||||||
|
textId: "id",
|
||||||
|
dataFormat: this.leaveTeamButton,
|
||||||
|
position: 3
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<DataTable
|
||||||
|
data={this.props.teams}
|
||||||
|
columns={columns}
|
||||||
|
pagination={true}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TeamsDataTable.propTypes = {
|
||||||
|
leaveTeamModalShow: func.isRequired,
|
||||||
|
teams: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
id: number.isRequired,
|
||||||
|
name: string.isRequired,
|
||||||
|
current_team: bool.isRequired,
|
||||||
|
role: string.isRequired,
|
||||||
|
members: number.isRequired,
|
||||||
|
can_be_leaved: bool.isRequired
|
||||||
|
}).isRequired
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(null, { leaveTeamModalShow })(TeamsDataTable);
|
|
@ -0,0 +1,59 @@
|
||||||
|
import React from "react";
|
||||||
|
import PropTypes, { number, string, bool } from "prop-types";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { FormattedMessage, FormattedPlural } from "react-intl";
|
||||||
|
import { Button, Glyphicon } from "react-bootstrap";
|
||||||
|
|
||||||
|
const Wrapper = styled.div`margin: 15px 0;`;
|
||||||
|
const TeamsPageDetails = ({ teams }) => {
|
||||||
|
const teamsNumber = teams.length;
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<FormattedPlural
|
||||||
|
value={teamsNumber}
|
||||||
|
one={
|
||||||
|
<FormattedMessage
|
||||||
|
id="settings_page.in_team"
|
||||||
|
values={{
|
||||||
|
num: teamsNumber
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
other={
|
||||||
|
<FormattedMessage
|
||||||
|
id="settings_page.in_teams"
|
||||||
|
values={{
|
||||||
|
num: teamsNumber
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
window.location = "/users/settings/teams/new";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Glyphicon glyph="plus" /> <FormattedMessage id="global_team_switch.new_team" />
|
||||||
|
</Button>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TeamsPageDetails.propTypes = {
|
||||||
|
teams: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
id: number.isRequired,
|
||||||
|
name: string.isRequired,
|
||||||
|
current_team: bool.isRequired,
|
||||||
|
role: string.isRequired,
|
||||||
|
members: number.isRequired,
|
||||||
|
can_be_leaved: bool.isRequired
|
||||||
|
})
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
TeamsPageDetails.defaultProps = {
|
||||||
|
teams: []
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TeamsPageDetails;
|
|
@ -204,6 +204,24 @@ class User < ApplicationRecord
|
||||||
Team.find_by_id(self.current_team_id)
|
Team.find_by_id(self.current_team_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Retrieves the data needed in all teams page
|
||||||
|
def teams_data
|
||||||
|
ActiveRecord::Base.connection.execute(
|
||||||
|
ActiveRecord::Base.send(
|
||||||
|
:sanitize_sql_array,
|
||||||
|
['SELECT teams.id AS id, teams.name AS name, user_teams.role ' \
|
||||||
|
'AS role, (SELECT COUNT(*) FROM user_teams WHERE ' \
|
||||||
|
'user_teams.team_id = teams.id) AS members, ' \
|
||||||
|
'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 ' \
|
||||||
|
'teams.id=user_teams.team_id WHERE user_teams.user_id=?',
|
||||||
|
self.current_team_id, self.id]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
# Search all active users for username & email. Can
|
# Search all active users for username & email. Can
|
||||||
# also specify which team to ignore.
|
# also specify which team to ignore.
|
||||||
def self.search(
|
def self.search(
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
json.array! teams do |team|
|
json.teams do
|
||||||
json.id team.id
|
json.collection teams do |team|
|
||||||
json.name team.name
|
json.id team.fetch('id')
|
||||||
json.current_team team == current_user.current_team
|
json.name team.fetch('name')
|
||||||
|
json.members team.fetch('members')
|
||||||
|
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')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
json.teams do
|
||||||
|
json.flash_message flash_message
|
||||||
|
json.collection teams do |team|
|
||||||
|
json.id team.fetch('id')
|
||||||
|
json.name team.fetch('name')
|
||||||
|
json.members team.fetch('members')
|
||||||
|
json.role 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')
|
||||||
|
end
|
||||||
|
end
|
|
@ -1819,3 +1819,8 @@ en:
|
||||||
More: "More"
|
More: "More"
|
||||||
Added: 'Added'
|
Added: 'Added'
|
||||||
by: 'by'
|
by: 'by'
|
||||||
|
|
||||||
|
client_api:
|
||||||
|
user_teams:
|
||||||
|
leave_team_error: "An error occured."
|
||||||
|
leave_flash: "Successfuly left team %{team}."
|
||||||
|
|
|
@ -23,6 +23,10 @@ Rails.application.routes.draw do
|
||||||
get '/recent_notifications', to: 'notifications#recent_notifications'
|
get '/recent_notifications', to: 'notifications#recent_notifications'
|
||||||
# users
|
# users
|
||||||
get '/current_user_info', to: 'users#current_user_info'
|
get '/current_user_info', to: 'users#current_user_info'
|
||||||
|
|
||||||
|
namespace :users do
|
||||||
|
delete '/leave_team', to: 'user_teams#leave_team'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Save sample table state
|
# Save sample table state
|
||||||
|
|
32
spec/controllers/client_api/users/user_teams_controller.rb
Normal file
32
spec/controllers/client_api/users/user_teams_controller.rb
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe ClientApi::Users::UserTeamsController, type: :controller do
|
||||||
|
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
|
||||||
|
expect(response).to be_success
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'Returns HTTP unprocessable_entity if user can\'t leave the team' do
|
||||||
|
delete :leave_team, params: { team: @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
|
||||||
|
delete :leave_team, format: :json
|
||||||
|
expect(response).to_not be_success
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
5
spec/factories/user_teams.rb
Normal file
5
spec/factories/user_teams.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
FactoryGirl.define do
|
||||||
|
factory :user_team do
|
||||||
|
role 'admin'
|
||||||
|
end
|
||||||
|
end
|
|
@ -149,4 +149,47 @@ describe User, type: :model do
|
||||||
expect(user.name).to eq 'Axe'
|
expect(user.name).to eq 'Axe'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'teams_data should return a list of teams' do
|
||||||
|
# needs persistence because is testing a sql query
|
||||||
|
let(:team) { create :team }
|
||||||
|
let(:user_one) do
|
||||||
|
create :user, email: 'user1@asdf.com', current_team_id: team.id
|
||||||
|
end
|
||||||
|
let(:user_two) { create :user, email: 'user2@asdf.com' }
|
||||||
|
|
||||||
|
it 'in a specific format: {id: .., name: .., members: .., role: ' \
|
||||||
|
'.., current_team: .., can_be_leaved: ..}' do
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
user_one.teams_data.first.each do |k, v|
|
||||||
|
expect(v).to eq(expected_result.fetch(k.to_sym))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should return correct number of team members' do
|
||||||
|
create :user_team, team: team, user: user_one
|
||||||
|
create :user_team, team: team, user: user_two
|
||||||
|
expected_result = {
|
||||||
|
id: team.id,
|
||||||
|
name: team.name,
|
||||||
|
members: 2,
|
||||||
|
role: 2,
|
||||||
|
current_team: true,
|
||||||
|
can_be_leaved: true
|
||||||
|
}
|
||||||
|
|
||||||
|
user_one.teams_data.first.each do |k, v|
|
||||||
|
expect(v).to eq(expected_result.fetch(k.to_sym))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
require 'shoulda-matchers'
|
require 'shoulda-matchers'
|
||||||
require 'database_cleaner'
|
require 'database_cleaner'
|
||||||
|
require 'devise'
|
||||||
|
require_relative 'support/controller_macros'
|
||||||
ENV['RAILS_ENV'] = 'test'
|
ENV['RAILS_ENV'] = 'test'
|
||||||
require File.expand_path('../../config/environment', __FILE__)
|
require File.expand_path('../../config/environment', __FILE__)
|
||||||
# Prevent database truncation if the environment is production
|
# Prevent database truncation if the environment is production
|
||||||
|
@ -78,6 +80,9 @@ RSpec.configure do |config|
|
||||||
|
|
||||||
# includes FactoryGirl in rspec
|
# includes FactoryGirl in rspec
|
||||||
config.include FactoryGirl::Syntax::Methods
|
config.include FactoryGirl::Syntax::Methods
|
||||||
|
# Devise
|
||||||
|
config.include Devise::Test::ControllerHelpers, type: :controller
|
||||||
|
config.extend ControllerMacros, type: :controller
|
||||||
end
|
end
|
||||||
|
|
||||||
# config shoulda matchers to work with rspec
|
# config shoulda matchers to work with rspec
|
||||||
|
|
10
spec/support/controller_macros.rb
Normal file
10
spec/support/controller_macros.rb
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
module ControllerMacros
|
||||||
|
def login_user
|
||||||
|
before(:each) do
|
||||||
|
@request.env['devise.mapping'] = Devise.mappings[:user]
|
||||||
|
user = FactoryGirl.create(:user)
|
||||||
|
user.confirm
|
||||||
|
sign_in user
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Add table
Reference in a new issue