mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-09-20 14:45:56 +08:00
Merge branch 'decoupling-settings-page' of https://github.com/biosistemika/scinote-web into zd_SCI_1576
This commit is contained in:
commit
327e27e123
1
.babelrc
1
.babelrc
|
@ -16,6 +16,7 @@
|
|||
"flow"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-flow-strip-types",
|
||||
"transform-object-rest-spread",
|
||||
"syntax-dynamic-import",
|
||||
"transform-react-jsx-source",
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
"rules": {
|
||||
"import/extensions": "off",
|
||||
"import/no-unresolved": "off",
|
||||
// Because of flow-typed & flow-bin
|
||||
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
|
||||
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
|
||||
"spaced-comment": [
|
||||
"error",
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -66,6 +66,7 @@ gem 'delayed_paperclip',
|
|||
gem 'rubyzip'
|
||||
gem 'jbuilder' # JSON structures via a Builder-style DSL
|
||||
gem 'activerecord-import'
|
||||
gem 'scenic', '~> 1.4'
|
||||
|
||||
gem 'paperclip', '~> 5.1' # File attachment, image attachment library
|
||||
gem 'aws-sdk', '~> 2'
|
||||
|
@ -112,6 +113,7 @@ group :test do
|
|||
gem 'poltergeist'
|
||||
gem 'phantomjs', :require => 'phantomjs/poltergeist'
|
||||
gem 'simplecov', require: false
|
||||
gem 'json_matchers'
|
||||
end
|
||||
|
||||
group :production do
|
||||
|
|
|
@ -247,6 +247,10 @@ GEM
|
|||
js_cookie_rails (2.1.4)
|
||||
railties (>= 3.1)
|
||||
json (1.8.6)
|
||||
json-schema (2.8.0)
|
||||
addressable (>= 2.4)
|
||||
json_matchers (0.7.2)
|
||||
json-schema (~> 2.7)
|
||||
kaminari (1.1.1)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.1.1)
|
||||
|
@ -418,6 +422,9 @@ GEM
|
|||
sprockets (>= 2.8, < 4.0)
|
||||
sprockets-rails (>= 2.0, < 4.0)
|
||||
tilt (>= 1.1, < 3)
|
||||
scenic (1.4.0)
|
||||
activerecord (>= 4.0.0)
|
||||
railties (>= 4.0.0)
|
||||
scss_lint (0.55.0)
|
||||
rake (>= 0.9, < 13)
|
||||
sass (~> 3.4.20)
|
||||
|
@ -534,6 +541,7 @@ DEPENDENCIES
|
|||
jquery-turbolinks
|
||||
jquery-ui-rails
|
||||
js_cookie_rails
|
||||
json_matchers
|
||||
kaminari
|
||||
listen (~> 3.0)
|
||||
logging (~> 2.0.0)
|
||||
|
@ -563,6 +571,7 @@ DEPENDENCIES
|
|||
rubyzip
|
||||
sanitize (~> 4.4)
|
||||
sass-rails (~> 5.0.6)
|
||||
scenic (~> 1.4)
|
||||
scss_lint
|
||||
sdoc (~> 0.4.0)
|
||||
shoulda-matchers
|
||||
|
|
23
app/controllers/client_api/configurations_controller.rb
Normal file
23
app/controllers/client_api/configurations_controller.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
module ClientApi
|
||||
class ConfigurationsController < ApplicationController
|
||||
|
||||
def about_scinote
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: {
|
||||
scinoteVersion: Scinote::Application::VERSION,
|
||||
addons: list_all_addons
|
||||
}, status: :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def list_all_addons
|
||||
Rails::Engine.subclasses
|
||||
.select { |c| c.name.start_with?('Scinote') }
|
||||
.map(&:parent)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,8 +4,9 @@ module ClientApi
|
|||
include ClientApi::Users::UserTeamsHelper
|
||||
|
||||
def index
|
||||
teams = current_user.datatables_teams
|
||||
success_response(template: '/client_api/teams/index',
|
||||
locals: { teams: current_user.teams_data })
|
||||
locals: { teams: teams })
|
||||
end
|
||||
|
||||
def create
|
||||
|
@ -55,6 +56,11 @@ module ClientApi
|
|||
error_response(message: error.to_s)
|
||||
end
|
||||
|
||||
def current_team
|
||||
success_response(template: '/client_api/teams/current_team',
|
||||
locals: { team: current_user.current_team })
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def team_params
|
||||
|
|
|
@ -5,5 +5,6 @@ $icon-font-path: "~bootstrap-sass/assets/fonts/bootstrap/";
|
|||
@import "~font-awesome/scss/font-awesome";
|
||||
@import "~react-bootstrap-table/dist/react-bootstrap-table.min";
|
||||
@import "react-tagsinput/react-tagsinput.css";
|
||||
@import "react-bootstrap-timezone-picker/dist/react-bootstrap-timezone-picker.min.css";
|
||||
|
||||
@import "../src/styles/main";
|
||||
|
|
|
@ -32,7 +32,7 @@ class AlertsContainer extends Component {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<Wrapper id="alert-flash">
|
||||
<Wrapper>
|
||||
<TransitionGroup>
|
||||
{this.props.alerts.map((alert) =>
|
||||
<CSSTransition key={alert.id}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
// @flow
|
||||
import React from "react";
|
||||
import type { Node } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { Modal } from "react-bootstrap";
|
||||
|
||||
type Props = {
|
||||
showModal: boolean,
|
||||
scinoteVersion: string,
|
||||
addons: Array<string>,
|
||||
onModalClose: Function
|
||||
};
|
||||
|
||||
export default (props: Props): Node => {
|
||||
const { showModal, scinoteVersion, addons, onModalClose } = props;
|
||||
return (
|
||||
<Modal show={showModal} onHide={onModalClose}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
<FormattedMessage id="general.about_scinote" />
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<strong>
|
||||
<FormattedMessage id="general.core_version" />
|
||||
</strong>
|
||||
<p>{scinoteVersion}</p>
|
||||
<strong>
|
||||
<FormattedMessage id="general.addon_versions" />
|
||||
</strong>
|
||||
{addons.map((addon: string): Node => <p>{addon}</p>)}
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -1,4 +1,5 @@
|
|||
import React from "react";
|
||||
// @flow
|
||||
import React, { Component } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { NavDropdown, MenuItem } from "react-bootstrap";
|
||||
import {
|
||||
|
@ -8,35 +9,81 @@ import {
|
|||
PREMIUM_LINK,
|
||||
CONTACT_US_LINK
|
||||
} from "../../../config/routes";
|
||||
import { getSciNoteInfo } from "../../../services/api/configurations_api";
|
||||
|
||||
const InfoDropdown = () =>
|
||||
<NavDropdown
|
||||
noCaret
|
||||
title={
|
||||
<span>
|
||||
<span className="glyphicon glyphicon-info-sign" />
|
||||
<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">
|
||||
<FormattedMessage id="info_dropdown.customer_support" />
|
||||
</MenuItem>
|
||||
<MenuItem href={TUTORIALS_LINK} target="_blank">
|
||||
<FormattedMessage id="info_dropdown.tutorials" />
|
||||
</MenuItem>
|
||||
<MenuItem href={RELEASE_NOTES_LINK} target="_blank">
|
||||
<FormattedMessage id="info_dropdown.release_notes" />
|
||||
</MenuItem>
|
||||
<MenuItem href={PREMIUM_LINK} target="_blank">
|
||||
<FormattedMessage id="info_dropdown.premium" />
|
||||
</MenuItem>
|
||||
<MenuItem href={CONTACT_US_LINK} target="_blank">
|
||||
<FormattedMessage id="info_dropdown.contact_us" />
|
||||
</MenuItem>
|
||||
</NavDropdown>;
|
||||
import AboutScinoteModal from "./AboutScinoteModal";
|
||||
|
||||
type State = {
|
||||
modalOpen: boolean,
|
||||
scinoteVersion: string,
|
||||
addons: Array<string>
|
||||
};
|
||||
|
||||
class InfoDropdown extends Component<*, State> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.state = { showModal: false, scinoteVersion: "", addons: [] };
|
||||
(this: any).showAboutSciNoteModal = this.showAboutSciNoteModal.bind(this);
|
||||
(this: any).closeModal = this.closeModal.bind(this);
|
||||
}
|
||||
|
||||
showAboutSciNoteModal(): void {
|
||||
getSciNoteInfo().then(response => {
|
||||
const { scinoteVersion, addons } = response;
|
||||
(this: any).setState({
|
||||
scinoteVersion,
|
||||
addons,
|
||||
showModal: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
closeModal(): void {
|
||||
(this: any).setState({ showModal: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<NavDropdown
|
||||
noCaret
|
||||
title={
|
||||
<span>
|
||||
<span className="glyphicon glyphicon-info-sign" />
|
||||
<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">
|
||||
<FormattedMessage id="info_dropdown.customer_support" />
|
||||
</MenuItem>
|
||||
<MenuItem href={TUTORIALS_LINK} target="_blank">
|
||||
<FormattedMessage id="info_dropdown.tutorials" />
|
||||
</MenuItem>
|
||||
<MenuItem href={RELEASE_NOTES_LINK} target="_blank">
|
||||
<FormattedMessage id="info_dropdown.release_notes" />
|
||||
</MenuItem>
|
||||
<MenuItem href={PREMIUM_LINK} target="_blank">
|
||||
<FormattedMessage id="info_dropdown.premium" />
|
||||
</MenuItem>
|
||||
<MenuItem href={CONTACT_US_LINK} target="_blank">
|
||||
<FormattedMessage id="info_dropdown.contact_us" />
|
||||
</MenuItem>
|
||||
<MenuItem divider />
|
||||
<MenuItem onClick={this.showAboutSciNoteModal}>
|
||||
<FormattedMessage id="info_dropdown.about_scinote" />
|
||||
<AboutScinoteModal
|
||||
showModal={this.state.showModal}
|
||||
scinoteVersion={this.state.scinoteVersion}
|
||||
addons={this.state.addons}
|
||||
onModalClose={this.closeModal}
|
||||
/>
|
||||
</MenuItem>
|
||||
</NavDropdown>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default InfoDropdown;
|
||||
|
|
|
@ -1,51 +1,71 @@
|
|||
// @flow
|
||||
import React, { Component } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { NavDropdown, MenuItem, Glyphicon } from "react-bootstrap";
|
||||
import styled from "styled-components";
|
||||
import _ from "lodash";
|
||||
|
||||
import { ROOT_PATH } from "../../../config/routes";
|
||||
import { ROOT_PATH, SETTINGS_NEW_TEAM_ROUTE } from "../../../config/routes";
|
||||
import { BORDER_GRAY_COLOR } from "../../../config/constants/colors";
|
||||
import { changeTeam } from "../../actions/TeamsActions";
|
||||
import { getTeamsList } from "../../actions/TeamsActions";
|
||||
import { getCurrentTeam, changeTeam } from "../../actions/TeamsActions";
|
||||
import { getTeams } from "../../../services/api/teams_api";
|
||||
|
||||
const StyledNavDropdown = styled(NavDropdown)`
|
||||
border-left: 1px solid ${BORDER_GRAY_COLOR};
|
||||
border-right: 1px solid ${BORDER_GRAY_COLOR};
|
||||
`;
|
||||
|
||||
class TeamSwitch extends Component {
|
||||
constructor(props) {
|
||||
type State = {
|
||||
allTeams: Array<Teams$Team>
|
||||
}
|
||||
|
||||
type Props = {
|
||||
current_team: Teams$CurrentTeam,
|
||||
eventKey: string,
|
||||
getCurrentTeam: Function,
|
||||
changeTeam: Function
|
||||
}
|
||||
|
||||
class TeamSwitch extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.displayTeams = this.displayTeams.bind(this);
|
||||
(this: any).state = { allTeams: [] };
|
||||
(this: any).displayTeams = this.displayTeams.bind(this);
|
||||
(this: any).setTeams = this.setTeams.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.getTeamsList();
|
||||
this.props.getCurrentTeam();
|
||||
}
|
||||
|
||||
setTeams() {
|
||||
getTeams().then(response => (this: any).setState({ allTeams: response }));
|
||||
}
|
||||
|
||||
changeTeam(teamId) {
|
||||
this.props.changeTeam(teamId);
|
||||
window.location = ROOT_PATH;
|
||||
setTimeout(() => {
|
||||
window.location = ROOT_PATH;
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
displayTeams() {
|
||||
if (!_.isEmpty(this.props.all_teams)) {
|
||||
return this.props.all_teams
|
||||
.filter(team => !team.current_team)
|
||||
if (!_.isEmpty((this: any).state.allTeams)) {
|
||||
return (this: any).state.allTeams
|
||||
.filter(team => team.id !== this.props.current_team.id)
|
||||
.map(team => (
|
||||
<MenuItem onSelect={() => this.changeTeam(team.id)} key={team.id}>
|
||||
{team.name}
|
||||
</MenuItem>
|
||||
));
|
||||
}
|
||||
return <MenuItem />;
|
||||
}
|
||||
|
||||
newTeamLink() {
|
||||
return (
|
||||
<MenuItem href="/users/settings/teams/new" key="addNewTeam">
|
||||
<MenuItem href={SETTINGS_NEW_TEAM_ROUTE} key="addNewTeam">
|
||||
<Glyphicon glyph="plus" />
|
||||
<FormattedMessage id="global_team_switch.new_team" />
|
||||
</MenuItem>
|
||||
|
@ -57,6 +77,7 @@ class TeamSwitch extends Component {
|
|||
<StyledNavDropdown
|
||||
noCaret
|
||||
eventKey={this.props.eventKey}
|
||||
onClick={this.setTeams}
|
||||
title={
|
||||
<span>
|
||||
<i className="fa fa-users" /> {this.props.current_team.name}
|
||||
|
@ -72,38 +93,11 @@ class TeamSwitch extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
TeamSwitch.propTypes = {
|
||||
getTeamsList: PropTypes.func.isRequired,
|
||||
eventKey: PropTypes.number.isRequired,
|
||||
changeTeam: PropTypes.func.isRequired,
|
||||
all_teams: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
current_team: PropTypes.bool.isRequired
|
||||
}).isRequired
|
||||
),
|
||||
current_team: PropTypes.shape({
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
current_team: PropTypes.bool.isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
// Map the states from store to component
|
||||
const mapStateToProps = ({ all_teams, current_team }) => ({
|
||||
current_team,
|
||||
all_teams: all_teams.collection
|
||||
const mapStateToProps = ({ current_team }) => ({
|
||||
current_team
|
||||
});
|
||||
|
||||
// Map the fetch activity action to component
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
changeTeam(teamId) {
|
||||
dispatch(changeTeam(teamId));
|
||||
},
|
||||
getTeamsList() {
|
||||
dispatch(getTeamsList());
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TeamSwitch);
|
||||
export default connect(mapStateToProps, { getCurrentTeam, changeTeam })(
|
||||
TeamSwitch
|
||||
);
|
||||
|
|
|
@ -5,7 +5,11 @@ import type {
|
|||
Actopm$SetCurrentTeam
|
||||
} from "flow-typed";
|
||||
import type { Dispatch } from "redux-thunk";
|
||||
import { getTeams, changeCurrentTeam } from "../../services/api/teams_api";
|
||||
import {
|
||||
getTeams,
|
||||
changeCurrentTeam,
|
||||
getCurrentTeam as fetchCurrentTeam
|
||||
} from "../../services/api/teams_api";
|
||||
import { GET_LIST_OF_TEAMS, SET_CURRENT_TEAM } from "../../config/action_types";
|
||||
|
||||
export function addTeamsData(data: Array<Teams$Team>): Action$AddTeamData {
|
||||
|
@ -15,7 +19,7 @@ export function addTeamsData(data: Array<Teams$Team>): Action$AddTeamData {
|
|||
};
|
||||
}
|
||||
|
||||
export function setCurrentTeam(team: Teams$Team): Actopm$SetCurrentTeam {
|
||||
export function setCurrentTeam(team: Teams$CurrentTeam): Actopm$SetCurrentTeam {
|
||||
return {
|
||||
team,
|
||||
type: SET_CURRENT_TEAM
|
||||
|
@ -26,9 +30,7 @@ export function getTeamsList(): Dispatch {
|
|||
return dispatch => {
|
||||
getTeams()
|
||||
.then(response => {
|
||||
const { teams, currentTeam } = response;
|
||||
dispatch(addTeamsData(teams));
|
||||
dispatch(setCurrentTeam(currentTeam));
|
||||
dispatch(addTeamsData(response));
|
||||
})
|
||||
.catch(error => {
|
||||
console.log("get Teams Error: ", error);
|
||||
|
@ -36,14 +38,16 @@ export function getTeamsList(): Dispatch {
|
|||
};
|
||||
}
|
||||
|
||||
export function getCurrentTeam(): Dispatch {
|
||||
return dispatch => {
|
||||
fetchCurrentTeam().then(response => dispatch(setCurrentTeam(response)));
|
||||
};
|
||||
}
|
||||
|
||||
export function changeTeam(teamID: number): Dispatch {
|
||||
return dispatch => {
|
||||
changeCurrentTeam(teamID)
|
||||
.then(response => {
|
||||
const { teams, currentTeam } = response;
|
||||
dispatch(addTeamsData(teams));
|
||||
dispatch(setCurrentTeam(currentTeam));
|
||||
})
|
||||
.then(response => dispatch(addTeamsData(response)))
|
||||
.catch(error => {
|
||||
console.log("get Teams Error: ", error);
|
||||
});
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import React, { Component } from "react";
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import { HelpBlock } from "react-bootstrap";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import type { ValidationError } from "flow-typed";
|
||||
import PropTypes from "prop-types";
|
||||
import styled from "styled-components";
|
||||
import shortid from "shortid";
|
||||
|
@ -11,8 +14,12 @@ const MyHelpBlock = styled(HelpBlock)`
|
|||
}
|
||||
`;
|
||||
|
||||
class ValidatedErrorHelpBlock extends Component {
|
||||
static renderErrorMessage(error) {
|
||||
type Props = {
|
||||
tag: string
|
||||
};
|
||||
|
||||
class ValidatedErrorHelpBlock extends React.Component<Props> {
|
||||
static renderErrorMessage(error: ValidationError): React.Node {
|
||||
const key = shortid.generate();
|
||||
if (error.intl) {
|
||||
return (
|
||||
|
@ -39,16 +46,14 @@ class ValidatedErrorHelpBlock extends Component {
|
|||
const errors = this.context.errors(tag) || [];
|
||||
return (
|
||||
<MyHelpBlock {...cleanProps}>
|
||||
{errors.map((error) => ValidatedErrorHelpBlock.renderErrorMessage(error))}
|
||||
{errors.map(
|
||||
(error: ValidationError) => ValidatedErrorHelpBlock.renderErrorMessage(error)
|
||||
)}
|
||||
</MyHelpBlock>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ValidatedErrorHelpBlock.propTypes = {
|
||||
tag: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
ValidatedErrorHelpBlock.contextTypes = {
|
||||
errors: PropTypes.func
|
||||
}
|
||||
|
|
|
@ -1,33 +1,64 @@
|
|||
import React, { Component } from "react";
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import update from "immutability-helper";
|
||||
import type {
|
||||
ValidationError,
|
||||
ValidationErrors
|
||||
} from "flow-typed";
|
||||
import PropTypes from "prop-types";
|
||||
import _ from "lodash";
|
||||
|
||||
class ValidatedForm extends Component {
|
||||
static parseErrors(errors) {
|
||||
type Props = {
|
||||
children?: React.Node
|
||||
};
|
||||
|
||||
type State = {
|
||||
[string]: Array<ValidationError>
|
||||
};
|
||||
|
||||
type ChildContext = {
|
||||
setErrors: Function,
|
||||
setErrorsForTag: Function,
|
||||
errors: Function,
|
||||
hasAnyError: Function,
|
||||
hasErrorForTag: Function,
|
||||
addErrorsForTag: Function,
|
||||
clearErrorsForTag: Function,
|
||||
clearErrors: Function
|
||||
};
|
||||
|
||||
class ValidatedForm extends React.Component<Props, State> {
|
||||
static defaultProps = {
|
||||
children: undefined
|
||||
}
|
||||
|
||||
static parseErrors(errors: ValidationErrors): Array<ValidationError> {
|
||||
// This method is quite smart, in the sense that accepts either
|
||||
// errors in 3 shapes: localized error messages ({}),
|
||||
// unlocalized error messages ({}), or mere strings (unlocalized)
|
||||
const arr = _.isString(errors) ? [errors] : errors;
|
||||
return arr.map((el) => _.isString(el) ? { message: el } : el);
|
||||
return arr.map(
|
||||
(el: string | ValidationError) => _.isString(el) ? { message: el } : el
|
||||
);
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {}
|
||||
this.state = {};
|
||||
|
||||
this.setErrors = this.setErrors.bind(this);
|
||||
this.setErrorsForTag = this.setErrorsForTag.bind(this);
|
||||
this.errors = this.errors.bind(this);
|
||||
this.hasAnyError = this.hasAnyError.bind(this);
|
||||
this.hasErrorForTag = this.hasErrorForTag.bind(this);
|
||||
this.addErrorsForTag = this.addErrorsForTag.bind(this);
|
||||
this.clearErrorsForTag = this.clearErrorsForTag.bind(this);
|
||||
this.clearErrors = this.clearErrors.bind(this);
|
||||
(this: any).setErrors = this.setErrors.bind(this);
|
||||
(this: any).setErrorsForTag = this.setErrorsForTag.bind(this);
|
||||
(this: any).errors = this.errors.bind(this);
|
||||
(this: any).hasAnyError = this.hasAnyError.bind(this);
|
||||
(this: any).hasErrorForTag = this.hasErrorForTag.bind(this);
|
||||
(this: any).addErrorsForTag = this.addErrorsForTag.bind(this);
|
||||
(this: any).clearErrorsForTag = this.clearErrorsForTag.bind(this);
|
||||
(this: any).clearErrors = this.clearErrors.bind(this);
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
getChildContext(): ChildContext {
|
||||
// Pass functions downstream via context
|
||||
return {
|
||||
setErrors: this.setErrors,
|
||||
|
@ -41,7 +72,7 @@ class ValidatedForm extends Component {
|
|||
};
|
||||
}
|
||||
|
||||
setErrors(errors) {
|
||||
setErrors(errors: { [string]: ValidationErrors }): void {
|
||||
const newState = {};
|
||||
_.entries(errors).forEach(([key, value]) => {
|
||||
newState[key] = ValidatedForm.parseErrors(value);
|
||||
|
@ -49,28 +80,28 @@ class ValidatedForm extends Component {
|
|||
this.setState(newState);
|
||||
}
|
||||
|
||||
setErrorsForTag(tag, errors) {
|
||||
setErrorsForTag(tag: string, errors: ValidationErrors): void {
|
||||
const newState = update(this.state, {
|
||||
[tag]: { $set: ValidatedForm.parseErrors(errors) }
|
||||
});
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
errors(tag) {
|
||||
errors(tag: string): Array<ValidationError> {
|
||||
return this.state[tag];
|
||||
}
|
||||
|
||||
hasAnyError() {
|
||||
hasAnyError(): boolean {
|
||||
return _.values(this.state) &&
|
||||
_.flatten(_.values(this.state)).length > 0;
|
||||
}
|
||||
|
||||
hasErrorForTag(tag) {
|
||||
hasErrorForTag(tag: string): boolean {
|
||||
return _.has(this.state, tag) && this.state[tag].length > 0;
|
||||
}
|
||||
|
||||
addErrorsForTag(tag, errors) {
|
||||
let newState;
|
||||
addErrorsForTag(tag: string, errors: ValidationErrors): void {
|
||||
let newState: State;
|
||||
if (_.has(this.state, tag)) {
|
||||
newState = update(this.state, { [tag]: { $push: errors } });
|
||||
} else {
|
||||
|
@ -79,12 +110,12 @@ class ValidatedForm extends Component {
|
|||
this.setState(newState);
|
||||
}
|
||||
|
||||
clearErrorsForTag(tag) {
|
||||
clearErrorsForTag(tag: string): void {
|
||||
const newState = update(this.state, { [tag]: { $set: [] } });
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
clearErrors() {
|
||||
clearErrors(): void {
|
||||
this.setState({});
|
||||
}
|
||||
|
||||
|
@ -97,14 +128,6 @@ class ValidatedForm extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
ValidatedForm.propTypes = {
|
||||
children: PropTypes.node
|
||||
}
|
||||
|
||||
ValidatedForm.defaultProps = {
|
||||
children: undefined
|
||||
}
|
||||
|
||||
ValidatedForm.childContextTypes = {
|
||||
setErrors: PropTypes.func,
|
||||
setErrorsForTag: PropTypes.func,
|
||||
|
|
|
@ -1,16 +1,33 @@
|
|||
import React, { Component } from "react";
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import { FormControl } from "react-bootstrap";
|
||||
import type { ValidationError } from "flow-typed";
|
||||
import PropTypes from "prop-types";
|
||||
import _ from "lodash";
|
||||
|
||||
class ValidatedFormControl extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
type Props = {
|
||||
tag: string,
|
||||
messageIds: {[string]: Array<string>},
|
||||
onChange?: Function,
|
||||
validatorsOnChange: Array<Function>,
|
||||
children?: React.Node
|
||||
};
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.cleanProps = this.cleanProps.bind(this);
|
||||
class ValidatedFormControl extends React.Component<Props> {
|
||||
static defaultProps = {
|
||||
onChange: undefined,
|
||||
children: undefined
|
||||
}
|
||||
|
||||
handleChange(e) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
(this: any).handleChange = this.handleChange.bind(this);
|
||||
(this: any).cleanProps = this.cleanProps.bind(this);
|
||||
}
|
||||
|
||||
handleChange(e: SyntheticEvent<HTMLInputElement>): void {
|
||||
const tag = this.props.tag;
|
||||
const messageIds = this.props.messageIds;
|
||||
const target = e.target;
|
||||
|
@ -21,8 +38,8 @@ class ValidatedFormControl extends Component {
|
|||
}
|
||||
|
||||
// Validate the field
|
||||
let errors = [];
|
||||
this.props.validatorsOnChange.forEach((validator) => {
|
||||
let errors: Array<ValidationError> = [];
|
||||
this.props.validatorsOnChange.forEach((validator: Function) => {
|
||||
errors = errors.concat(validator(target, messageIds));
|
||||
});
|
||||
this.context.setErrorsForTag(tag, errors);
|
||||
|
@ -50,19 +67,6 @@ class ValidatedFormControl extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
ValidatedFormControl.propTypes = {
|
||||
tag: PropTypes.string.isRequired,
|
||||
messageIds: PropTypes.objectOf(PropTypes.string),
|
||||
validatorsOnChange: PropTypes.arrayOf(PropTypes.func),
|
||||
onChange: PropTypes.func
|
||||
}
|
||||
|
||||
ValidatedFormControl.defaultProps = {
|
||||
messageIds: {},
|
||||
validatorsOnChange: [],
|
||||
onChange: undefined
|
||||
}
|
||||
|
||||
ValidatedFormControl.contextTypes = {
|
||||
setErrorsForTag: PropTypes.func
|
||||
}
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import React from "react";
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import { FormGroup } from "react-bootstrap";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const ValidatedFormGroup = (props, context) => {
|
||||
type Props = {
|
||||
tag: string
|
||||
};
|
||||
|
||||
const ValidatedFormGroup = (props: Props, context: any) => {
|
||||
// Remove additional props from the props
|
||||
const { tag, ...cleanProps } = props;
|
||||
|
||||
|
@ -19,10 +25,6 @@ const ValidatedFormGroup = (props, context) => {
|
|||
);
|
||||
};
|
||||
|
||||
ValidatedFormGroup.propTypes = {
|
||||
tag: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
ValidatedFormGroup.contextTypes = {
|
||||
hasErrorForTag: PropTypes.func
|
||||
}
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import { Button } from "react-bootstrap";
|
||||
import PropTypes from "prop-types";
|
||||
import type { Node } from 'react';
|
||||
|
||||
const ValidatedSubmitButton = (props, context) =>
|
||||
type Props = {
|
||||
children?: Node
|
||||
};
|
||||
|
||||
const ValidatedSubmitButton = (props: Props, context: any) =>
|
||||
<Button {...props} disabled={context.hasAnyError()}>
|
||||
{props.children}
|
||||
</Button>
|
||||
;
|
||||
|
||||
ValidatedSubmitButton.propTypes = {
|
||||
children: PropTypes.node
|
||||
}
|
||||
|
||||
ValidatedSubmitButton.defaultProps = {
|
||||
children: undefined
|
||||
}
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
// @flow
|
||||
|
||||
import _ from "lodash";
|
||||
import type { ValidationError } from "flow-typed";
|
||||
import { AVATAR_MAX_SIZE_MB } from "../../../config/constants/numeric";
|
||||
import { AVATAR_VALID_EXTENSIONS } from "../../../config/constants/strings";
|
||||
|
||||
export const avatarExtensionValidator = (target, messageIds = {}) => {
|
||||
export const avatarExtensionValidator = (
|
||||
target: HTMLInputElement,
|
||||
messageIds: { [string]: string } = {}): Array<ValidationError> => {
|
||||
const messageId = _.has(messageIds, "invalid_file_extension") ?
|
||||
messageIds.invalid_file_extension :
|
||||
"validators.file.invalid_file_extension";
|
||||
|
@ -25,7 +30,9 @@ export const avatarExtensionValidator = (target, messageIds = {}) => {
|
|||
return [];
|
||||
}
|
||||
|
||||
export const avatarSizeValidator = (target, messageIds = {}) => {
|
||||
export const avatarSizeValidator = (
|
||||
target: HTMLInputElement,
|
||||
messageIds: { [string]: string } = {}): Array<ValidationError> => {
|
||||
const messageId = _.has(messageIds, "file_too_large") ?
|
||||
messageIds.file_too_large :
|
||||
"validators.file.file_too_large";
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
// @flow
|
||||
|
||||
import _ from "lodash";
|
||||
import type { ValidationError } from "flow-typed";
|
||||
import {
|
||||
NAME_MIN_LENGTH,
|
||||
NAME_MAX_LENGTH,
|
||||
|
@ -9,7 +12,9 @@ import {
|
|||
} from "../../../config/constants/numeric";
|
||||
import { EMAIL_REGEX } from "../../../config/constants/strings";
|
||||
|
||||
export const nameMinLengthValidator = (target, messageIds = {}) => {
|
||||
export const nameMinLengthValidator = (
|
||||
target: HTMLInputElement,
|
||||
messageIds: { [string]: string } = {}): Array<ValidationError> => {
|
||||
const messageId = _.has(messageIds, "text_too_short") ?
|
||||
messageIds.text_too_short :
|
||||
"validators.text.text_too_short";
|
||||
|
@ -25,7 +30,9 @@ export const nameMinLengthValidator = (target, messageIds = {}) => {
|
|||
return [];
|
||||
};
|
||||
|
||||
export const nameMaxLengthValidator = (target, messageIds = {}) => {
|
||||
export const nameMaxLengthValidator = (
|
||||
target: HTMLInputElement,
|
||||
messageIds: { [string]: string } = {}): Array<ValidationError> => {
|
||||
const messageId = _.has(messageIds, "text_too_long") ?
|
||||
messageIds.text_too_long :
|
||||
"validators.text.text_too_long";
|
||||
|
@ -41,7 +48,9 @@ export const nameMaxLengthValidator = (target, messageIds = {}) => {
|
|||
return [];
|
||||
};
|
||||
|
||||
export const nameLengthValidator = (target, messageIds = {}) => {
|
||||
export const nameLengthValidator = (
|
||||
target: HTMLInputElement,
|
||||
messageIds: { [string]: string } = {}): Array<ValidationError> => {
|
||||
const res = nameMinLengthValidator(target, messageIds);
|
||||
if (res.length > 0) {
|
||||
return res;
|
||||
|
@ -49,7 +58,9 @@ export const nameLengthValidator = (target, messageIds = {}) => {
|
|||
return nameMaxLengthValidator(target, messageIds);
|
||||
};
|
||||
|
||||
export const textBlankValidator = (target, messageIds = {}) => {
|
||||
export const textBlankValidator = (
|
||||
target: HTMLInputElement,
|
||||
messageIds: { [string]: string } = {}): Array<ValidationError> => {
|
||||
const messageId = _.has(messageIds, "text_blank") ?
|
||||
messageIds.text_blank :
|
||||
"validators.text.text_blank";
|
||||
|
@ -64,7 +75,9 @@ export const textBlankValidator = (target, messageIds = {}) => {
|
|||
return [];
|
||||
}
|
||||
|
||||
export const textMaxLengthValidator = (target, messageIds = {}) => {
|
||||
export const textMaxLengthValidator = (
|
||||
target: HTMLInputElement,
|
||||
messageIds: { [string]: string } = {}): Array<ValidationError> => {
|
||||
const messageId = _.has(messageIds, "text_too_long") ?
|
||||
messageIds.text_too_long :
|
||||
"validators.text.text_too_long";
|
||||
|
@ -80,7 +93,9 @@ export const textMaxLengthValidator = (target, messageIds = {}) => {
|
|||
return [];
|
||||
};
|
||||
|
||||
export const passwordLengthValidator = (target, messageIds = {}) => {
|
||||
export const passwordLengthValidator = (
|
||||
target: HTMLInputElement,
|
||||
messageIds: { [string]: string } = {}): Array<ValidationError> => {
|
||||
const messageIdTooShort = _.has(messageIds, "text_too_short") ?
|
||||
messageIds.text_too_short :
|
||||
"validators.text.text_too_short";
|
||||
|
@ -105,7 +120,9 @@ export const passwordLengthValidator = (target, messageIds = {}) => {
|
|||
return [];
|
||||
};
|
||||
|
||||
export const userInitialsMaxLengthValidator = (target, messageIds = {}) => {
|
||||
export const userInitialsMaxLengthValidator = (
|
||||
target: HTMLInputElement,
|
||||
messageIds: { [string]: string } = {}): Array<ValidationError> => {
|
||||
const messageId = _.has(messageIds, "text_too_long") ?
|
||||
messageIds.text_too_long :
|
||||
"validators.text.text_too_long";
|
||||
|
@ -121,7 +138,9 @@ export const userInitialsMaxLengthValidator = (target, messageIds = {}) => {
|
|||
return [];
|
||||
};
|
||||
|
||||
export const emailValidator = (target, messageIds = {}) => {
|
||||
export const emailValidator = (
|
||||
target: HTMLInputElement,
|
||||
messageIds: { [string]: string } = {}): Array<ValidationError> => {
|
||||
const res = textBlankValidator(target, messageIds);
|
||||
if (res.length > 0) {
|
||||
return res;
|
||||
|
|
|
@ -6,7 +6,10 @@ export default {
|
|||
update: "Update",
|
||||
edit: "Edit",
|
||||
loading: "Loading ...",
|
||||
upload: "Upload"
|
||||
upload: "Upload",
|
||||
about_scinote: "About sciNote",
|
||||
core_version: "sciNote core version",
|
||||
addon_versions: "Addon versions"
|
||||
},
|
||||
page_title: {
|
||||
root: "SciNote",
|
||||
|
@ -204,7 +207,8 @@ export default {
|
|||
tutorials: "Tutorials",
|
||||
release_notes: "Release notes",
|
||||
premium: "Premium",
|
||||
contact_us: "Contact us"
|
||||
contact_us: "Contact us",
|
||||
about_scinote: "About sciNote"
|
||||
},
|
||||
user_account_dropdown: {
|
||||
greeting: "Hi, {name}",
|
||||
|
|
|
@ -8,7 +8,7 @@ import { leaveTeam } from "../../../../../services/api/teams_api";
|
|||
|
||||
import {
|
||||
addTeamsData,
|
||||
setCurrentTeam
|
||||
getCurrentTeam
|
||||
} from "../../../../../components/actions/TeamsActions";
|
||||
|
||||
type Team = {
|
||||
|
@ -23,7 +23,7 @@ type Props = {
|
|||
team: Team,
|
||||
addTeamsData: Function,
|
||||
hideLeaveTeamModal: Function,
|
||||
setCurrentTeam: Function
|
||||
getCurrentTeam: Function
|
||||
};
|
||||
|
||||
class LeaveTeamModal extends Component<Props> {
|
||||
|
@ -41,10 +41,9 @@ class LeaveTeamModal extends Component<Props> {
|
|||
const { id, user_team_id } = this.props.team;
|
||||
leaveTeam(id, user_team_id)
|
||||
.then(response => {
|
||||
const { teams, currentTeam } = response;
|
||||
this.props.updateTeamsState(teams);
|
||||
this.props.addTeamsData(teams);
|
||||
this.props.setCurrentTeam(currentTeam);
|
||||
this.props.updateTeamsState(response);
|
||||
this.props.addTeamsData(response);
|
||||
this.props.getCurrentTeam();
|
||||
})
|
||||
.catch(error => {
|
||||
console.log("error: ", error.response.data.message);
|
||||
|
@ -101,5 +100,5 @@ class LeaveTeamModal extends Component<Props> {
|
|||
|
||||
export default connect(null, {
|
||||
addTeamsData,
|
||||
setCurrentTeam
|
||||
getCurrentTeam
|
||||
})(LeaveTeamModal);
|
||||
|
|
|
@ -12,7 +12,6 @@ import LeaveTeamModal from "./LeaveTeamModal";
|
|||
const DefaultTeam = {
|
||||
id: 0,
|
||||
name: "",
|
||||
current_team: false,
|
||||
user_team_id: 0,
|
||||
role: "",
|
||||
members: 0,
|
||||
|
|
|
@ -44,10 +44,10 @@ TeamsPageDetails.propTypes = {
|
|||
PropTypes.shape({
|
||||
id: number.isRequired,
|
||||
name: string.isRequired,
|
||||
current_team: bool.isRequired,
|
||||
role: string.isRequired,
|
||||
members: number.isRequired,
|
||||
can_be_left: bool.isRequired
|
||||
can_be_left: bool.isRequired,
|
||||
user_team_id: number.isRequired
|
||||
})
|
||||
)
|
||||
};
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from "react";
|
||||
import styled from "styled-components";
|
||||
import { Breadcrumb } from "react-bootstrap";
|
||||
|
@ -36,7 +35,7 @@ class SettingsTeams extends Component<Props, State> {
|
|||
{
|
||||
id: 0,
|
||||
name: "",
|
||||
current_team: true,
|
||||
user_team_id: 0,
|
||||
role: "",
|
||||
members: 0,
|
||||
can_be_left: false
|
||||
|
@ -47,8 +46,8 @@ class SettingsTeams extends Component<Props, State> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
getTeams().then(({ teams }) => {
|
||||
this.updateTeamsState(teams);
|
||||
getTeams().then(response => {
|
||||
this.updateTeamsState(response);
|
||||
});
|
||||
// set team tab on active
|
||||
this.props.tabState("2");
|
||||
|
|
6
app/javascript/src/services/api/configurations_api.js
Normal file
6
app/javascript/src/services/api/configurations_api.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
// @flow
|
||||
import axiosInstance from "./config";
|
||||
import { ABOUT_SCINOTE_PATH } from "./endpoints";
|
||||
|
||||
export const getSciNoteInfo = (): Promise<*> =>
|
||||
axiosInstance.get(ABOUT_SCINOTE_PATH).then(({ data }) => data);
|
|
@ -11,14 +11,15 @@ export const TEAMS_PATH = "/client_api/teams";
|
|||
export const CHANGE_TEAM_PATH = "/client_api/teams/change_team";
|
||||
export const TEAM_DETAILS_PATH = "/client_api/teams/:team_id/details";
|
||||
export const TEAM_UPDATE_PATH = "/client_api/teams/update";
|
||||
export const CURRENT_USER_PATH = "/client_api/current_user_info"
|
||||
export const CURRENT_TEAM_PATH = "/client_api/teams/current_team";
|
||||
|
||||
// users
|
||||
export const USER_PROFILE_INFO = "/client_api/users/profile_info";
|
||||
export const UPDATE_USER_PATH = "/client_api/users/update";
|
||||
export const PREFERENCES_INFO_PATH = "/client_api/users/preferences_info"
|
||||
export const STATISTICS_INFO_PATH = "/client_api/users/statistics_info"
|
||||
export const SIGN_OUT_PATH = "/client_api/users/sign_out_user"
|
||||
export const PREFERENCES_INFO_PATH = "/client_api/users/preferences_info";
|
||||
export const STATISTICS_INFO_PATH = "/client_api/users/statistics_info";
|
||||
export const SIGN_OUT_PATH = "/client_api/users/sign_out_user";
|
||||
export const CURRENT_USER_PATH = "/client_api/current_user_info";
|
||||
|
||||
// info dropdown_title
|
||||
export const CUSTOMER_SUPPORT_LINK = "http://scinote.net/support";
|
||||
|
@ -36,3 +37,6 @@ export const INVITE_USERS_PATH = "/client_api/users/invite_users";
|
|||
|
||||
// settings
|
||||
export const SETTINGS_TEAMS = "/settings/teams";
|
||||
|
||||
// scinote configurations
|
||||
export const ABOUT_SCINOTE_PATH = "/client_api/about_scinote";
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
// @flow
|
||||
import type { Teams$NewTeam, Team$Update } from "flow-typed";
|
||||
import _ from "lodash";
|
||||
import axiosInstance from "./config";
|
||||
import {
|
||||
TEAM_DETAILS_PATH,
|
||||
TEAM_UPDATE_PATH,
|
||||
TEAMS_PATH,
|
||||
CHANGE_TEAM_PATH,
|
||||
LEAVE_TEAM_PATH
|
||||
LEAVE_TEAM_PATH,
|
||||
CURRENT_TEAM_PATH
|
||||
} from "./endpoints";
|
||||
|
||||
export const getTeamDetails = (teamID: number): Promise<*> => {
|
||||
|
@ -24,24 +24,17 @@ export const updateTeam = (teamID: number, teamData: Team$Update): Promise<*> =>
|
|||
.then(({ data }) => data.team);
|
||||
|
||||
export const getTeams = (): Promise<*> =>
|
||||
axiosInstance.get(TEAMS_PATH).then(({ data }) => {
|
||||
const teams = data.teams.collection;
|
||||
const currentTeam = _.find(teams, team => team.current_team);
|
||||
return { teams, currentTeam };
|
||||
});
|
||||
axiosInstance.get(TEAMS_PATH).then(({ data }) => data.teams);
|
||||
|
||||
export const changeCurrentTeam = (teamID: number): Promise<*> =>
|
||||
axiosInstance.post(CHANGE_TEAM_PATH, { team_id: teamID }).then(({ data }) => {
|
||||
const teams = data.teams.collection;
|
||||
const currentTeam = _.find(teams, team => team.current_team);
|
||||
return { teams, currentTeam };
|
||||
});
|
||||
axiosInstance
|
||||
.post(CHANGE_TEAM_PATH, { team_id: teamID })
|
||||
.then(({ data }) => data.teams);
|
||||
|
||||
export const leaveTeam = (teamID: number, userTeamID: number): Promise<*> => {
|
||||
const teamUrl = `${LEAVE_TEAM_PATH}?team=${teamID}&user_team=${userTeamID}`;
|
||||
return axiosInstance.delete(teamUrl).then(({ data }) => {
|
||||
const teams = data.teams.collection;
|
||||
const currentTeam = _.find(teams, team => team.current_team);
|
||||
return { teams, currentTeam };
|
||||
});
|
||||
return axiosInstance.delete(teamUrl).then(({ data }) => data.teams);
|
||||
};
|
||||
|
||||
export const getCurrentTeam = (): Promise<*> =>
|
||||
axiosInstance.get(CURRENT_TEAM_PATH).then(({ data }) => data.team);
|
||||
|
|
|
@ -198,6 +198,7 @@ class User < ApplicationRecord
|
|||
has_many :user_notifications, inverse_of: :user
|
||||
has_many :notifications, through: :user_notifications
|
||||
has_many :zip_exports, inverse_of: :user, dependent: :destroy
|
||||
has_many :datatables_teams, class_name: '::Views::Datatables::DatatablesTeam'
|
||||
|
||||
# If other errors besides parameter "avatar" exist,
|
||||
# they will propagate to "avatar" also, so remove them
|
||||
|
@ -218,25 +219,6 @@ class User < ApplicationRecord
|
|||
Team.find_by_id(self.current_team_id)
|
||||
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_left, user_teams.id AS user_team_id ' \
|
||||
'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
|
||||
# also specify which team to ignore.
|
||||
def self.search(
|
||||
|
|
16
app/models/views/datatables/datatables_team.rb
Normal file
16
app/models/views/datatables/datatables_team.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
module Views
|
||||
module Datatables
|
||||
class DatatablesTeam < ApplicationRecord
|
||||
belongs_to :user
|
||||
default_scope { order(name: :asc) }
|
||||
|
||||
private
|
||||
|
||||
# this isn't strictly necessary, but it will prevent
|
||||
# rails from calling save, which would fail anyway.
|
||||
def readonly?
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -31,7 +31,7 @@ module ClientApi
|
|||
end
|
||||
|
||||
def teams_data
|
||||
{ teams: @user.teams_data }
|
||||
{ teams: @user.datatables_teams }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ module ClientApi
|
|||
|
||||
def teams_data
|
||||
{
|
||||
teams: @user.teams_data,
|
||||
teams: @user.datatables_teams,
|
||||
flash_message: I18n.t('client_api.user_teams.leave_flash',
|
||||
team: @team.name)
|
||||
}
|
||||
|
|
4
app/views/client_api/teams/current_team.json.jbuilder
Normal file
4
app/views/client_api/teams/current_team.json.jbuilder
Normal file
|
@ -0,0 +1,4 @@
|
|||
json.team do
|
||||
json.id team.id
|
||||
json.name team.name
|
||||
end
|
|
@ -1,11 +1,10 @@
|
|||
json.teams do
|
||||
json.collection teams do |team|
|
||||
json.id team.fetch('id')
|
||||
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_left team.fetch('can_be_left')
|
||||
json.user_team_id team.fetch('user_team_id')
|
||||
json.array! teams do |team|
|
||||
json.id team.id
|
||||
json.name team.name
|
||||
json.members team.members
|
||||
json.role retrive_role_name(team.role)
|
||||
json.can_be_left team.can_be_left
|
||||
json.user_team_id team.user_team_id
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,7 +18,7 @@ Rails.application.routes.draw do
|
|||
get '/settings/*all', to: 'client_api/settings#index'
|
||||
|
||||
namespace :client_api, defaults: { format: 'json' } do
|
||||
%i(activities teams notifications users).each do |path|
|
||||
%i(activities teams notifications users configurations).each do |path|
|
||||
draw path
|
||||
end
|
||||
end
|
||||
|
|
2
config/routes/configurations.rb
Normal file
2
config/routes/configurations.rb
Normal file
|
@ -0,0 +1,2 @@
|
|||
# scinote configurations routes
|
||||
get '/about_scinote', to: 'configurations#about_scinote'
|
|
@ -4,6 +4,7 @@ get '/teams', to: 'teams/teams#index'
|
|||
namespace :teams do
|
||||
get '/new', to: 'teams#new'
|
||||
get '/:team_id/details', to: 'teams#details'
|
||||
get '/current_team', to: 'teams#current_team'
|
||||
post '/', to: 'teams#create'
|
||||
post '/change_team', to: 'teams#change_team'
|
||||
post '/update', to: 'teams#update'
|
||||
|
|
5
db/migrate/20171026090804_create_datatables_teams.rb
Normal file
5
db/migrate/20171026090804_create_datatables_teams.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
class CreateDatatablesTeams < ActiveRecord::Migration[5.0]
|
||||
def change
|
||||
create_view :datatables_teams
|
||||
end
|
||||
end
|
1261
db/schema.rb
1261
db/schema.rb
File diff suppressed because it is too large
Load diff
13
db/views/datatables_teams_v01.sql
Normal file
13
db/views/datatables_teams_v01.sql
Normal file
|
@ -0,0 +1,13 @@
|
|||
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.created_by_id = user_teams.user_id THEN false ELSE true END AS can_be_left,
|
||||
user_teams.id AS user_team_id,
|
||||
user_teams.user_id AS user_id
|
||||
FROM teams INNER JOIN user_teams ON teams.id=user_teams.team_id
|
|
@ -1,47 +0,0 @@
|
|||
# feature/login.feature
|
||||
Feature: Log in / Log out
|
||||
As a user with account
|
||||
I want to Log in with my account
|
||||
So that I can use sciNote
|
||||
I want to Log out
|
||||
|
||||
Background:
|
||||
Given the following users is registered:
|
||||
| email | password | team
|
||||
| nonadmin@myorg.com | mypassword1234 | BioSistemika Process
|
||||
|
||||
Scenario: Log in successfully
|
||||
Given I am on Log in page
|
||||
Then I fill in Email "nonadmin@myorg.com" and Password "mypassword1234"
|
||||
And I click on "Log in" button
|
||||
Then I should see "BioSistemika Process"
|
||||
|
||||
Scenario: Unsuccessful Log in
|
||||
Given I am on Log in page
|
||||
Then I do not fill in Email and Password
|
||||
And I click on button "Log in"
|
||||
Then I should see error message "Invalid email or password"
|
||||
|
||||
Scenario: Unsuccessful Log in
|
||||
Given I am on Log in page
|
||||
Then I fill in Email "monday@myorg.com" and Password "monday1234"
|
||||
And I click on button "Log in"
|
||||
Then I should see error message "Invalid email or password"
|
||||
|
||||
Scenario: Unsuccessful Log in
|
||||
Given I am on Log in page
|
||||
Then I fill in Email "nonadmin@myorg.com" and Password "mypassword123344"
|
||||
And I click on button "Log in"
|
||||
Then I should see error message "Invalid email or password"
|
||||
|
||||
Scenario: Unsuccessful Log in
|
||||
Given I am on Log in page
|
||||
Then I fill in Email "monday@myorg.com" and Password "mypassword1234"
|
||||
And I click on button "Log in"
|
||||
Then I should see error message "Invalid email or password"
|
||||
|
||||
Scenario: Successful Log out
|
||||
Given home page of a Karli Novak user
|
||||
Then I click to avatar
|
||||
And I click on "Settings"
|
||||
Then I should see message "Logged out successfully."
|
21
features/navigation/addons_versions_modal.feature
Normal file
21
features/navigation/addons_versions_modal.feature
Normal file
|
@ -0,0 +1,21 @@
|
|||
Feature: Addon versions
|
||||
As a sciNote User
|
||||
I want know what addon are activated
|
||||
So that I know what features are enabled
|
||||
|
||||
Background:
|
||||
Given the "BioSistemika Process" team exists
|
||||
Given the following users are registered
|
||||
| email | password | password_confirmation | full_name | initials |
|
||||
| admin@myorg.com | mypassword1234 | mypassword1234 | Karli Novak | KN |
|
||||
And "admin@myorg.com" is in "BioSistemika Process" team as a "admin"
|
||||
And is signed in with "admin@myorg.com", "mypassword1234"
|
||||
|
||||
@javascript
|
||||
Scenario: Open the sciNote addons modal
|
||||
Given I'm on the profile page
|
||||
And I click "#nav-info-dropdown" icon
|
||||
And I click "About sciNote" link within ".dropdown.open"
|
||||
Then I should see "About sciNote"
|
||||
And I should see "sciNote core version"
|
||||
And I should see "Addon versions"
|
53
features/sessions/log_in.feature
Normal file
53
features/sessions/log_in.feature
Normal file
|
@ -0,0 +1,53 @@
|
|||
Feature: Log in
|
||||
As an existing User
|
||||
I want to Log in with my account
|
||||
So that I can use sciNote
|
||||
|
||||
Background:
|
||||
Given the "BioSistemika Process" team exists
|
||||
Given the following users are registered
|
||||
| email | password | password_confirmation |
|
||||
| night.slarker@gmail.com | mypassword1234 | mypassword1234 |
|
||||
And "night.slarker@gmail.com" is in "BioSistemika Process" team as a "admin"
|
||||
|
||||
@javascript
|
||||
Scenario: Successful Log in
|
||||
Given I am on Log in page
|
||||
Then I fill in Email "night.slarker@gmail.com" and Password "mypassword1234"
|
||||
And I click "Log in" button
|
||||
Then I should see "BioSistemika Process"
|
||||
|
||||
@javascript
|
||||
Scenario: Unsuccessful Log in
|
||||
Given I am on Log in page
|
||||
And I click "Log in" button
|
||||
Then I should see "Invalid Email or password." flash message
|
||||
|
||||
@javascript
|
||||
Scenario: Unsuccessful Log in
|
||||
Given I am on Log in page
|
||||
Then I fill in Email "monday@myorg.com" and Password "monday1234"
|
||||
And I click "Log in" button
|
||||
Then I should see "Invalid Email or password." flash message
|
||||
|
||||
@javascript
|
||||
Scenario: Unsuccessful Log in
|
||||
Given I am on Log in page
|
||||
Then I fill in Email "night.slarker@gmail.com" and Password "mypassword123455"
|
||||
And I click "Log in" button
|
||||
Then I should see "Invalid Email or password." flash message
|
||||
|
||||
@javascript
|
||||
Scenario: Unsuccessful Log in
|
||||
Given I am on Log in page
|
||||
Then I fill in Email "monday@myorg.com" and Password "mypassword1234"
|
||||
And I click "Log in" button
|
||||
Then I should see "Invalid Email or password." flash message
|
||||
|
||||
@javascript
|
||||
Scenario: Successful Log out
|
||||
Given "night.slarker@gmail.com" is signed in with "mypassword1234"
|
||||
And I'm on the home page of "BioSistemika Process" team
|
||||
Then I click on "#user-account-dropdown" element
|
||||
And I click "Log out" link within dropdown menu
|
||||
Then I should see "Logged out successfully." flash message
|
|
@ -9,13 +9,13 @@ Background:
|
|||
| email | password | password_confirmation | full_name | initials |
|
||||
| nonadmin@myorg.com | mypassword1234 | mypassword1234 | Karli Novak | KN |
|
||||
And "nonadmin@myorg.com" is in "BioSistemika Process" team as a "normal_user"
|
||||
And is signed in with "nonadmin@myorg.com", "mypassword1234"
|
||||
And "nonadmin@myorg.com" is signed in with "mypassword1234"
|
||||
|
||||
@javascript
|
||||
Scenario: Successful navigate to profile page
|
||||
Given I'm on the home page of "BioSistemika Process" team
|
||||
And I click on Avatar
|
||||
And I click "Settings" link within "user-account-dropdown"
|
||||
And I click "Settings" link within "#user-account-dropdown"
|
||||
Then I should see "My Profile"
|
||||
|
||||
@javascript
|
||||
|
|
|
@ -13,7 +13,7 @@ Feature: Team settings
|
|||
And "karli@myorg.com" is in "BioSistemika Process" team as a "admin"
|
||||
And "marija@myorg.com" is in "BioSistemika Process" team as a "normal_user"
|
||||
And "suazana@myorg.com" is in "BioSistemika Process" team as a "guest"
|
||||
And is signed in with "karli@myorg.com", "mypassword1234"
|
||||
And "karli@myorg.com" is signed in with "mypassword1234"
|
||||
|
||||
@javascript
|
||||
Scenario: Successfully changes team name
|
||||
|
|
|
@ -15,7 +15,13 @@ Given(/^I click "(.+)" link$/) do |link|
|
|||
end
|
||||
|
||||
Given(/^I click "(.+)" link within "(.+)"$/) do |link, element|
|
||||
within("##{element}") do
|
||||
within(element) do
|
||||
click_link link
|
||||
end
|
||||
end
|
||||
|
||||
Then(/^I click "(.+)" link within dropdown menu$/) do |link|
|
||||
within('.dropdown-menu') do
|
||||
click_link link
|
||||
end
|
||||
end
|
||||
|
@ -74,7 +80,7 @@ end
|
|||
|
||||
Then(/^I should see "([^"]*)" flash message$/) do |message|
|
||||
wait_for_ajax
|
||||
expect(find_by_id('alert-flash')).to have_content(message)
|
||||
expect(find('.alert')).to have_content(message)
|
||||
end
|
||||
|
||||
Then(/^I click on Edit on "([^"]*)" input field$/) do |container_id|
|
||||
|
@ -94,6 +100,10 @@ Then(/^I should see "([^"]*)" in "([^"]*)" input field$/) do |text, container_id
|
|||
expect(container).to have_xpath("//input[@value='#{text}']")
|
||||
end
|
||||
|
||||
Given("I click {string} icon") do |id|
|
||||
find(:css, id).click
|
||||
end
|
||||
|
||||
Then(/^(?:|I )click on "([^"]*)" element$/) do |selector|
|
||||
find(selector).click
|
||||
end
|
||||
|
@ -116,5 +126,6 @@ Then(/^I change "([^"]*)" with "([^"]*)" in "([^"]*)" textarea field$/) do |old_
|
|||
end
|
||||
|
||||
Then(/^I should see "([^"]*)" on "([^"]*)" element$/) do |text, element|
|
||||
wait_for_ajax
|
||||
expect(find(element)).to have_content(text)
|
||||
end
|
||||
|
|
|
@ -17,7 +17,7 @@ Then(/^I fill the sign up form with$/) do |table|
|
|||
end
|
||||
end
|
||||
|
||||
Given(/^is signed in with "([^"]*)", "([^"]*)"$/) do |email, password|
|
||||
Given(/^"([^"]*)" is signed in with "([^"]*)"$/) do |email, password|
|
||||
visit '/users/sign_in'
|
||||
fill_in 'user_email', with: email
|
||||
fill_in 'user_password', with: password
|
||||
|
@ -46,3 +46,12 @@ end
|
|||
Then("I should be on Change your password page") do
|
||||
expect(page).to have_current_path(edit_user_password_path, only_path: true)
|
||||
end
|
||||
|
||||
Given(/^I am on Log in page$/) do
|
||||
visit '/users/sign_in'
|
||||
end
|
||||
|
||||
Then(/^I fill in Email "([^"]*)" and Password "([^"]*)"$/) do |email, password|
|
||||
fill_in 'user_email', with: email
|
||||
fill_in 'user_password', with: password
|
||||
end
|
||||
|
|
6
flow-typed/npm/axios_v0.16.x.js
vendored
6
flow-typed/npm/axios_v0.16.x.js
vendored
|
@ -1,5 +1,5 @@
|
|||
// flow-typed signature: c788eedb73f0df0fed02bf99c0b49bcc
|
||||
// flow-typed version: 2adcdf60cc/axios_v0.16.x/flow_>=v0.25.x
|
||||
// flow-typed signature: 783541c5bb930cc2cb39610705a4adc1
|
||||
// flow-typed version: d84de54b07/axios_v0.16.x/flow_>=v0.25.x
|
||||
|
||||
declare module 'axios' {
|
||||
declare interface ProxyConfig {
|
||||
|
@ -60,7 +60,7 @@ declare module 'axios' {
|
|||
declare class AxiosXHR<T> {
|
||||
config: AxiosXHRConfig<T>;
|
||||
data: T;
|
||||
headers: Object;
|
||||
headers?: Object;
|
||||
status: number;
|
||||
statusText: string,
|
||||
request: http$ClientRequest | XMLHttpRequest
|
||||
|
|
3470
flow-typed/npm/lodash_v4.x.x.js
vendored
3470
flow-typed/npm/lodash_v4.x.x.js
vendored
File diff suppressed because it is too large
Load diff
39
flow-typed/npm/prettier-eslint_vx.x.x.js
vendored
Normal file
39
flow-typed/npm/prettier-eslint_vx.x.x.js
vendored
Normal file
|
@ -0,0 +1,39 @@
|
|||
// flow-typed signature: ace44a98a89509a513ab899eea5ba4fd
|
||||
// flow-typed version: <<STUB>>/prettier-eslint_v^8.2.1/flow_v0.56.0
|
||||
|
||||
/**
|
||||
* This is an autogenerated libdef stub for:
|
||||
*
|
||||
* 'prettier-eslint'
|
||||
*
|
||||
* Fill this stub out by replacing all the `any` types.
|
||||
*
|
||||
* Once filled out, we encourage you to share your work with the
|
||||
* community by sending a pull request to:
|
||||
* https://github.com/flowtype/flow-typed
|
||||
*/
|
||||
|
||||
declare module 'prettier-eslint' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* We include stubs for each file inside this npm package in case you need to
|
||||
* require those files directly. Feel free to delete any files that aren't
|
||||
* needed.
|
||||
*/
|
||||
declare module 'prettier-eslint/dist/index' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'prettier-eslint/dist/utils' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
// Filename aliases
|
||||
declare module 'prettier-eslint/dist/index.js' {
|
||||
declare module.exports: $Exports<'prettier-eslint/dist/index'>;
|
||||
}
|
||||
declare module 'prettier-eslint/dist/utils.js' {
|
||||
declare module.exports: $Exports<'prettier-eslint/dist/utils'>;
|
||||
}
|
4
flow-typed/npm/react-intl_v2.x.x.js
vendored
4
flow-typed/npm/react-intl_v2.x.x.js
vendored
|
@ -1,5 +1,5 @@
|
|||
// flow-typed signature: 4c4c0d4f407d88878f9e0b815c57c823
|
||||
// flow-typed version: 97b6f00328/react-intl_v2.x.x/flow_>=v0.53.x
|
||||
// flow-typed signature: ffba6b43bdc8cce76dba8a7d4fb7b539
|
||||
// flow-typed version: cc3eacb5a2/react-intl_v2.x.x/flow_>=v0.53.x <=v0.56.x
|
||||
|
||||
/**
|
||||
* Original implementation of this file by @marudor at https://github.com/marudor/flowInterfaces
|
||||
|
|
6
flow-typed/teams_types.js
vendored
6
flow-typed/teams_types.js
vendored
|
@ -24,9 +24,13 @@ export type Teams$NewTeam = {
|
|||
export type Teams$Team = {
|
||||
id: number,
|
||||
name: string,
|
||||
current_team: boolean,
|
||||
role: string,
|
||||
members: number,
|
||||
can_be_left: boolean,
|
||||
user_team_id: number
|
||||
};
|
||||
|
||||
export type Teams$CurrentTeam = {
|
||||
id: number,
|
||||
name: string
|
||||
}
|
||||
|
|
14
flow-typed/types.js
vendored
14
flow-typed/types.js
vendored
|
@ -16,6 +16,20 @@ export type Alert = {
|
|||
timeout: number
|
||||
};
|
||||
|
||||
export type ValidationErrorSimple = {|
|
||||
message: string
|
||||
|};
|
||||
|
||||
export type ValidationErrorIntl = {|
|
||||
intl: boolean,
|
||||
messageId: string,
|
||||
values: string
|
||||
|};
|
||||
|
||||
export type ValidationError = ValidationErrorSimple | ValidationErrorIntl;
|
||||
|
||||
export type ValidationErrors = string | Array<string> | Array<ValidationError>;
|
||||
|
||||
export type Activity = {
|
||||
id: number,
|
||||
message: string,
|
||||
|
|
1505
package-lock.json
generated
1505
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -33,6 +33,7 @@
|
|||
"flow-bin": "^0.56.0",
|
||||
"flow-typed": "^2.1.5",
|
||||
"prettier": "^1.7.0",
|
||||
"prettier-eslint": "^8.2.1",
|
||||
"webpack-dev-server": "^2.5.1"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -95,4 +96,4 @@
|
|||
"webpack-manifest-plugin": "^1.1.2",
|
||||
"webpack-merge": "^4.1.0"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe ClientApi::ConfigurationsController, type: :controller do
|
||||
login_user
|
||||
|
||||
describe '#about_scinote' do
|
||||
let(:subject) { get :about_scinote, format: :json }
|
||||
it { is_expected.to be_success }
|
||||
end
|
||||
end
|
|
@ -128,4 +128,9 @@ describe ClientApi::Teams::TeamsController, type: :controller do
|
|||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #current_team' do
|
||||
let(:subject) { get :current_team, as: :json }
|
||||
it { is_expected.to have_http_status(:ok) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
FactoryGirl.define do
|
||||
factory :my_module_group do
|
||||
name Faker::Name.unique.name
|
||||
experiment { Experiment.first || create(:experiment_two) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,7 +7,6 @@ describe MyModuleGroup, type: :model do
|
|||
|
||||
describe 'Database table' do
|
||||
it { should have_db_column :id }
|
||||
it { should have_db_column :name }
|
||||
it { should have_db_column :created_at }
|
||||
it { should have_db_column :updated_at }
|
||||
it { should have_db_column :created_by_id }
|
||||
|
@ -21,8 +20,6 @@ describe MyModuleGroup, type: :model do
|
|||
end
|
||||
|
||||
describe 'Should be a valid object' do
|
||||
it { should validate_presence_of :name }
|
||||
it { should validate_presence_of :experiment }
|
||||
it { should validate_length_of(:name).is_at_most(Constants::NAME_MAX_LENGTH) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -152,40 +152,10 @@ describe User, type: :model do
|
|||
end
|
||||
let(:user_two) { create :user, email: 'user2@asdf.com' }
|
||||
|
||||
it 'in a specific format: {id: .., name: .., members: .., role: ' \
|
||||
'.., current_team: .., can_be_left: ..}' do
|
||||
user_team = 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_left: false,
|
||||
user_team_id: user_team.id
|
||||
}
|
||||
|
||||
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
|
||||
user_team = create :user_team, team: team, user: user_one
|
||||
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_left: true,
|
||||
user_team_id: user_team.id
|
||||
}
|
||||
|
||||
user_one.teams_data.first.each do |k, v|
|
||||
expect(v).to eq(expected_result.fetch(k.to_sym))
|
||||
end
|
||||
expect(user_one.datatables_teams.first.members).to eq 2
|
||||
end
|
||||
end
|
||||
|
||||
|
|
23
spec/models/views/datatables/teams_spec.rb
Normal file
23
spec/models/views/datatables/teams_spec.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Views::Datatables::DatatablesTeam, type: :model do
|
||||
describe 'Database table' do
|
||||
it { should have_db_column :id }
|
||||
it { should have_db_column :name }
|
||||
it { should have_db_column :members }
|
||||
it { should have_db_column :role }
|
||||
it { should have_db_column :user_team_id }
|
||||
it { should have_db_column :user_id }
|
||||
it { should have_db_column :can_be_left }
|
||||
end
|
||||
|
||||
describe 'is readonly' do
|
||||
let(:user) { create :user }
|
||||
it do
|
||||
expect {
|
||||
Views::Datatables::DatatablesTeam.create!(user_id: user.id)
|
||||
}.to raise_error(ActiveRecord::ReadOnlyRecord,
|
||||
'Views::Datatables::DatatablesTeam is marked as readonly')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -55,10 +55,11 @@ describe ClientApi::TeamsService do
|
|||
ClientApi::TeamsService.new(current_user: user_one, team_id: team_one.id)
|
||||
end
|
||||
|
||||
it 'should return user teams' do
|
||||
it 'should return an array of valid teams' do
|
||||
create :user_team, user: user_one, team: team_one
|
||||
data = team_service.teams_data.fetch(:teams)
|
||||
expect(data.first.fetch('name')).to eq team_one.name
|
||||
expect(team_service.teams_data).to(
|
||||
match_response_schema('datatables_teams')
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -130,7 +130,7 @@ describe ClientApi::UserTeamService do
|
|||
user_team_id: user_team.id,
|
||||
role: 1
|
||||
)
|
||||
team_id = ut_service.teams_data[:teams].first.fetch('id')
|
||||
team_id = ut_service.teams_data[:teams].first.id
|
||||
expect(team_id).to eq team_one.id
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,6 +18,7 @@ require 'simplecov'
|
|||
require 'faker'
|
||||
require 'active_record'
|
||||
require 'bullet'
|
||||
require "json_matchers/rspec"
|
||||
|
||||
RSpec.configure do |config|
|
||||
# rspec-expectations config goes here. You can use an alternate
|
||||
|
|
21
spec/support/api/schemas/datatables_teams.json
Normal file
21
spec/support/api/schemas/datatables_teams.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"type": "object",
|
||||
"required": ["teams"],
|
||||
"properties": {
|
||||
"teams": {
|
||||
"type": "array",
|
||||
"items":{
|
||||
"required": ["id", "name", "members", "role", "can_be_left", "user_team_id"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"name": { "type": "string" },
|
||||
"members": { "type": "integer" },
|
||||
"role": { "type": "integer" },
|
||||
"can_be_left": { "type": "boolean" },
|
||||
"user_team_id": { "type": "integer" },
|
||||
"user_id": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
117
yarn.lock
117
yarn.lock
|
@ -102,7 +102,7 @@ ansi-html@0.0.7:
|
|||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e"
|
||||
|
||||
ansi-regex@^2.0.0:
|
||||
ansi-regex@^2.0.0, ansi-regex@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
|
||||
|
||||
|
@ -114,7 +114,7 @@ ansi-styles@^2.2.1:
|
|||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
|
||||
|
||||
ansi-styles@^3.1.0:
|
||||
ansi-styles@^3.0.0, ansi-styles@^3.1.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88"
|
||||
dependencies:
|
||||
|
@ -1424,6 +1424,12 @@ combined-stream@^1.0.5, combined-stream@~1.0.5:
|
|||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
common-tags@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.4.0.tgz#1187be4f3d4cf0c0427d43f74eef1f73501614c0"
|
||||
dependencies:
|
||||
babel-runtime "^6.18.0"
|
||||
|
||||
commondir@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
|
||||
|
@ -1874,6 +1880,10 @@ diffie-hellman@^5.0.0:
|
|||
miller-rabin "^4.0.0"
|
||||
randombytes "^2.0.0"
|
||||
|
||||
dlv@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.0.tgz#fee1a7c43f63be75f3f679e85262da5f102764a7"
|
||||
|
||||
dns-equal@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d"
|
||||
|
@ -2163,6 +2173,48 @@ eslint-scope@^3.7.1:
|
|||
esrecurse "^4.1.0"
|
||||
estraverse "^4.1.1"
|
||||
|
||||
eslint@^4.5.0:
|
||||
version "4.10.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.10.0.tgz#f25d0d7955c81968c2309aa5c9a229e045176bb7"
|
||||
dependencies:
|
||||
ajv "^5.2.0"
|
||||
babel-code-frame "^6.22.0"
|
||||
chalk "^2.1.0"
|
||||
concat-stream "^1.6.0"
|
||||
cross-spawn "^5.1.0"
|
||||
debug "^3.0.1"
|
||||
doctrine "^2.0.0"
|
||||
eslint-scope "^3.7.1"
|
||||
espree "^3.5.1"
|
||||
esquery "^1.0.0"
|
||||
estraverse "^4.2.0"
|
||||
esutils "^2.0.2"
|
||||
file-entry-cache "^2.0.0"
|
||||
functional-red-black-tree "^1.0.1"
|
||||
glob "^7.1.2"
|
||||
globals "^9.17.0"
|
||||
ignore "^3.3.3"
|
||||
imurmurhash "^0.1.4"
|
||||
inquirer "^3.0.6"
|
||||
is-resolvable "^1.0.0"
|
||||
js-yaml "^3.9.1"
|
||||
json-stable-stringify "^1.0.1"
|
||||
levn "^0.3.0"
|
||||
lodash "^4.17.4"
|
||||
minimatch "^3.0.2"
|
||||
mkdirp "^0.5.1"
|
||||
natural-compare "^1.4.0"
|
||||
optionator "^0.8.2"
|
||||
path-is-inside "^1.0.2"
|
||||
pluralize "^7.0.0"
|
||||
progress "^2.0.0"
|
||||
require-uncached "^1.0.3"
|
||||
semver "^5.3.0"
|
||||
strip-ansi "^4.0.0"
|
||||
strip-json-comments "~2.0.1"
|
||||
table "^4.0.1"
|
||||
text-table "~0.2.0"
|
||||
|
||||
eslint@^4.7.2:
|
||||
version "4.8.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.8.0.tgz#229ef0e354e0e61d837c7a80fdfba825e199815e"
|
||||
|
@ -3012,6 +3064,10 @@ indent-string@^2.1.0:
|
|||
dependencies:
|
||||
repeating "^2.0.0"
|
||||
|
||||
indent-string@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289"
|
||||
|
||||
indexes-of@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
|
||||
|
@ -3631,6 +3687,10 @@ lodash.memoize@^4.1.2:
|
|||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
|
||||
|
||||
lodash.merge@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5"
|
||||
|
||||
lodash.mergewith@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55"
|
||||
|
@ -3643,6 +3703,10 @@ lodash.tail@^4.1.1:
|
|||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664"
|
||||
|
||||
lodash.unescape@4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c"
|
||||
|
||||
lodash.uniq@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||
|
@ -3651,6 +3715,13 @@ lodash.uniq@^4.5.0:
|
|||
version "4.17.4"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
|
||||
|
||||
loglevel-colored-level-prefix@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/loglevel-colored-level-prefix/-/loglevel-colored-level-prefix-1.0.0.tgz#6a40218fdc7ae15fc76c3d0f3e676c465388603e"
|
||||
dependencies:
|
||||
chalk "^1.1.3"
|
||||
loglevel "^1.4.1"
|
||||
|
||||
loglevel@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.4.1.tgz#95b383f91a3c2756fd4ab093667e4309161f2bcd"
|
||||
|
@ -4827,10 +4898,33 @@ preserve@^0.2.0:
|
|||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
|
||||
|
||||
prettier@^1.7.0:
|
||||
prettier-eslint@^8.2.1:
|
||||
version "8.2.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier-eslint/-/prettier-eslint-8.2.1.tgz#cd66cf8b1a2c2fce2217f1b28474809031b9a77c"
|
||||
dependencies:
|
||||
common-tags "^1.4.0"
|
||||
dlv "^1.1.0"
|
||||
eslint "^4.5.0"
|
||||
indent-string "^3.2.0"
|
||||
lodash.merge "^4.6.0"
|
||||
loglevel-colored-level-prefix "^1.0.0"
|
||||
prettier "^1.7.1"
|
||||
pretty-format "^20.0.3"
|
||||
require-relative "^0.8.7"
|
||||
typescript "^2.5.1"
|
||||
typescript-eslint-parser "^8.0.0"
|
||||
|
||||
prettier@^1.7.0, prettier@^1.7.1:
|
||||
version "1.7.4"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.7.4.tgz#5e8624ae9363c80f95ec644584ecdf55d74f93fa"
|
||||
|
||||
pretty-format@^20.0.3:
|
||||
version "20.0.3"
|
||||
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-20.0.3.tgz#020e350a560a1fe1a98dc3beb6ccffb386de8b14"
|
||||
dependencies:
|
||||
ansi-regex "^2.1.1"
|
||||
ansi-styles "^3.0.0"
|
||||
|
||||
private@^0.1.6, private@^0.1.7:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1"
|
||||
|
@ -5391,6 +5485,10 @@ require-main-filename@^1.0.1:
|
|||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
|
||||
|
||||
require-relative@^0.8.7:
|
||||
version "0.8.7"
|
||||
resolved "https://registry.yarnpkg.com/require-relative/-/require-relative-0.8.7.tgz#7999539fc9e047a37928fa196f8e1563dabd36de"
|
||||
|
||||
require-uncached@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3"
|
||||
|
@ -5545,7 +5643,7 @@ selfsigned@^1.9.1:
|
|||
dependencies:
|
||||
node-forge "0.6.33"
|
||||
|
||||
"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0:
|
||||
"semver@2 || 3 || 4 || 5", semver@5.4.1, semver@^5.1.0, semver@^5.3.0:
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
|
||||
|
||||
|
@ -6095,6 +6193,17 @@ typedarray@^0.0.6:
|
|||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||
|
||||
typescript-eslint-parser@^8.0.0:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint-parser/-/typescript-eslint-parser-8.0.1.tgz#e8cac537d996e16c3dbb0d7c4d509799e67afe0c"
|
||||
dependencies:
|
||||
lodash.unescape "4.0.1"
|
||||
semver "5.4.1"
|
||||
|
||||
typescript@^2.5.1:
|
||||
version "2.5.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.3.tgz#df3dcdc38f3beb800d4bc322646b04a3f6ca7f0d"
|
||||
|
||||
ua-parser-js@^0.7.9:
|
||||
version "0.7.14"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.14.tgz#110d53fa4c3f326c121292bbeac904d2e03387ca"
|
||||
|
|
Loading…
Reference in a new issue