Merge branch 'decoupling-settings-page' of https://github.com/biosistemika/scinote-web into zd_SCI_1493_v3

This commit is contained in:
zmagod 2017-08-25 08:58:20 +02:00
commit 34be62a648
36 changed files with 12259 additions and 484 deletions

View file

@ -17,12 +17,7 @@
}
},
"rules": {
"import/no-unresolved": [
"error",
{
"ignore": ["app/javascript/"]
}
],
"import/no-unresolved": [2, {"commonjs": true, "amd": true}],
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
"spaced-comment": [
"error",

View file

@ -8,3 +8,18 @@ export const DESTROY_GLOBAL_ACTIVITIES_DATA = "DESTROY_GLOBAL_ACTIVITIES_DATA";
// users
export const SET_CURRENT_USER = "SET_CURRENT_USER";
export const CHANGE_CURRENT_USER_FULL_NAME = "CHANGE_CURRENT_USER_FULL_NAME";
export const CHANGE_CURRENT_USER_INITIALS = "CHANGE_CURRENT_USER_INITIALS";
export const CHANGE_CURRENT_USER_EMAIL = "CHANGE_CURRENT_USER_EMAIL";
export const CHANGE_CURRENT_USER_PASSWORD = "CHANGE_CURRENT_USER_PASSWORD";
export const CHANGE_CURRENT_USER_AVATAR = "CHANGE_CURRENT_USER_AVATAR";
export const CHANGE_CURRENT_USER_TIMEZONE = "CHANGE_CURRENT_USER_TIMEZONE";
export const CHANGE_ASSIGNMENTS_NOTIFICATION =
"CHANGE_ASSIGNMENTS_NOTIFICATION";
export const CHANGE_ASSIGNMENTS_NOTIFICATION_EMAIL =
"CHANGE_ASSIGNMENTS_NOTIFICATION_EMAIL";
export const CHANGE_RECENT_NOTIFICATION = "CHANGE_RECENT_NOTIFICATION";
export const CHANGE_RECENT_NOTIFICATION_EMAIL =
"CHANGE_RECENT_NOTIFICATION_EMAIL";
export const CHANGE_SYSTEM_MESSAGE_NOTIFICATION_EMAIL =
"CHANGE_SYSTEM_MESSAGE_NOTIFICATION_EMAIL";

View file

@ -1,9 +1,20 @@
export const MAIN_COLOR_BLUE = "#37a0d9";
export const LIGHT_BLUE_COLOR = "#37a0d9";
export const WHITE_COLOR = "#fff";
export const BORDER_GRAY_COLOR = "#d2d2d2";
export const DARK_GRAY_COLOR = "#7a7a7a";
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_BLACK = "#000";
export const COLOR_GRAY_LIGHT_YADCF = "#ccc";
export const ICON_GREEN_COLOR = "#8fd13f";
export const NOTIFICATION_YES = "#5a8921";
export const NOTIFICATION_YES_BORDER = "#4d751c";
export const SIDEBAR_HOVER_GRAY_COLOR = "#D2D2D2";
export const COLOR_ALTO = "#dddddd";
export const COLOR_GRAY = "#909088";
export const COLOR_ALABASTER = "#fcfcfc";

View file

@ -21,3 +21,8 @@ export const RELEASE_NOTES_LINK = "http://scinote.net/docs/release-notes/";
export const PREMIUM_LINK = "http://scinote.net/premium/";
export const CONTACT_US_LINK =
"http://scinote.net/story-of-scinote/#contact-scinote";
// settings
export const SETTINGS_ACCOUNT_PROFILE = "/settings/account/profile";
export const SETTINGS_ACCOUNT_PREFERENCES = "/settings/account/preferences";
export const SETTINGS_TEAMS = "/settings/teams";

View file

@ -1,7 +1,11 @@
export default {
"en-US": {
general: {
close: "Close"
close: "Close",
cancel: "Cancel",
update: "Update",
edit: "Edit",
loading: "Loading ..."
},
navbar: {
page_title: "sciNote",
@ -13,6 +17,44 @@ export default {
notifications_label: "Notifications",
info_label: "Info"
},
settings_page: {
account: "Account",
team: "Team",
avatar: "Avatar",
edit_avatar: "Edit Avatar",
change: "Change",
change_password: "Change Password",
new_email: "New email",
initials: "Initials",
full_name: "Full name",
my_profile: "My Profile",
my_statistics: "My Statistics",
teams: "Teams",
project: "Project",
projects: "Projects",
experiment: "Experiment",
experiments: "Experiments",
protocol: "Protocol",
protocols: "Protocols",
time_zone: "Time zone",
time_zone_warning:
"Time zone setting affects all time & date fields throughout application.",
profile: "Profile",
preferences: "Preferences",
assignement: "Assignement",
assignement_msg:
"Assignment notifications appear whenever you get assigned to a team, project, task.",
recent_changes: "Recent changes",
recent_changes_msg:
"Recent changes notifications appear whenever there is a change on a task you are assigned to.",
system_message: "System message",
system_message_msg:
"System message notifications are specifically sent by site maintainers to notify all users about a system update.",
show_in_scinote: "Show in sciNote",
notify_me_via_email: "Notify me via email",
no: "No",
yes: "Yes"
},
activities: {
modal_title: "Activities",
no_data: "No Data",

View file

@ -0,0 +1,5 @@
import React from "react";
const NotFound = () => <h1>404 Not found</h1>;
export default NotFound;

View file

@ -1,6 +1,19 @@
import axios from "../../app/axios";
import { CURRENT_USER_PATH } from "../../app/routes";
import { SET_CURRENT_USER } from "../../app/action_types";
import {
SET_CURRENT_USER,
CHANGE_CURRENT_USER_FULL_NAME,
CHANGE_CURRENT_USER_INITIALS,
CHANGE_CURRENT_USER_EMAIL,
CHANGE_CURRENT_USER_PASSWORD,
CHANGE_CURRENT_USER_AVATAR,
CHANGE_CURRENT_USER_TIMEZONE,
CHANGE_ASSIGNMENTS_NOTIFICATION,
CHANGE_ASSIGNMENTS_NOTIFICATION_EMAIL,
CHANGE_RECENT_NOTIFICATION,
CHANGE_RECENT_NOTIFICATION_EMAIL,
CHANGE_SYSTEM_MESSAGE_NOTIFICATION_EMAIL
} from "../../app/action_types";
function addCurrentUser(data) {
return {
@ -21,3 +34,80 @@ export function getCurrentUser() {
});
};
}
export function changeFullName(name) {
return {
type: CHANGE_CURRENT_USER_FULL_NAME,
payload: name
};
}
export function changeInitials(initials) {
return {
type: CHANGE_CURRENT_USER_INITIALS,
payload: initials
};
}
export function changeEmail(email) {
return {
type: CHANGE_CURRENT_USER_EMAIL,
payload: email
};
}
export function changePassword(password) {
return {
type: CHANGE_CURRENT_USER_PASSWORD,
payload: password
};
}
export function changeAvatar(avatarSrc) {
return {
type: CHANGE_CURRENT_USER_AVATAR,
payload: avatarSrc
};
}
export function changeTimezone(timezone) {
return {
type: CHANGE_CURRENT_USER_TIMEZONE,
payload: timezone
};
}
export function changeAssignmentsNotification(status) {
return {
type: CHANGE_ASSIGNMENTS_NOTIFICATION,
payload: status
};
}
export function changeAssignmentsNotificationEmail(status) {
return {
type: CHANGE_ASSIGNMENTS_NOTIFICATION_EMAIL,
payload: status
};
}
export function changeRecentNotification(status) {
return {
type: CHANGE_RECENT_NOTIFICATION,
payload: status
};
}
export function changeRecentNotificationEmail(status) {
return {
type: CHANGE_RECENT_NOTIFICATION_EMAIL,
payload: status
};
}
export function changeSystemMessageNotificationEmail(status) {
return {
type: CHANGE_SYSTEM_MESSAGE_NOTIFICATION_EMAIL,
payload: status
};
}

View file

@ -0,0 +1,149 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import ReactDataGrid from 'react-data-grid';
import styled from "styled-components";
import {
WHITE_COLOR,
COLOR_GRAY
} from "../../app/constants/colors";
const StyledReactDataGrid = styled(ReactDataGrid)`
background-color: inherit;
.react-grid-Grid {
background-color: inherit;
}
.react-grid-Header {
.react-grid-HeaderCell {
background-color: ${COLOR_GRAY};
color: ${WHITE_COLOR};
font-weight: normal;
vertical-align: bottom;
&:first-child {
border-left: none;
}
}
}
.react-grid-Canvas {
background-color: inherit;
}
`;
class DataGrid extends Component {
constructor(props) {
super(props);
this.cleanProps = this.cleanProps.bind(this);
this.transformColumns = this.transformColumns.bind(this);
this.setupDefaultProps = this.setupDefaultProps.bind(this);
this.transformColumns();
this.setupDefaultProps();
// Store the original rows array, and make a copy that
// can be used for modifying eg.filtering, sorting
this.state = {
originalRows: this.props.data,
rows: this.props.data.slice(0)
};
}
setupDefaultProps() {
// Setup the default props if they're not provided
if ('rowGetter' in this.props) {
this._rowGetter = this.props.rowGetter;
} else {
this._rowGetter = ((i) => this.state.rows[i]);
}
this._rowGetter = this._rowGetter.bind(this);
if ('rowsCount' in this.props) {
this._rowsCount = this.props.rowsCount;
} else {
this._rowsCount = this.props.data.length;
}
if ('onGridSort' in this.props) {
this._onGridSort = this.props.onGridSort;
} else {
this._onGridSort = ((sortColumn, sortDirection) => {
const comparer = (a, b) => {
if (sortDirection === 'ASC') {
return (a[sortColumn] > b[sortColumn]) ? 1 : -1;
} else if (sortDirection === 'DESC') {
return (a[sortColumn] < b[sortColumn]) ? 1 : -1;
}
return 0;
};
let rows;
if (sortDirection === 'NONE') {
rows = this.state.originalRows.slice(0);
} else {
rows = this.state.rows.sort(comparer);
}
this.setState({ rows });
});
}
this._onGridSort = this._onGridSort.bind(this);
}
transformColumns() {
// Transform columns from the "sciNote" representation into
// ReactDataGrid-compatible representation
this._columns =
this.props.columns
.sort((a, b) => a.position - b.position)
.filter((col) => (!('visible' in col) || col.visible))
.map((col) => ({
key: col.textId,
name: col.name,
locked: col.locked,
sortable: col.sortable
}));
}
cleanProps() {
// Remove additional props from the props value
const {
columns, rowGetter, rowsCount, ...cleanProps
} = this.props;
return cleanProps;
}
render() {
return (
<StyledReactDataGrid
columns={this._columns}
rowGetter={this._rowGetter}
rowsCount={this._rowsCount}
onGridSort={this._onGridSort}
{...this.cleanProps()}
/>
);
}
}
DataGrid.propTypes = {
columns: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
isKey: PropTypes.bool.isRequired,
textId: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
position: PropTypes.number.isRequired,
visible: PropTypes.bool,
sortable: PropTypes.bool,
locked: PropTypes.bool
})
).isRequired,
data: PropTypes.arrayOf(PropTypes.object).isRequired,
rowGetter: PropTypes.func,
rowsCount: PropTypes.number,
onGridSort: PropTypes.func
};
export default DataGrid;

View file

@ -0,0 +1,111 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table';
import styled from "styled-components";
import {
WHITE_COLOR,
COLOR_GRAY,
COLOR_ALTO,
COLOR_ALABASTER
} from "../../app/constants/colors";
const StyledBootstrapTable = styled(BootstrapTable)`
thead {
background-color: ${COLOR_GRAY};
> tr > th,
>tr > td {
padding: 6px;
padding-right: 30px;
}
> tr > th {
border-bottom: 2px solid ${COLOR_ALTO};
border-bottom-width: 0;
border-left: 2px solid ${COLOR_ALABASTER};
color: ${WHITE_COLOR};
font-weight: normal;
vertical-align: bottom;
&:first-child {
border-left: none;
}
}
}
td, th {
box-sizing: content-box;
}
td {
overflow-wrap: break-word;
text-overflow: ellipsis;
word-break: break-word;
}
`;
class DataTable extends Component {
static cleanColumnAttributes(col) {
// Remove additional attributes from the columns
const {
id, isKey, textId, name, position, visible,
sortable, locked, ...cleanCol
} = col;
return cleanCol;
}
constructor(props) {
super(props);
this.cleanProps = this.cleanProps.bind(this);
this.displayHeader = this.displayHeader.bind(this);
}
cleanProps() {
// Remove additional props from the props value
const {columns, ...cleanProps} = this.props;
return cleanProps;
}
displayHeader() {
const orderedCols = this.props.columns.sort((a, b) => a.position - b.position);
return orderedCols.map((col) =>
<TableHeaderColumn
key={col.id}
dataField={col.textId}
isKey={col.isKey}
hidden={('visible' in col) && !col.visible}
dataSort={col.sortable}
{...DataTable.cleanColumnAttributes(col)}
>
{col.name}
</TableHeaderColumn>
);
}
render() {
return (
<StyledBootstrapTable {...this.cleanProps()}>
{this.displayHeader()}
</StyledBootstrapTable>
);
}
}
DataTable.propTypes = {
columns: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
isKey: PropTypes.bool.isRequired,
textId: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
position: PropTypes.number.isRequired,
visible: PropTypes.bool,
sortable: PropTypes.bool,
locked: PropTypes.bool
})
).isRequired,
data: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default DataTable;

View file

@ -1,11 +1,71 @@
import { SET_CURRENT_USER } from "../../app/action_types";
import {
SET_CURRENT_USER,
CHANGE_CURRENT_USER_FULL_NAME,
CHANGE_CURRENT_USER_INITIALS,
CHANGE_CURRENT_USER_EMAIL,
CHANGE_CURRENT_USER_PASSWORD,
CHANGE_CURRENT_USER_AVATAR,
CHANGE_CURRENT_USER_TIMEZONE,
CHANGE_ASSIGNMENTS_NOTIFICATION,
CHANGE_ASSIGNMENTS_NOTIFICATION_EMAIL,
CHANGE_RECENT_NOTIFICATION,
CHANGE_RECENT_NOTIFICATION_EMAIL,
CHANGE_SYSTEM_MESSAGE_NOTIFICATION_EMAIL
} from "../../app/action_types";
export function currentUser(
state = { id: 0, fullName: "", avatarPath: "" },
state = {
id: 0,
fullName: "",
initials: "",
email: "",
avatarPath: "",
avatarThumbPath: "",
timezone: "",
assignmentsNotification: false,
assignmentsNotificationEmail: false,
recentNotification: false,
recentNotificationEmail: false,
systemMessageNotificationEmail: false
},
action
) {
if (action.type === SET_CURRENT_USER) {
return Object.assign({}, state, action.payload);
switch (action.type) {
case SET_CURRENT_USER:
return Object.assign({}, state, action.payload);
case CHANGE_CURRENT_USER_FULL_NAME:
return Object.assign({}, state, { fullName: action.payload });
case CHANGE_CURRENT_USER_INITIALS:
return Object.assign({}, state, { initials: action.payload });
case CHANGE_CURRENT_USER_EMAIL:
return Object.assign({}, state, { email: action.payload });
case CHANGE_CURRENT_USER_PASSWORD:
console.log("handle sending password to the server");
// return Object.assign({}, state, { password: action.payload });
return state;
case CHANGE_CURRENT_USER_AVATAR:
return Object.assign({}, state, { avatar: action.payload });
case CHANGE_CURRENT_USER_TIMEZONE:
return Object.assign({}, state, { timezone: action.payload });
case CHANGE_ASSIGNMENTS_NOTIFICATION:
return Object.assign({}, state, {
assignmentsNotification: action.payload
});
case CHANGE_ASSIGNMENTS_NOTIFICATION_EMAIL:
return Object.assign({}, state, {
assignmentsNotificationEmail: action.payload
});
case CHANGE_RECENT_NOTIFICATION:
return Object.assign({}, state, { recentNotification: action.payload });
case CHANGE_RECENT_NOTIFICATION_EMAIL:
return Object.assign({}, state, {
recentNotificationEmail: action.payload
});
case CHANGE_SYSTEM_MESSAGE_NOTIFICATION_EMAIL:
return Object.assign({}, state, {
systemMessageNotificationEmail: action.payload
});
default:
return state;
}
return state;
}

View file

@ -1,22 +1,22 @@
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import { Provider } from "react-redux";
import { IntlProvider } from "react-intl";
import { addLocaleData } from "react-intl";
import { IntlProvider, addLocaleData } from "react-intl";
import enLocaleData from "react-intl/locale-data/en";
import { flattenMessages } from "../../locales/utils";
import store from "../../app/store";
import messages from "../../locales/messages";
import Navigation from "../../shared/navigation";
import MainNav from "./components/MainNav";
addLocaleData([...enLocaleData]);
const locale = "en-US";
const SettingsPage = () =>
<div>
<Navigation page="Settings" />
....
<MainNav />
</div>;
document.addEventListener("DOMContentLoaded", () => {
@ -26,7 +26,11 @@ document.addEventListener("DOMContentLoaded", () => {
locale={locale}
messages={flattenMessages(messages[locale])}
>
<SettingsPage />
<div>
<BrowserRouter>
<SettingsPage />
</BrowserRouter>
</div>
</IntlProvider>
</Provider>,
document.getElementById("root")

View file

@ -0,0 +1,67 @@
import React, { Component } from "react";
import { Route, Switch, Redirect } from "react-router-dom";
import { LinkContainer } from "react-router-bootstrap";
import { Nav, NavItem } from "react-bootstrap";
import { FormattedMessage } from "react-intl";
import Navigation from "../../../shared/navigation";
import { SETTINGS_ACCOUNT_PROFILE, SETTINGS_TEAMS } from "../../../app/routes";
import NotFound from "../../../shared/404/NotFound";
import SettingsAccount from ".././components/account/SettingsAccount";
import SettingsTeams from ".././components/team/SettingsTeams";
export default class MainNav extends Component {
constructor(props) {
super(props);
this.state = {
active: "1"
};
this.handleSelect = this.handleSelect.bind(this);
}
handleSelect(eventKey) {
event.preventDefault();
this.setState({ active: eventKey });
}
render() {
return (
<div>
<Navigation page="Settings" />
<div className="container">
<Nav bsStyle="tabs" activeKey="1" onSelect={this.handleSelect}>
<LinkContainer
active={this.state.active === "1"}
to={SETTINGS_ACCOUNT_PROFILE}
>
<NavItem eventKey="1">
<FormattedMessage id="settings_page.account" />
</NavItem>
</LinkContainer>
<LinkContainer
to={SETTINGS_TEAMS}
active={this.state.active === "2"}
>
<NavItem eventKey="2">
<FormattedMessage id="settings_page.team" />
</NavItem>
</LinkContainer>
</Nav>
<Switch>
<Route exact path="/" component={SettingsAccount} />
<Route
exact
path="/settings"
render={() => <Redirect to="/settings/account/profile" />}
/>
<Route path="/settings/account" component={SettingsAccount} />
<Route path="/settings/teams" component={SettingsTeams} />
<Route component={NotFound} />
</Switch>
</div>
</div>
);
}
}

View file

@ -0,0 +1,48 @@
import React from "react";
import { string, func } from "prop-types";
import { FormGroup, FormControl, ControlLabel, Button } from "react-bootstrap";
import styled from "styled-components";
import { FormattedMessage } from "react-intl";
const Wrapper = styled.div`margin-top: 19px;`;
const MyFormControl = styled(FormControl)`
width: 160px;
display: inline;
`;
const MyButton = styled(Button)`
border-radius: 0 50px 50px 0;
margin-top: -3px;
margin-left: -3px;
`;
const InputDisabled = props =>
<Wrapper>
<form>
<FormGroup>
<ControlLabel>
<FormattedMessage id={props.labelTitle} />
</ControlLabel>
<FormGroup>
<MyFormControl
type={props.inputType}
value={props.inputValue}
disabled
/>
<MyButton bsStyle="default" onClick={props.enableEdit}>
<FormattedMessage id="general.edit" />
</MyButton>
</FormGroup>
</FormGroup>
</form>
</Wrapper>;
InputDisabled.propTypes = {
labelTitle: string.isRequired,
inputType: string.isRequired,
inputValue: string.isRequired,
enableEdit: func.isRequired
};
export default InputDisabled;

View file

@ -0,0 +1,152 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import styled from "styled-components";
import { FormattedMessage } from "react-intl";
import { FormGroup, FormControl, ControlLabel, Button } from "react-bootstrap";
import { BORDER_LIGHT_COLOR } from "../../../../app/constants/colors";
const StyledInputEnabled = styled.div`
border: 1px solid ${BORDER_LIGHT_COLOR};
padding: 19px;
margin: 20px 0;
input {
margin-bottom: 15px;
}
`;
const ErrorMsg = styled.div`color: red;`;
class InputEnabled extends Component {
constructor(props) {
super(props);
if (props.inputType === "password") {
this.state = {
value: "",
value2: ""
};
} else {
this.state = {
value: this.props.inputValue
};
}
this.handleChange = this.handleChange.bind(this);
this.handleChange2 = this.handleChange2.bind(this);
this.handleUpdate = this.handleUpdate.bind(this);
}
handleChange(event) {
this.setState({ value: event.target.value });
}
handleChange2(event) {
this.setState({ value2: event.target.value });
}
handleSubmit(event) {
event.preventDefault();
}
handleUpdate() {
this.props.saveData(this.state.value);
this.props.disableEdit();
}
confirmationField() {
let inputs;
const type = this.props.inputType;
if (type === "email" || type === "password") {
inputs = (
<div>
<p>
Current password (we need your current password to confirm your
changes)
</p>
<FormControl type="password" />
</div>
);
}
return inputs;
}
errorMsg() {
return this.state.value !== this.state.value2
? <ErrorMsg>Passwords do not match!</ErrorMsg>
: "";
}
inputField() {
let input;
if (this.props.inputType === "password") {
input = (
<div>
<FormControl
type={this.props.inputType}
value={this.state.value}
onChange={this.handleChange}
/>
<p>New password confirmation</p>
<FormControl
type={this.props.inputType}
value={this.state.value2}
onChange={this.handleChange2}
/>
{this.errorMsg()}
</div>
);
} else {
input = (
<FormControl
type={this.props.inputType}
value={this.state.value}
onChange={this.handleChange}
/>
);
}
return input;
}
render() {
return (
<StyledInputEnabled>
<form onSubmit={this.handleSubmit}>
<FormGroup>
<h4>
<FormattedMessage id="settings_page.change" />{" "}
<FormattedMessage id={this.props.labelTitle} />
</h4>
{this.confirmationField()}
<ControlLabel>
{this.props.labelValue}
</ControlLabel>
{this.inputField()}
<Button bsStyle="primary" onClick={this.props.disableEdit}>
<FormattedMessage id="general.cancel" />
</Button>
<Button bsStyle="default" onClick={this.handleUpdate}>
<FormattedMessage id="general.update" />
</Button>
</FormGroup>
</form>
</StyledInputEnabled>
);
}
}
InputEnabled.propTypes = {
inputType: PropTypes.string.isRequired,
labelValue: PropTypes.string.isRequired,
inputValue: PropTypes.string.isRequired,
disableEdit: PropTypes.func.isRequired,
saveData: PropTypes.func.isRequired,
labelTitle: PropTypes.string.isRequired
};
export default InputEnabled;

View file

@ -0,0 +1,39 @@
import React, { Component } from "react";
import { Route, Switch } from "react-router-dom";
import styled from "styled-components";
import SettingsLeftTab from "./SettingsLeftTab";
import SettingsProfile from "./profile/SettingsProfile";
import SettingsPreferences from "./preferences/SettingsPreferences";
import { BORDER_LIGHT_COLOR } from "../../../../app/constants/colors";
const Wrapper = styled.div`
background: white;
box-sizing: border-box;
border: 1px solid ${BORDER_LIGHT_COLOR};
border-top: none;
margin: 0;
padding: 16px 0 50px 0;
`;
class SettingsAccount extends Component {
render() {
return (
<Wrapper className="row">
<div className="col-xs-12 col-sm-3">
<SettingsLeftTab />
</div>
<Switch>
<Route path="/settings/account/profile" component={SettingsProfile} />
<Route
path="/settings/account/preferences"
component={SettingsPreferences}
/>
</Switch>
</Wrapper>
);
}
}
export default SettingsAccount;

View file

@ -0,0 +1,61 @@
import React, { Component } from "react";
import { Nav, NavItem } from "react-bootstrap";
import { LinkContainer } from "react-router-bootstrap";
import styled from "styled-components";
import { FormattedMessage } from "react-intl";
import {
SETTINGS_ACCOUNT_PROFILE,
SETTINGS_ACCOUNT_PREFERENCES
} from "../../../../app/routes";
import {
SIDEBAR_HOVER_GRAY_COLOR,
LIGHT_BLUE_COLOR
} from "../../../../app/constants/colors";
const MyLinkContainer = styled(LinkContainer)`
a {
color: ${LIGHT_BLUE_COLOR};
padding-left: 0;
}
&.active > a:after {
content: '';
position: absolute;
left: 100%;
top: 50%;
margin-top: -19px;
border-top: 19px solid transparent;
border-left: 13px solid ${LIGHT_BLUE_COLOR};
border-bottom: 19px solid transparent;
}
a:hover {
background-color: ${SIDEBAR_HOVER_GRAY_COLOR} !important;
}
&.active {
a {
background-color: ${LIGHT_BLUE_COLOR} !important;
border-radius: 3px 0 0 3px;
border-left: 13px solid ${LIGHT_BLUE_COLOR};
border-radius: 3px 0 0 3px;
}
}
`;
const SettingsLeftTab = () =>
<Nav bsStyle="pills" stacked activeKey={1}>
<MyLinkContainer to={SETTINGS_ACCOUNT_PROFILE}>
<NavItem>
<FormattedMessage id="settings_page.profile" />
</NavItem>
</MyLinkContainer>
<MyLinkContainer to={SETTINGS_ACCOUNT_PREFERENCES}>
<NavItem>
<FormattedMessage id="settings_page.preferences" />
</NavItem>
</MyLinkContainer>
</Nav>;
export default SettingsLeftTab;

View file

@ -0,0 +1,3 @@
export const ASSIGNMENT_NOTIFICATION = "ASSIGNMENT";
export const RECENT_NOTIFICATION = "RECENT_NOTIFICATION";
export const SYSTEM_NOTIFICATION = "SYSTEM_NOTIFICATION";

View file

@ -0,0 +1,83 @@
import React, { Component } from "react";
import PropType from "prop-types";
import { Button } from "react-bootstrap";
import styled from "styled-components";
import TimezonePicker from "react-bootstrap-timezone-picker";
import "react-bootstrap-timezone-picker/dist/react-bootstrap-timezone-picker.min.css";
import { FormattedMessage } from "react-intl";
import { BORDER_LIGHT_COLOR } from "../../../../../app/constants/colors";
const Wrapper = styled.div`
border: 1px solid ${BORDER_LIGHT_COLOR};
padding: 19px;
margin: 20px 0;
input {
margin-bottom: 3px;
}
.settings-warning {
margin-bottom: 15px;
}
`;
class InputTimezone extends Component {
constructor(props) {
super(props);
this.state = {
value: props.inputValue
};
this.handleChange = this.handleChange.bind(this);
this.handleUpdate = this.handleUpdate.bind(this);
}
handleChange(timezone) {
this.setState({ value: timezone });
}
handleUpdate() {
if (this.state.value !== "") {
this.props.saveData(this.state.value);
}
this.props.disableEdit();
}
render() {
return (
<Wrapper>
<h4>
{this.props.labelValue}
</h4>
<TimezonePicker
absolute
defaultValue="Europe/London"
value={this.props.inputValue}
placeholder="Select timezone..."
onChange={this.handleChange}
/>
<div className="settings-warning">
<small>
<FormattedMessage id="settings_page.time_zone_warning" />
</small>
</div>
<Button bsStyle="primary" onClick={this.props.disableEdit}>
Cancel
</Button>
<Button bsStyle="default" onClick={this.handleUpdate}>
Update
</Button>
</Wrapper>
);
}
}
InputTimezone.propTypes = {
labelValue: PropType.string.isRequired,
inputValue: PropType.string.isRequired,
disableEdit: PropType.func.isRequired,
saveData: PropType.func.isRequired
};
export default InputTimezone;

View file

@ -0,0 +1,92 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import styled from "styled-components";
import { FormattedMessage } from "react-intl";
import NotificationsSwitchGroup from "./NotificationsSwitchGroup";
import { WHITE_COLOR } from "../../../../../app/constants/colors";
const Wrapper = styled.div`margin-bottom: 6px;`;
const IconWrapper = styled.div`
margin-top: 12px;
margin-left: 7px;
`;
const Icon = styled.span`
border-radius: 50%;
color: ${WHITE_COLOR};
display: block;
font-size: 15px;
height: 30px;
margin-right: 15px;
padding: 7px;
padding-bottom: 5px;
padding-top: 5px;
width: 30px;
`;
const Image = styled.span`
border-radius: 50%;
color: ${WHITE_COLOR};
display: block;
font-size: 15px;
height: 30px;
margin-right: 15px;
width: 30px;
overflow: hidden;
`;
class NotificationsGroup extends Component {
constructor(props) {
super(props);
}
render() {
let imgOrIcon;
if (this.props.imgUrl === "") {
imgOrIcon = (
<Icon style={{ backgroundColor: this.props.iconBackground }}>
<i className={this.props.iconClasses} />
</Icon>
);
} else {
imgOrIcon = (
<Image>
<img src={this.props.imgUrl} alt="default avatar" />
</Image>
);
}
return (
<Wrapper className="row">
<IconWrapper className="col-sm-1">
{imgOrIcon}
</IconWrapper>
<div className="col-sm-10">
<h5>
<strong>
<FormattedMessage id={this.props.title} />
</strong>
</h5>
<p>
<FormattedMessage id={this.props.subtitle} />
</p>
<NotificationsSwitchGroup type={this.props.type} />
</div>
</Wrapper>
);
}
}
NotificationsGroup.propTypes = {
title: PropTypes.string.isRequired,
subtitle: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
imgUrl: PropTypes.string.isRequired,
iconClasses: PropTypes.string.isRequired,
iconBackground: PropTypes.string.isRequired
};
export default NotificationsGroup;

View file

@ -0,0 +1,103 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import styled from "styled-components";
import { FormattedMessage } from "react-intl";
import {
WHITE_COLOR,
NOTIFICATION_YES,
NOTIFICATION_YES_BORDER
} from "../../../../../app/constants/colors";
const Wrapper = styled.div`margin-top: 13px;`;
const LeftButton = styled.button`border-radius: 50px 0 0 50px;`;
const RightButton = styled.button`
border-radius: 0 50px 50px 0;
&.btn-primary {
color: ${WHITE_COLOR};
border-color: ${NOTIFICATION_YES_BORDER};
background-color: ${NOTIFICATION_YES};
&:hover {
background-color: ${NOTIFICATION_YES_BORDER};
}
}
`;
class NotificationsSwitch extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
if (!this.props.isDisabled) {
this.props.toggleSwitch();
}
}
render() {
let switchBtn;
if (this.props.isSwitchOn) {
switchBtn = (
<div className="btn-group">
<LeftButton
className="btn btn-default"
disabled={this.props.isDisabled}
onClick={this.handleClick}
>
<FormattedMessage id="settings_page.no" />
</LeftButton>
<RightButton
className="btn btn-primary"
disabled
onClick={this.handleClick}
>
<FormattedMessage id="settings_page.yes" />
</RightButton>
</div>
);
} else {
switchBtn = (
<div className="btn-group">
<LeftButton
className="btn btn-danger"
disabled
onClick={this.handleClick}
>
<FormattedMessage id="settings_page.no" />
</LeftButton>
<RightButton
className="btn btn-default"
disabled={this.props.isDisabled}
onClick={this.handleClick}
>
<FormattedMessage id="settings_page.yes" />
</RightButton>
</div>
);
}
return (
<Wrapper className="row">
<div className="col-sm-4 col-sm-offset-1">
<FormattedMessage id={this.props.title} />
</div>
<div className="col-sm-7">
{switchBtn}
</div>
</Wrapper>
);
}
}
NotificationsSwitch.propTypes = {
title: PropTypes.string.isRequired,
isSwitchOn: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool.isRequired,
toggleSwitch: PropTypes.func.isRequired
};
export default NotificationsSwitch;

View file

@ -0,0 +1,157 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import { string, bool, func } from "prop-types";
import NotificationsSwitch from "./NotificationsSwitch";
import {
ASSIGNMENT_NOTIFICATION,
RECENT_NOTIFICATION,
SYSTEM_NOTIFICATION
} from "../constants";
import {
changeAssignmentsNotification,
changeAssignmentsNotificationEmail,
changeRecentNotification,
changeRecentNotificationEmail,
changeSystemMessageNotificationEmail
} from "../../../../../shared/actions/UsersActions";
class NotificationsSwitchGroup extends Component {
constructor(props) {
super(props);
this.state = {
isSciNoteSwitchOn: false,
isEmailSwitchOn: false
};
this.toggleFirstSwitch = this.toggleFirstSwitch.bind(this);
this.toggleSecondSwitch = this.toggleSecondSwitch.bind(this);
this.isSwitchDisabled = this.isSwitchDisabled.bind(this);
}
componentWillMount() {
switch (this.props.type) {
case ASSIGNMENT_NOTIFICATION:
this.setState({
isSciNoteSwitchOn: this.props.assignmentsNotification,
isEmailSwitchOn: this.props.assignmentsNotificationEmail,
sciNoteDispatch: state =>
this.props.changeAssignmentsNotification(state),
emailDispatch: state =>
this.props.changeAssignmentsNotificationEmail(state)
});
break;
case RECENT_NOTIFICATION:
this.setState({
isSciNoteSwitchOn: this.props.recentNotification,
isEmailSwitchOn: this.props.recentNotificationEmail,
sciNoteDispatch: state => this.props.changeRecentNotification(state),
emailDispatch: state =>
this.props.changeRecentNotificationEmail(state)
});
break;
case SYSTEM_NOTIFICATION:
this.setState({
isSciNoteSwitchOn: true,
isEmailSwitchOn: this.props.systemMessageNotificationEmail,
sciNoteDispatch: state => `${state}: Do Nothing`,
emailDispatch: state =>
this.props.changeSystemMessageNotificationEmail(state)
});
break;
default:
this.setState({
isSciNoteSwitchOn: true,
isEmailSwitchOn: false
});
}
}
toggleFirstSwitch() {
if (this.state.isSciNoteSwitchOn) {
this.setState({ isSciNoteSwitchOn: false, isEmailSwitchOn: false });
this.state.sciNoteDispatch(false);
this.state.emailDispatch(false);
} else {
this.setState({ isSciNoteSwitchOn: true });
this.state.sciNoteDispatch(true);
}
}
toggleSecondSwitch() {
if (this.state.isEmailSwitchOn) {
this.setState({ isEmailSwitchOn: false });
this.state.emailDispatch(false);
} else {
this.setState({ isEmailSwitchOn: true });
this.state.emailDispatch(true);
}
}
isSwitchDisabled() {
if (this.props.type === SYSTEM_NOTIFICATION) {
return true;
}
return false;
}
render() {
return (
<div>
<NotificationsSwitch
title="settings_page.show_in_scinote"
isSwitchOn={this.state.isSciNoteSwitchOn}
toggleSwitch={this.toggleFirstSwitch}
isDisabled={this.isSwitchDisabled()}
/>
<NotificationsSwitch
title="settings_page.notify_me_via_email"
isSwitchOn={this.state.isEmailSwitchOn}
toggleSwitch={this.toggleSecondSwitch}
isDisabled={!this.state.isSciNoteSwitchOn}
/>
</div>
);
}
}
NotificationsSwitchGroup.propTypes = {
type: string.isRequired,
assignmentsNotification: bool.isRequired,
assignmentsNotificationEmail: bool.isRequired,
recentNotification: bool.isRequired,
recentNotificationEmail: bool.isRequired,
systemMessageNotificationEmail: bool.isRequired,
changeAssignmentsNotification: func.isRequired,
changeAssignmentsNotificationEmail: func.isRequired,
changeRecentNotification: func.isRequired,
changeRecentNotificationEmail: func.isRequired,
changeSystemMessageNotificationEmail: func.isRequired
};
const mapStateToProps = state => state.current_user;
const mapDispatchToProps = dispatch => ({
changeAssignmentsNotification(status) {
dispatch(changeAssignmentsNotification(status));
},
changeAssignmentsNotificationEmail(status) {
dispatch(changeAssignmentsNotificationEmail(status));
},
changeRecentNotification(status) {
dispatch(changeRecentNotification(status));
},
changeRecentNotificationEmail(status) {
dispatch(changeRecentNotificationEmail(status));
},
changeSystemMessageNotificationEmail(status) {
dispatch(changeSystemMessageNotificationEmail(status));
}
});
export default connect(mapStateToProps, mapDispatchToProps)(
NotificationsSwitchGroup
);

View file

@ -0,0 +1,128 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import styled from "styled-components";
import { FormattedMessage } from "react-intl";
import InputDisabled from "../InputDisabled";
import InputTimezone from "./InputTimezone";
import { changeTimezone } from "../../../../../shared/actions/UsersActions";
import NotificationsGroup from "./NotificationsGroup";
import {
ASSIGNMENT_NOTIFICATION,
RECENT_NOTIFICATION,
SYSTEM_NOTIFICATION
} from "../constants";
import {
MAIN_COLOR_BLUE,
ICON_GREEN_COLOR,
BORDER_LIGHT_COLOR
} from "../../../../../app/constants/colors";
const WrapperInputDisabled = styled.div`
margin: 20px 0;
padding-bottom: 15px;
border-bottom: 1px solid ${BORDER_LIGHT_COLOR};
.settings-warning {
margin-top: -5px;
}
`;
class SettingsPreferences extends Component {
constructor(props) {
super(props);
this.state = {
isTimeZoneEditable: false
};
}
toggleIsEditable(fieldNameEnabled) {
const editableState = this.state[fieldNameEnabled];
this.setState({ [fieldNameEnabled]: !editableState });
}
render() {
const isTimeZoneEditable = "isTimeZoneEditable";
let timezoneField;
if (this.state.isTimeZoneEditable) {
timezoneField = (
<InputTimezone
labelValue="Time zone"
inputValue={this.props.timezone}
disableEdit={() => this.toggleIsEditable(isTimeZoneEditable)}
saveData={timeZone => this.props.changeTimezone(timeZone)}
/>
);
} else {
timezoneField = (
<WrapperInputDisabled>
<InputDisabled
labelTitle="settings_page.time_zone"
inputValue={this.props.timezone}
inputType="text"
enableEdit={() => this.toggleIsEditable(isTimeZoneEditable)}
/>
<div className="settings-warning">
<small>
<FormattedMessage id="settings_page.time_zone_warning" />
</small>
</div>
</WrapperInputDisabled>
);
}
return (
<div className="col-xs-12 col-sm-9">
{timezoneField}
<h3>Notifications</h3>
<NotificationsGroup
type={ASSIGNMENT_NOTIFICATION}
title="settings_page.assignement"
subtitle="settings_page.assignement_msg"
imgUrl=""
iconClasses="fa fa-newspaper-o"
iconBackground={MAIN_COLOR_BLUE}
/>
<NotificationsGroup
type={RECENT_NOTIFICATION}
title="settings_page.recent_changes"
subtitle="settings_page.recent_changes_msg"
imgUrl={this.props.avatarPath}
iconClasses=""
iconBackground=""
/>
<NotificationsGroup
type={SYSTEM_NOTIFICATION}
title="settings_page.system_message"
subtitle="settings_page.system_message_msg"
imgUrl=""
iconClasses="glyphicon glyphicon-tower"
iconBackground={ICON_GREEN_COLOR}
/>
</div>
);
}
}
SettingsPreferences.propTypes = {
timezone: PropTypes.string.isRequired,
changeTimezone: PropTypes.func.isRequired,
avatarPath: PropTypes.string.isRequired
};
const mapStateToProps = state => state.current_user;
const mapDispatchToProps = dispatch => ({
changeTimezone(timezone) {
dispatch(changeTimezone(timezone));
}
});
export default connect(mapStateToProps, mapDispatchToProps)(
SettingsPreferences
);

View file

@ -0,0 +1,42 @@
import React from "react";
import PropTypes, { string } from "prop-types";
import styled from "styled-components";
import { FormattedMessage } from "react-intl";
import {
WHITE_COLOR,
DARK_GRAY_COLOR
} from "../../../../../app/constants/colors";
const AvatarWrapper = styled.div`
width: 100px;
height: 100px;
position: relative;
cursor: pointer;
`;
const EditAvatar = styled.span`
color: ${WHITE_COLOR};
background-color: ${DARK_GRAY_COLOR};
position: absolute;
left: 0;
bottom: 0;
width: 100%;
opacity: 0.7;
padding: 5px;
`;
const Avatar = props =>
<AvatarWrapper onClick={props.enableEdit}>
<img src={props.imgSource} alt="default avatar" />
<EditAvatar className="text-center">
<span className="glyphicon glyphicon-pencil" />
<FormattedMessage id="settings_page.edit_avatar" />
</EditAvatar>
</AvatarWrapper>;
Avatar.propTypes = {
imgSource: string.isRequired,
enableEdit: PropTypes.func.isRequired
};
export default Avatar;

View file

@ -0,0 +1,214 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import styled from "styled-components";
import { FormattedMessage } from "react-intl";
import Avatar from "./Avatar";
import InputDisabled from "../InputDisabled";
import InputEnabled from "../InputEnabled";
import {
changeFullName,
changeInitials,
changeEmail,
changePassword,
changeAvatar
} from "../../../../../shared/actions/UsersActions";
const AvatarLabel = styled.h4`
margin-top: 15px;
font-size: 13px;
font-weight: 700;
`;
class MyProfile extends Component {
constructor(props) {
super(props);
this.state = {
isFullNameEditable: false,
areInitialsEditable: false,
isEmailEditable: false,
isPasswordEditable: false,
isAvatarEditable: false
};
this.toggleIsEditable = this.toggleIsEditable.bind(this);
}
toggleIsEditable(fieldNameEnabled) {
const editableState = this.state[fieldNameEnabled];
this.setState({ [fieldNameEnabled]: !editableState });
}
render() {
const areInitialsEditable = "areInitialsEditable";
const isFullNameEditable = "isFullNameEditable";
const isEmailEditable = "isEmailEditable";
const isPasswordEditable = "isPasswordEditable";
const isAvatarEditable = "isAvatarEditable";
let fullNameField;
let initialsField;
let emailField;
let passwordField;
let avatarField;
if (this.state.isAvatarEditable) {
avatarField = (
<InputEnabled
labelTitle="settings_page.avatar"
labelValue="Avatar"
inputType="file"
inputValue=""
disableEdit={() => this.toggleIsEditable(isAvatarEditable)}
saveData={avatarSrc => this.props.changeAvatar(avatarSrc)}
/>
);
} else {
avatarField = (
<Avatar
imgSource={this.props.avatarThumbPath}
enableEdit={() => this.toggleIsEditable(isAvatarEditable)}
/>
);
}
if (this.state.isPasswordEditable) {
passwordField = (
<InputEnabled
labelTitle="settings_page.change_password"
labelValue="Change password"
inputType="password"
inputValue=""
disableEdit={() => this.toggleIsEditable(isPasswordEditable)}
saveData={newPassword => this.props.changePassword(newPassword)}
/>
);
} else {
passwordField = (
<InputDisabled
labelTitle="settings_page.change_password"
inputType="password"
inputValue=""
enableEdit={() => this.toggleIsEditable(isPasswordEditable)}
/>
);
}
if (this.state.isEmailEditable) {
emailField = (
<InputEnabled
labelTitle="settings_page.new_email"
labelValue="New email"
inputType="email"
inputValue={this.props.email}
disableEdit={() => this.toggleIsEditable(isEmailEditable)}
saveData={newEmail => this.props.changeEmail(newEmail)}
/>
);
} else {
emailField = (
<InputDisabled
labelTitle="settings_page.new_email"
inputValue={this.props.email}
inputType="email"
enableEdit={() => this.toggleIsEditable(isEmailEditable)}
/>
);
}
if (this.state.areInitialsEditable) {
initialsField = (
<InputEnabled
labelTitle="settings_page.initials"
labelValue="Initials"
inputType="text"
inputValue={this.props.initials}
disableEdit={() => this.toggleIsEditable(areInitialsEditable)}
saveData={newName => this.props.changeInitials(newName)}
/>
);
} else {
initialsField = (
<InputDisabled
labelTitle="settings_page.initials"
inputValue={this.props.initials}
inputType="text"
enableEdit={() => this.toggleIsEditable(areInitialsEditable)}
/>
);
}
if (this.state.isFullNameEditable) {
fullNameField = (
<InputEnabled
labelTitle="settings_page.full_name"
labelValue="Full name"
inputType="text"
inputValue={this.props.fullName}
disableEdit={() => this.toggleIsEditable(isFullNameEditable)}
saveData={newName => this.props.changeFullName(newName)}
/>
);
} else {
fullNameField = (
<InputDisabled
labelTitle="settings_page.full_name"
inputValue={this.props.fullName}
inputType="text"
enableEdit={() => this.toggleIsEditable(isFullNameEditable)}
/>
);
}
return (
<div>
<h2>
<FormattedMessage id="settings_page.my_profile" />
</h2>
<AvatarLabel>
<FormattedMessage id="settings_page.avatar" />
</AvatarLabel>
{avatarField}
{fullNameField}
{initialsField}
{emailField}
{passwordField}
</div>
);
}
}
MyProfile.propTypes = {
fullName: PropTypes.string.isRequired,
avatarThumbPath: PropTypes.string.isRequired,
initials: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
changeFullName: PropTypes.func.isRequired,
changeInitials: PropTypes.func.isRequired,
changeEmail: PropTypes.func.isRequired,
changePassword: PropTypes.func.isRequired,
changeAvatar: PropTypes.func.isRequired
};
const mapStateToProps = state => state.current_user;
const mapDispatchToProps = dispatch => ({
changeFullName(name) {
dispatch(changeFullName(name));
},
changeInitials(initials) {
dispatch(changeInitials(initials));
},
changeEmail(email) {
dispatch(changeEmail(email));
},
changePassword(password) {
dispatch(changePassword(password));
},
changeAvatar(avatarSrc) {
dispatch(changeAvatar(avatarSrc));
}
});
export default connect(mapStateToProps, mapDispatchToProps)(MyProfile);

View file

@ -0,0 +1,81 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import styled from "styled-components";
import { FormattedMessage } from "react-intl";
import MyStatisticsBox from "./MyStatisticsBox";
const Wrapper = styled.div`
margin-left: -15px;
width: 260px;
`;
class MyStatistics extends Component {
render() {
const stats = this.props.statistics;
const statBoxes = () => {
let boxes = (
<div>
<FormattedMessage id="general.loading" />
</div>
);
if (stats) {
boxes = (
<Wrapper>
<MyStatisticsBox
typeLength={stats.number_of_teams}
plural="settings_page.teams"
singular="settings_page.team"
/>
<MyStatisticsBox
typeLength={stats.number_of_projects}
plural="settings_page.projects"
singular="settings_page.project"
/>
<MyStatisticsBox
typeLength={stats.number_of_experiments}
plural="settings_page.experiments"
singular="settings_page.experiment"
/>
<MyStatisticsBox
typeLength={stats.number_of_protocols}
plural="settings_page.protocols"
singular="settings_page.protocol"
/>
</Wrapper>
);
}
return boxes;
};
return (
<div>
<h2>
<FormattedMessage id="settings_page.my_statistics" />
</h2>
{statBoxes()}
</div>
);
}
}
MyStatistics.defaultProps = {
statistics: null
};
MyStatistics.propTypes = {
statistics: PropTypes.shape({
number_of_teams: PropTypes.number.isRequired,
number_of_projects: PropTypes.number.isRequired,
number_of_experiments: PropTypes.number.isRequired,
number_of_protocols: PropTypes.number.isRequired
})
};
const mapStateToProps = state => state.current_user;
export default connect(mapStateToProps, {})(MyStatistics);

View file

@ -0,0 +1,37 @@
import React from "react";
import styled from "styled-components";
import PropTypes from "prop-types";
import { FormattedMessage } from "react-intl";
import { MAIN_COLOR_BLUE } from "../../../../../app/constants/colors";
const Box = styled.div`
width: 100px;
height: 100px;
color: #fff;
background-color: ${MAIN_COLOR_BLUE};
display: inline-block;
margin: 15px;
text-align: center;
border-radius: 0.25em;
`;
const MyStatisticsBox = props =>
<Box>
<h2>
{props.typeLength}
</h2>
<h5>
{props.typeLength === 1
? <FormattedMessage id={props.singular} />
: <FormattedMessage id={props.plural} />}
</h5>
</Box>;
MyStatisticsBox.propTypes = {
typeLength: PropTypes.number.isRequired,
plural: PropTypes.string.isRequired,
singular: PropTypes.string.isRequired
};
export default MyStatisticsBox;

View file

@ -0,0 +1,16 @@
import React from "react";
import MyProfile from "./MyProfile";
import MyStatistics from "./MyStatistics";
const SettingsProfile = () =>
<div>
<div className="col-xs-12 col-sm-4">
<MyProfile />
</div>
<div className="col-xs-12 col-sm-5">
<MyStatistics />
</div>
</div>;
export default SettingsProfile;

View file

@ -0,0 +1,20 @@
import React from "react";
import styled from "styled-components";
import { BORDER_LIGHT_COLOR } from "../../../../app/constants/colors";
const Wrapper = styled.div`
background: white;
box-sizing: border-box;
border: 1px solid ${BORDER_LIGHT_COLOR};
border-top: none;
margin: 0;
padding: 16px 0 50px 0;
`;
const SettingsTeams = () =>
<Wrapper>
<h1 className="text-center">Settings Teams</h1>
</Wrapper>;
export default SettingsTeams;

View file

@ -35,6 +35,7 @@ $color-candlelight: #ffda23;
$color-orange: #ff900b;
$color-saturated-green: #008600;
$color-blue-yadcf: #337ab7;
$primary-hover-color: #75b22b;
// Red colors
$color-mojo: #cf4b48;

View file

@ -1,12 +1,23 @@
@import 'constants';
@import 'react-bootstrap-timezone-picker/dist/react-bootstrap-timezone-picker.min.css';
@import '~react-bootstrap-table/dist/react-bootstrap-table.min';
body {
background-color: $color-concrete;
color: $color-emperor;
font-family: "Open Sans",Arial,Helvetica,sans-serif;
font-family: "Open Sans", Arial, Helvetica, sans-serif;
font-size: 13px;
}
.label-primary {
background-color: $color-theme-primary;
}
.btn-primary {
background-color: $color-theme-secondary;
border-color: $primary-hover-color;
margin-right: 7px;
&:hover {
background-color: $primary-hover-color;
}
}

View file

@ -1,5 +1,15 @@
json.user do
json.id user.id
json.fullName user.full_name
json.initials user.initials
json.email user.email
json.avatarPath avatar_path(user, :icon_small)
json.avatarThumbPath avatar_path(user, :thumb)
json.statistics user.statistics
json.timezone user.time_zone
json.assignmentsNotification user.assignments_notification
json.assignmentsNotificationEmail user.assignments_notification_email
json.recentNotification user.recent_notification
json.recentNotificationEmail user.recent_notification_email
json.systemMessageNotificationEmail user.system_message_notification_email
end

View file

@ -0,0 +1,3 @@
json.user do
json.fullName user.full_name
end

View file

@ -10,6 +10,7 @@ Rails.application.routes.draw do
# Settings root
get '/settings', to: 'client_api/settings#index'
get '/settings/*all', to: 'client_api/settings#index'
# Client APP endpoints
namespace :client_api, defaults: { format: 'json' } do

9824
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -63,6 +63,7 @@
"rails-erb-loader": "^5.0.2",
"react": "^15.6.1",
"react-bootstrap": "^0.31.1",
"react-bootstrap-timezone-picker": "^1.0.11",
"react-dom": "^15.6.1",
"react-intl": "^2.3.0",
"react-intl-redux": "^0.6.0",
@ -70,6 +71,7 @@
"react-redux": "^5.0.5",
"react-router-bootstrap": "^0.24.2",
"react-router-dom": "^4.1.2",
"react-timezone": "^0.2.0",
"redux": "^3.7.2",
"redux-thunk": "^2.2.0",
"resolve-url-loader": "^2.1.0",
@ -78,6 +80,8 @@
"styled-components": "^2.1.1",
"webpack": "^3.2.0",
"webpack-manifest-plugin": "^1.1.2",
"webpack-merge": "^4.1.0"
"webpack-merge": "^4.1.0",
"react-bootstrap-table": "^4.0.0",
"react-data-grid": "^2.0.2"
}
}

1017
yarn.lock

File diff suppressed because it is too large Load diff