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

This commit is contained in:
zmagod 2017-11-09 15:55:10 +01:00
commit 327e27e123
65 changed files with 4249 additions and 3123 deletions

View file

@ -16,6 +16,7 @@
"flow"
],
"plugins": [
"transform-flow-strip-types",
"transform-object-rest-spread",
"syntax-dynamic-import",
"transform-react-jsx-source",

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

@ -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" />&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">
<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" />&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">
<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;

View file

@ -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" />&nbsp;
<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" />&nbsp;{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
);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,7 +12,6 @@ import LeaveTeamModal from "./LeaveTeamModal";
const DefaultTeam = {
id: 0,
name: "",
current_team: false,
user_team_id: 0,
role: "",
members: 0,

View file

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

View file

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

View 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);

View file

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

View file

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

View file

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

View 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

View file

@ -31,7 +31,7 @@ module ClientApi
end
def teams_data
{ teams: @user.teams_data }
{ teams: @user.datatables_teams }
end
end

View file

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

View file

@ -0,0 +1,4 @@
json.team do
json.id team.id
json.name team.name
end

View file

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

View file

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

View file

@ -0,0 +1,2 @@
# scinote configurations routes
get '/about_scinote', to: 'configurations#about_scinote'

View file

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

View file

@ -0,0 +1,5 @@
class CreateDatatablesTeams < ActiveRecord::Migration[5.0]
def change
create_view :datatables_teams
end
end

File diff suppressed because it is too large Load diff

View 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

View file

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

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View 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'>;
}

View file

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

View file

@ -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
View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View 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
View file

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