Merge branch 'decoupling-settings-page' into td_SCI_1565

This commit is contained in:
Toni Dezman 2017-08-25 09:06:38 +02:00
commit 989be6563f
20 changed files with 270 additions and 77 deletions

View file

@ -2,9 +2,6 @@ module ClientApi
class TeamsController < ApplicationController
MissingTeamError = Class.new(StandardError)
# TODO remove this when the user authentication will be implemented
skip_before_action :verify_authenticity_token
def index
success_response
end

View file

@ -4,6 +4,7 @@ export const GET_LIST_OF_TEAMS = "GET_LIST_OF_TEAMS";
// activities
export const GLOBAL_ACTIVITIES_DATA = "GLOBAL_ACTIVITIES_DATA";
export const DESTROY_GLOBAL_ACTIVITIES_DATA = "DESTROY_GLOBAL_ACTIVITIES_DATA";
// users
export const SET_CURRENT_USER = "SET_CURRENT_USER";

View file

@ -0,0 +1,7 @@
import axios from "axios";
export default axios.create({
headers: {
"X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').content
}
});

View file

@ -6,6 +6,10 @@ 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";

View file

@ -8,7 +8,14 @@ export default {
loading: "Loading ..."
},
navbar: {
page_title: "sciNote"
page_title: "sciNote",
home_label: "Home",
protocols_label: "Protocols",
repositories_label: "Repositories",
activities_label: "Activities",
search_label: "Search",
notifications_label: "Notifications",
info_label: "Info"
},
settings_page: {
account: "Account",

View file

@ -1,6 +1,9 @@
import axios from "axios";
import axios from "../../app/axios";
import { ACTIVITIES_PATH } from "../../app/routes";
import { GLOBAL_ACTIVITIES_DATA } from "../../app/action_types";
import {
GLOBAL_ACTIVITIES_DATA,
DESTROY_GLOBAL_ACTIVITIES_DATA
} from "../../app/action_types";
function addActivitiesData(data) {
return {
@ -9,9 +12,15 @@ function addActivitiesData(data) {
};
}
export function getActivities(last_id = 0) {
export function destroyActivities() {
return {
type: DESTROY_GLOBAL_ACTIVITIES_DATA
};
}
export function getActivities(lastId = 0) {
return dispatch => {
let path = `${ACTIVITIES_PATH}?from=${last_id}`;
const path = `${ACTIVITIES_PATH}?from=${lastId}`;
axios
.get(path, { withCredentials: true })
.then(response => {

View file

@ -1,4 +1,4 @@
import axios from "axios";
import axios from "../../app/axios";
import _ from "lodash";
import { TEAMS_PATH, CHANGE_TEAM_PATH } from "../../app/routes";
import { GET_LIST_OF_TEAMS, SET_CURRENT_TEAM } from "../../app/action_types";

View file

@ -1,4 +1,4 @@
import axios from "axios";
import axios from "../../app/axios";
import { CURRENT_USER_PATH } from "../../app/routes";
import {
SET_CURRENT_USER,

View file

@ -1,11 +1,34 @@
import React from "react";
import PropTypes from "prop-types";
import { FormattedDate } from "react-intl";
import Moment from "react-moment";
import styled from "styled-components";
import { WHITE_COLOR } from "../../../app/constants/colors"
const StyledLi = styled.li`
margin-bottom: 1em;
`
const StyledSpan = styled.span`
display: inline;
padding: 5px 30px;
font-size: 1em;
font-weight: bold;
line-height: 1;
color: ${WHITE_COLOR};
white-space: nowrap;
vertical-align: baseline;
border-radius: .25em;
`;
const ActivityDateElement = ({ date }) =>
<li className="data-element">
<FormattedDate value={date} day="2-digit" month="2-digit" year="numeric" />
</li>;
<StyledLi className="text-center">
<StyledSpan className="label label-primary">
<Moment format="DD.MM.YYYY">
{date}
</Moment>
</StyledSpan>
</StyledLi>;
ActivityDateElement.propTypes = {
date: PropTypes.instanceOf(Date).isRequired

View file

@ -1,24 +1,54 @@
import React from "react";
import PropTypes from "prop-types";
import { FormattedTime } from "react-intl";
import Moment from "react-moment";
import styled from "styled-components";
import {
WHITE_COLOR,
COLOR_CONCRETE,
BORDER_GRAY_COLOR
} from "../../../app/constants/colors";
const StyledLi = styled.li`
border-radius: .25em;
margin-bottom: 1em;
background-color: ${WHITE_COLOR};
border: 1px solid ${COLOR_CONCRETE};
`;
const TimeSpan = styled.span`
min-width: 150px;
display: table-cell;
vertical-align: middle;
border-top-left-radius: .25em;
border-bottom-left-radius: .25em;
border: 3px solid ${BORDER_GRAY_COLOR};
background-color: ${BORDER_GRAY_COLOR};
padding-left: 10px;
padding-right: 10px;
vertical-align: top;
`;
const TextSpan = styled.span`
display: table-cell;
padding: 3px 10px;
text-align: justify;
`
const ActivityElement = ({ activity }) =>
<li>
<span>
<FormattedTime
value={activity.created_at}
hour="numeric"
minute="numeric"
/>
</span>
<span dangerouslySetInnerHTML={{ __html: activity.message }} />
</li>;
<StyledLi>
<TimeSpan>
<Moment format="HH.mm">
{activity.created_at}
</Moment>
</TimeSpan>
<TextSpan dangerouslySetInnerHTML={{ __html: activity.message }} />
</StyledLi>;
ActivityElement.propTypes = {
activity: PropTypes.shape({
message: PropTypes.string.isRequired,
created_at: PropTypes.string.isRequired
})
}).isRequired
};
export default ActivityElement;

View file

@ -4,10 +4,46 @@ import PropTypes from "prop-types";
import { FormattedMessage } from "react-intl";
import { Modal, Button } from "react-bootstrap";
import _ from "lodash";
import styled from "styled-components";
import { getActivities } from "../../actions/ActivitiesActions";
import ActivityElement from "./ActivityElement";
import ActivityDateElement from "./ActivityDateElement";
import {
WHITE_COLOR,
COLOR_CONCRETE,
COLOR_MINE_SHAFT,
COLOR_GRAY_LIGHT_YADCF
} from "../../../app/constants/colors";
const StyledBottom = styled(Button)`
display: inline-block;
margin-bottom: 0;
font-weight: normal;
text-align: center;
vertical-align: middle;
touch-action: manipulation;
cursor: pointer;
background-image: none;
border: 1px solid transparent;
white-space: nowrap;
padding: 6px 12px;
font-size: 14px;
line-height: 1.42857143;
border-radius: 4px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
color: ${COLOR_MINE_SHAFT};
background-color: ${WHITE_COLOR};
border-color: ${COLOR_GRAY_LIGHT_YADCF};
`;
const StyledModalBody = styled(Modal.Body)`
background-color: ${COLOR_CONCRETE};
color: ${COLOR_MINE_SHAFT};;
`;
class GlobalActivitiesModal extends Component {
constructor(props) {
@ -25,9 +61,9 @@ class GlobalActivitiesModal extends Component {
);
}
return this.props.activities.map((activity, i, arr) => {
let newDate = new Date(activity.created_at);
const newDate = new Date(activity.created_at);
if (i > 0) {
let prevDate = new Date(arr[i - 1].created_at);
const prevDate = new Date(arr[i - 1].created_at);
if (prevDate < newDate) {
return [
<ActivityDateElement key={newDate} date={newDate} />,
@ -45,20 +81,21 @@ class GlobalActivitiesModal extends Component {
}
addMoreActivities() {
let last_id = _.last(this.props.activities).id;
this.props.fetchActivities(last_id);
const lastId = _.last(this.props.activities).id;
this.props.fetchActivities(lastId);
}
addMoreButton() {
if (this.props.more) {
return (
<li>
<Button onClick={this.addMoreActivities}>
<li className="text-center">
<StyledBottom onClick={this.addMoreActivities}>
<FormattedMessage id="activities.more_activities" />
</Button>
</StyledBottom>
</li>
);
}
return "";
}
render() {
@ -69,12 +106,12 @@ class GlobalActivitiesModal extends Component {
<FormattedMessage id="activities.modal_title" />
</Modal.Title>
</Modal.Header>
<Modal.Body>
<ul>
<StyledModalBody>
<ul className="list-unstyled">
{this.displayActivities()}
{this.addMoreButton()}
</ul>
</Modal.Body>
</StyledModalBody>
<Modal.Footer>
<Button onClick={this.props.onCloseModal}>
<FormattedMessage id="general.close" />
@ -100,13 +137,13 @@ GlobalActivitiesModal.propTypes = {
};
const mapStateToProps = ({ global_activities }) => {
let { activities, more } = global_activities;
const { activities, more } = global_activities;
return { activities, more };
};
const mapDispatchToProps = dispatch => ({
fetchActivities(last_id) {
dispatch(getActivities(last_id));
fetchActivities(lastId) {
dispatch(getActivities(lastId));
}
});

View file

@ -12,7 +12,14 @@ import {
const InfoDropdown = () =>
<NavDropdown
noCaret
title={<span className="glyphicon glyphicon-info-sign" />}
title={
<span>
<span className="glyphicon glyphicon-info-sign" />&nbsp;
<span className="visible-xs-inline visible-sm-inline">
<FormattedMessage id="navbar.info_label" />
</span>
</span>
}
id="nav-info-dropdown"
>
<MenuItem href={CUSTOMER_SUPPORT_LINK} target="_blank">

View file

@ -83,7 +83,14 @@ class NotificationsDropdown extends Component {
<StyledNavDropdown
noCaret
id="notifications-dropdown"
title={<i className="fa fa-bell" />}
title={
<span>
<i className="fa fa-bell" />&nbsp;
<span className="visible-xs-inline visible-sm-inline">
<FormattedMessage id="navbar.notifications_label" />
</span>
</span>
}
onClick={this.getRecentNotifications}
>
<StyledListHeader>

View file

@ -7,6 +7,7 @@ import {
Glyphicon
} from "react-bootstrap";
import styled from "styled-components";
import { FormattedMessage } from "react-intl";
import { SEARCH_PATH } from "../../../app/routes";
import { ENTER_KEY_CODE } from "../../../app/constants/numeric";
@ -60,7 +61,14 @@ class SearchDropdown extends Component {
return (
<StyledNavDropdown
noCaret
title={<span className="glyphicon glyphicon-search" />}
title={
<span>
<span className="glyphicon glyphicon-search" />&nbsp;
<span className="visible-xs-inline visible-sm-inline">
<FormattedMessage id="navbar.search_label" />
</span>
</span>
}
onClick={this.setFocusToInput}
id="search-dropdown"
>

View file

@ -2,13 +2,14 @@ import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { Navbar, Nav, NavItem } from "react-bootstrap";
import { FormattedMessage } from "react-intl";
import styled from "styled-components";
import {
MAIN_COLOR_BLUE,
WHITE_COLOR,
BORDER_GRAY_COLOR
} from "../../app/constants/colors";
import { getActivities } from "../actions/ActivitiesActions";
import { getActivities, destroyActivities } from "../actions/ActivitiesActions";
import TeamSwitch from "./components/TeamSwitch";
import GlobalActivitiesModal from "./components/GlobalActivitiesModal";
import SearchDropdown from "./components/SearchDropdown";
@ -43,23 +44,36 @@ class Navigation extends Component {
this.state = {
showActivitesModal: false,
page: "",
currentTeam: { id: 0 }
current_team: { id: 0 }
};
this.selectItemCallback = this.selectItemCallback.bind(this);
this.closeModalCallback = this.closeModalCallback.bind(this);
}
selectItemCallback(key, ev) {
if (key === 4) {
ev.preventDefault();
this.setState({ showActivitesModal: !this.state.showActivitesModal });
// Call action creator to fetch activities from the server
this.props.fetchActivities();
switch (key) {
case 1:
window.location = "/";
break;
case 2:
window.location = "/protocols";
break;
case 3:
window.location = `/teams/${this.props.current_team.id}/repositories`;
break;
case 4:
ev.preventDefault();
this.setState({ showActivitesModal: !this.state.showActivitesModal });
// Call action creator to fetch activities from the server
this.props.fetchActivities();
break;
default:
}
}
closeModalCallback() {
this.setState({ showActivitesModal: false });
this.props.destroyActivities();
}
render() {
@ -74,30 +88,39 @@ class Navigation extends Component {
</Navbar.Brand>
</Navbar.Header>
<Nav>
<NavItem eventKey={1} href="/">
<span className="glyphicon glyphicon-home" title="Home" />
<NavItem eventKey={1}>
<span className="glyphicon glyphicon-home" title="Home" />&nbsp;
<span className="visible-xs-inline visible-sm-inline">
<FormattedMessage id="navbar.home_label" />
</span>
</NavItem>
<NavItem eventKey={2} href="/protocols">
<NavItem eventKey={2}>
<span
className="glyphicon glyphicon-list-alt"
title="Protocol repositories"
/>
/>&nbsp;
<span className="visible-xs-inline visible-sm-inline">
<FormattedMessage id="navbar.protocols_label" />
</span>
</NavItem>
<NavItem
eventKey={3}
href={`/teams/${this.state.currentTeam.id}/repositories`}
>
<NavItem eventKey={3}>
<i
className="fa fa-cubes"
aria-hidden="true"
title="Repositories"
/>
/>&nbsp;
<span className="visible-xs-inline visible-sm-inline">
<FormattedMessage id="navbar.repositories_label" />
</span>
</NavItem>
<NavItem eventKey={4}>
<span
className="glyphicon glyphicon-equalizer"
title="Activities"
/>
/>&nbsp;
<span className="visible-xs-inline visible-sm-inline">
<FormattedMessage id="navbar.activities_label" />
</span>
</NavItem>
</Nav>
<Nav pullRight>
@ -118,14 +141,26 @@ class Navigation extends Component {
}
Navigation.propTypes = {
fetchActivities: PropTypes.func.isRequired
fetchActivities: PropTypes.func.isRequired,
destroyActivities: PropTypes.func.isRequired,
current_team: PropTypes.shape({
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
current_team: PropTypes.bool.isRequired
}).isRequired
};
// Map the fetch activity action to component
// Map the states from store to component props
const mapStateToProps = ({ current_team }) => ({ current_team });
// Map the fetch activity action to component props
const mapDispatchToProps = dispatch => ({
fetchActivities() {
dispatch(getActivities());
},
destroyActivities() {
dispatch(destroyActivities());
}
});
export default connect(null, mapDispatchToProps)(Navigation);
export default connect(mapStateToProps, mapDispatchToProps)(Navigation);

View file

@ -1,21 +1,27 @@
import {
GLOBAL_ACTIVITIES_DATA,
MORE_GLOBAL_ACTIVITIES
DESTROY_GLOBAL_ACTIVITIES_DATA
} from "../../app/action_types";
export function globalActivities(
state = { more: true, activities: [] },
action
) {
if (action.type === GLOBAL_ACTIVITIES_DATA) {
return {
...state,
activities: [
...state.activities,
...action.payload.global_activities.activities
],
more: action.payload.global_activities.more
};
const initialStateu = { more: true, activities: [] };
export function globalActivities(state = initialStateu, action) {
switch (action.type) {
case GLOBAL_ACTIVITIES_DATA:
return {
...state,
activities: [
...state.activities,
...action.payload.global_activities.activities
],
more: action.payload.global_activities.more
};
case DESTROY_GLOBAL_ACTIVITIES_DATA:
return {
...state,
...initialStateu
};
default:
return state;
}
return state;
}

View file

@ -9,6 +9,10 @@ body {
font-size: 13px;
}
.label-primary {
background-color: $color-theme-primary;
}
.btn-primary {
background-color: $color-theme-secondary;
border-color: $primary-hover-color;

View file

@ -4,6 +4,7 @@
<meta data-hook="head-js">
<title><%= t('nav.title') %></title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<%= csrf_meta_tags %>
<%= stylesheet_link_tag 'bootstrap/css/bootstrap.min' %>
<%= stylesheet_link_tag 'font-awesome/css/font-awesome.min' %>
<%= stylesheet_pack_tag 'styles/main' %>

View file

@ -53,6 +53,7 @@
"glob": "^7.1.2",
"js-yaml": "^3.9.0",
"lodash": "^4.17.4",
"moment": "^2.18.1",
"node-sass": "^4.5.3",
"path-complete-extname": "^0.1.0",
"postcss-loader": "^2.0.6",
@ -66,6 +67,7 @@
"react-dom": "^15.6.1",
"react-intl": "^2.3.0",
"react-intl-redux": "^0.6.0",
"react-moment": "^0.6.4",
"react-redux": "^5.0.5",
"react-router-bootstrap": "^0.24.2",
"react-router-dom": "^4.1.2",

View file

@ -3572,6 +3572,10 @@ mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkd
dependencies:
minimist "0.0.8"
moment@^2.18.1:
version "2.18.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@ -4719,6 +4723,10 @@ 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"