adds team details view

This commit is contained in:
zmagod 2017-08-31 15:56:55 +02:00
parent 1325732b0c
commit 91d2d8a935
11 changed files with 359 additions and 80 deletions

View file

@ -1,50 +1,31 @@
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
user_team_service = ClientApi::UserTeamService.new(user: current_user)
user_team_service.destroy_user_team_and_assign_new_team_owner!
success_response(user_team_service.teams_data)
rescue
unsuccess_response
end
def update_role
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(locals)
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)
}
locals: locals
end
end
end
@ -60,27 +41,7 @@ module ClientApi
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

View file

@ -22,15 +22,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 +57,31 @@ 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"
},
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",

View file

@ -1,7 +1,10 @@
import React, { Component } from "react";
import PropTypes, { number, string, bool } from "prop-types";
import ReactRouterPropTypes from "react-router-prop-types";
import styled from "styled-components";
import { FormattedMessage } from "react-intl";
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 } from "../../../../app/routes";
@ -10,6 +13,8 @@ import {
COLOR_CONCRETE
} from "../../../../app/constants/colors";
import TeamsMembers from "./components/TeamsMembers";
const Wrapper = styled.div`
background: white;
box-sizing: border-box;
@ -24,6 +29,21 @@ 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.div`
&:hover {
text-decoration: underline;
cursor: pointer;
}
`;
class SettingsTeamPageContainer extends Component {
constructor(props) {
super(props);
@ -31,24 +51,102 @@ class SettingsTeamPageContainer extends Component {
team: {},
users: []
};
this.updateDescription = this.updateDescription.bind(this);
this.updateRole = this.updateRole.bind(this)
}
componentDidMount() {
const path = TEAM_DETAILS_PATH.replace(":team_id", this.props.params.id);
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({ team, users });
});
}
updateDescription() {
console.log("banana");
}
updateRole(userId) {
}
renderDescription() {
if (this.state.team.description) {
return this.state.team.description;
}
return (
<FormattedHTMLMessage id="settings_page.single_team.no_description" />
);
}
render() {
return (
<Wrapper>
<TabTitle>
<FormattedMessage id="settings_page.all_teams" />
{` / ${this.state.team.name}`}
</TabTitle>
<Row>
<Col xs={6} sm={3}>
<BadgeWrapper>
<Glyphicon glyph="calendar" />
</BadgeWrapper>
<Well>
<FormattedHTMLMessage
id="settings_page.single_team.created_on"
values={{
created_at: moment(this.state.team.created_at).format(
"DD.MM.YYYY"
)
}}
/>
</Well>
</Col>
<Col xs={10} sm={5}>
<BadgeWrapper>
<Glyphicon glyph="user" />
</BadgeWrapper>
<Well>
<FormattedHTMLMessage
id="settings_page.single_team.created_by"
values={{ created_by: this.state.team.created_by }}
/>
</Well>
</Col>
<Col xs={8} sm={4}>
<BadgeWrapper>
<Glyphicon glyph="hdd" />
</BadgeWrapper>
<Well>
<FormattedHTMLMessage
id="settings_page.single_team.space_usage"
values={{
space_usage: prettysize(this.state.team.space_taken)
}}
/>
</Well>
</Col>
</Row>
<Row>
<Col sm={12} onClick={this.updateDescription}>
<BadgeWrapper>
<Glyphicon glyph="info-sign" />
</BadgeWrapper>
<StyledWell>
{this.renderDescription()}
</StyledWell>
</Col>
</Row>
<TeamsMembers members={this.state.users} updateRole={this.updateRole} />
</Wrapper>
);
}
}
SettingsTeamPageContainer.PropTypes = {
match: ReactRouterPropTypes.match.isRequired
};
export default SettingsTeamPageContainer;

View file

@ -0,0 +1,118 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import {
Panel,
Button,
Glyphicon,
DropdownButton,
MenuItem
} from "react-bootstrap";
import { FormattedMessage } from "react-intl";
import DataTable from "../../../../../shared/data_table";
class TeamsMembers extends Component {
constructor(params) {
super(params);
this.memberAction = this.memberAction.bind(this);
}
currentRole(memberRole, role) {
return memberRole === role ? <Glyphicon glyph="ok" /> : " ";
}
memberAction(data, row) {
return (
<DropdownButton bsStyle="default" title={"banana"} id="actions-dropdown">
<MenuItem className="dropdown-header" disabled>
<FormattedMessage id="settings_page.single_team.actions.user_role" />
</MenuItem>
<MenuItem >
{this.currentRole(data.current_role, "Guest")}
<FormattedMessage id="settings_page.single_team.actions.guest" />
</MenuItem>
<MenuItem >
{this.currentRole(data.current_role, "Normal user")}
<FormattedMessage id="settings_page.single_team.actions.normal_user" />
</MenuItem>
<MenuItem >
{this.currentRole(data.current_role, "Administrator")}
<FormattedMessage id="settings_page.single_team.actions.administrator" />
</MenuItem>
<MenuItem divider />
<MenuItem>
<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} />
</Panel>
);
}
}
export default TeamsMembers;

View file

@ -39,9 +39,9 @@ class TeamsDataTable extends Component {
);
}
linkToTeam(name, col) {
linkToTeam(name, row) {
return (
<Link to={`${SETTINGS_TEAMS_ROUTE}/${col.id}`}>
<Link to={`${SETTINGS_TEAMS_ROUTE}/${row.id}`}>
{name}
</Link>
);

View file

@ -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;
}
}

View file

@ -0,0 +1,62 @@
module ClientApi
class UserTeamService
include NotificationsHelper
include InputSanitizeHelper
def initialize(arg)
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))
raise ClientApi::CustomUserTeamError unless @user_team.team == @team
end
def destroy_user_team_and_assign_new_team_owner!
raise ClientApi::CustomUserTeamError unless 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 teams_data
{
teams: @user.teams_data,
flash_message: 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_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)
team_id = args.fetch(:team_id) { raise ClientApi::CustomUserTeamError }
user_team_id = args.fetch(:user_team_id) { raise ClientApi::CustomUserTeamError }
user = args.fetch(:user) { raise ClientApi::CustomUserTeamError }
{user: user, user_team_id: user_team_id, team_id: team_id }
end
end
CustomUserTeamError = Class.new(StandardError)
end

View file

@ -1,12 +1,16 @@
json.team_details do
json.id team.id
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
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.email team_user.user.full_name
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

View file

@ -29,6 +29,7 @@ Rails.application.routes.draw do
namespace :users do
delete '/leave_team', to: 'user_teams#leave_team'
put '/update_role', to: 'user_teams#update_role'
end
end

View file

@ -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"
}
}

View file

@ -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"