Merge branch 'plugin/settings-manager-dev' of github.com:soupette/strapi into plugin/settings-manager-dev

This commit is contained in:
Jim Laurie 2017-07-12 16:38:56 +02:00
commit 1b0e48014d
25 changed files with 698 additions and 31 deletions

View File

@ -0,0 +1,125 @@
/**
*
* InputNumber
* Customization
* - deactivateErrorHighlight: bool
* allow the user to remove bootstrap class 'has-danger' on the inputText
* - customBootstrapClass : string
* overrides the default 'col-md-6' on the inputText
* - handleBlur: function
* overrides the default input validations
* - errors : array
* custom errors if set to false it deactivate error display
*
* Required
* - name : string
* - handleChange : function
* - value : string
* - validations : object
*
* Optionnal
* - description : input description
* - handleFocus : function
* - placeholder : string if set to "" nothing will display
*
*/
import React from 'react';
import styles from './styles.scss';
class InputNumber extends React.Component { // eslint-disable-line react/prefer-stateless-function
constructor(props) {
super(props);
this.state = {
errors: false,
hasInitialValue: false,
};
}
componentDidMount() {
if (this.props.value && this.props.value.length !== '') {
this.setState({ hasInitialValue: true });
}
}
componentWillReceiveProps(nextProps) {
if (this.props.errors !== nextProps.errors) {
let errors = false;
if (_.isEmpty(nextProps.errors)) {
errors = nextProps.errors === true ? [] : false;
} else {
errors = nextProps.errors;
}
this.setState({ errors });
}
}
handleBlur = ({ target }) => {
// prevent error display if input is initially empty
if (target.value.length > 0 || this.state.hasInitialValue) {
// validates basic string validations
// add custom logic here such as alerts...
const errors = this.validate(target.value);
this.setState({ errors, hasInitialValue: true });
}
}
validate = (value) => {
const errors = !_.isEmpty(_.pick(this.props.validations, 'required')) && value.length > 0 ?
false : ['This field is required'];
return errors;
}
render() {
const inputValue = this.props.value || '';
// override default onBlur
const handleBlur = this.props.handleBlur || this.handleBlur;
// override bootStrapClass
const bootStrapClass = this.props.customBootstrapClass ? this.props.customBootstrapClass : 'col-md-4';
// set error class with override possibility
const bootStrapClassDanger = !this.props.deactivateErrorHighlight && this.state.errors ? 'has-danger' : '';
const placeholder = this.props.placeholder || `Change ${this.props.name} field`;
return (
<div className={`${styles.inputNumber} ${bootStrapClass} ${bootStrapClassDanger}`}>
<label htmlFor={this.props.name}>{this.props.name}</label>
<input
type="number"
name={this.props.name}
id={this.props.name}
value={inputValue}
onBlur={handleBlur}
onChange={this.props.handleChange}
onFocus={this.props.handleFocus}
className={`form-control ${this.state.errors? 'form-control-danger' : ''}`}
placeholder={placeholder}
/>
<small>{this.props.inputDescription}</small>
{_.map(this.state.errors, (error, key) => (
<div key={key} className="form-control-feedback">{error}</div>
))}
</div>
);
}
}
InputNumber.propTypes = {
customBootstrapClass: React.PropTypes.string,
deactivateErrorHighlight: React.PropTypes.bool,
errors: React.PropTypes.oneOfType([
React.PropTypes.bool,
React.PropTypes.array,
]),
handleBlur: React.PropTypes.func,
handleChange: React.PropTypes.func.isRequired,
handleFocus: React.PropTypes.func,
inputDescription: React.PropTypes.string,
name: React.PropTypes.string.isRequired,
placeholder: React.PropTypes.string,
validations: React.PropTypes.object.isRequired,
value: React.PropTypes.oneOfType([
React.PropTypes.number.isRequired,
React.PropTypes.string.isRequired,
]),
}
export default InputNumber;

View File

@ -0,0 +1,13 @@
/*
* InputNumber Messages
*
* This contains all the text for the InputNumber component.
*/
import { defineMessages } from 'react-intl';
export default defineMessages({
header: {
id: 'app.components.InputNumber.header',
defaultMessage: 'This is the InputNumber component !',
},
});

View File

@ -0,0 +1,11 @@
.inputNumber { /* stylelint-disable */
margin-top: 1.8rem;
> label {
text-transform: capitalize;
}
> small {
margin-top: .5rem;
display: block;
color: #ABAFBB;
}
}

View File

@ -0,0 +1,11 @@
// import InputNumber from '../index';
import expect from 'expect';
// import { shallow } from 'enzyme';
// import React from 'react';
describe('<InputNumber />', () => {
it('Expect to have unit tests specified', () => {
expect(true).toEqual(false);
});
});

View File

@ -0,0 +1,154 @@
/**
*
* InputText
* Customization
* - deactivateErrorHighlight: bool
* allow the user to remove bootstrap class 'has-danger' on the inputText
* - customBootstrapClass : string
* overrides the default 'col-md-6' on the inputText
* - handleBlur: function
* overrides the default input validations
* - errors : array
* custom errors if set to false it deactivate error display
*
* Required
* - name : string
* - handleChange : function
* - value : string
* - validations : object
*
* Optionnal
* - description : input description
* - handleFocus : function
* - placeholder : string if set to "" nothing will display
*/
import React from 'react';
import styles from './styles.scss';
class InputText extends React.Component { // eslint-disable-line react/prefer-stateless-function
constructor(props) {
super(props);
this.state = {
errors: false,
hasInitialValue: false,
};
}
componentDidMount() {
if (this.props.value && this.props.value.length > 0) {
this.setState({ hasInitialValue: true });
}
}
componentWillReceiveProps(nextProps) {
if (this.props.errors !== nextProps.errors) {
let errors = false;
if (_.isEmpty(nextProps.errors)) {
errors = nextProps.errors === true ? [] : false;
} else {
errors = nextProps.errors;
}
this.setState({ errors });
}
}
handleBlur = ({ target }) => {
// prevent error display if input is initially empty
if (target.value.length > 0 || this.state.hasInitialValue) {
// validates basic string validations
// add custom logic here such as alerts...
const errors = this.validate(target.value);
this.setState({ errors, hasInitialValue: true });
}
}
// Basic string validations
validate = (value) => {
let errors = [];
const requiredError = 'Field is required';
_.mapKeys(this.props.validations, (validationValue, validationKey) => {
switch (validationKey) {
case 'maxLength':
if (value.length > validationValue) {
errors.push('Field is too long');
}
break;
case 'minLength':
if (value.length < validationValue) {
errors.push('Field is too short');
}
break;
case 'required':
if (value.length === 0) {
errors.push(requiredError);
}
break;
case 'regex':
if (!validationValue.test(value)) {
errors.push('Field is not valid');
}
break;
default:
errors = false;
}
});
if (_.isEmpty(errors)) {
errors = false;
} else if (_.includes(errors, requiredError)) {
errors = _.reject(errors, (error) => error !== requiredError);
}
return errors;
}
render() {
const inputValue = this.props.value || '';
// override default onBlur
const handleBlur = this.props.handleBlur || this.handleBlur;
// override bootStrapClass
const bootStrapClass = this.props.customBootstrapClass ? this.props.customBootstrapClass : 'col-md-6';
// set error class with override possibility
const bootStrapClassDanger = !this.props.deactivateErrorHighlight && this.state.errors ? 'has-danger' : '';
const placeholder = this.props.placeholder || `Change ${this.props.name} field`;
return (
<div className={`${styles.inputText} ${bootStrapClass} ${bootStrapClassDanger}`}>
<label htmlFor={this.props.name}>{this.props.name}</label>
<input
name={this.props.name}
id={this.props.name}
onBlur={handleBlur}
onFocus={this.props.handleFocus}
onChange={this.props.handleChange}
value={inputValue}
type="text"
className={`form-control ${this.state.errors? 'form-control-danger' : ''}`}
placeholder={placeholder}
/>
<small>{this.props.inputDescription}</small>
{_.map(this.state.errors, (error, key) => (
<div key={key} className="form-control-feedback">{error}</div>
))}
</div>
);
}
}
InputText.propTypes = {
customBootstrapClass: React.PropTypes.string,
deactivateErrorHighlight: React.PropTypes.bool,
errors: React.PropTypes.oneOfType([
React.PropTypes.bool,
React.PropTypes.array,
]),
handleBlur: React.PropTypes.func,
handleChange: React.PropTypes.func.isRequired,
handleFocus: React.PropTypes.func,
inputDescription: React.PropTypes.string,
name: React.PropTypes.string.isRequired,
placeholder: React.PropTypes.string,
validations: React.PropTypes.object.isRequired,
value: React.PropTypes.string.isRequired,
}
export default InputText;

View File

@ -0,0 +1,13 @@
/*
* InputText Messages
*
* This contains all the text for the InputText component.
*/
import { defineMessages } from 'react-intl';
export default defineMessages({
header: {
id: 'app.components.InputText.header',
defaultMessage: 'This is the InputText component !',
},
});

View File

@ -0,0 +1,11 @@
.inputText { /* stylelint-disable */
margin-top: 1.8rem;
> label {
text-transform: capitalize;
}
> small {
margin-top: .5rem;
display: block;
color: #ABAFBB;
}
}

View File

@ -0,0 +1,11 @@
// import InputText from '../index';
import expect from 'expect';
// import { shallow } from 'enzyme';
// import React from 'react';
describe('<InputText />', () => {
it('Expect to have unit tests specified', () => {
expect(true).toEqual(false);
});
});

View File

@ -0,0 +1,69 @@
/**
*
* InputToggle
* Customization
* - customBootstrapClass : string
* overrides the default col-md-4 class
*
* Required
* - handleChange: function
* - name: string
* - isChecked: bool
*/
import React from 'react';
import styles from './styles.scss';
class InputToggle extends React.Component { // eslint-disable-line react/prefer-stateless-function
constructor(props) {
super(props);
this.state = {
isChecked: false,
};
}
componentDidMount() {
const isChecked = this.props.isChecked ? this.props.isChecked : false;
this.setState({ isChecked });
}
toggle = (e) => {
let isChecked = this.state.isChecked;
// prevent the toggle if the user clicks on the already selected input
if (e.target.id === "on" && !this.state.isChecked) {
isChecked = true;
} else if (e.target.id === "off" && this.state.isChecked) {
isChecked = false;
}
const target = {
name: this.props.name,
value: isChecked,
};
this.setState({ isChecked });
this.props.handleChange({target});
}
render() {
const btnClassOff = this.state.isChecked ? 'btn ' : `btn ${styles.gradientOff}`;
const btnClassOn = this.state.isChecked ? `btn ${styles.gradientOn}` : 'btn';
const customBootstrapClass = this.props.customBootstrapClass ? this.props.customBootstrapClass : 'col-md-4';
return (
<div className={customBootstrapClass}>
<div className={`${styles.inputToggle} btn-group`} data-toggle="buttons">
<button className={btnClassOff} id="off" onClick={this.toggle}>OFF</button>
<button className={btnClassOn} id="on" onClick={this.toggle}>ON</button>
</div>
</div>
);
}
}
InputToggle.propTypes = {
customBootstrapClass: React.PropTypes.string,
handleChange: React.PropTypes.func.isRequired,
isChecked: React.PropTypes.bool.isRequired,
name: React.PropTypes.string.isRequired,
}
export default InputToggle;

View File

@ -0,0 +1,13 @@
/*
* InputRadio Messages
*
* This contains all the text for the InputRadio component.
*/
import { defineMessages } from 'react-intl';
export default defineMessages({
header: {
id: 'app.components.InputRadio.header',
defaultMessage: 'This is the InputRadio component !',
},
});

View File

@ -0,0 +1,36 @@
.inputToggle { /* stylelint-disable */
> button {
// display and box model
width: 5.3rem;
height: 3.4rem;
padding: 0;
line-height: 3.2rem;
border: 1px solid #E3E9F3;
// color
color: #333740;
background-color: white;
// text
font-weight: 600;
font-size: 1.2rem;
letter-spacing: 0.07rem;
&:first-of-type {
border-right: none;
}
&:nth-of-type(2) {
border-left: none;
}
&:hover {
z-index: 0 !important;
}
}
}
.gradientOff {
background-image: linear-gradient( to bottom right, #F65A1D, #F68E0E );
color: white !important;
}
.gradientOn {
background-image: linear-gradient( to bottom right, #005EEA, #0097F6);
color: white !important;
}

View File

@ -0,0 +1,11 @@
// import InputRadio from '../index';
import expect from 'expect';
// import { shallow } from 'enzyme';
// import React from 'react';
describe('<InputRadio />', () => {
it('Expect to have unit tests specified', () => {
expect(true).toEqual(false);
});
});

View File

@ -9,12 +9,28 @@ import React from 'react';
import PluginLeftMenuHeader from 'components/PluginLeftMenuHeader';
import styles from './styles.scss';
function PluginLeftMenu() {
return (
<div className={`${styles.pluginLeftMenu} col-md-3`}>
<PluginLeftMenuHeader />
</div>
);
class PluginLeftMenu extends React.Component { // eslint-disable-line react/prefer-stateless-function
constructor(props) {
super(props);
}
render() {
return (
<div className={`${styles.pluginLeftMenu} col-md-3`}>
<PluginLeftMenuHeader />
</div>
);
}
}
// function PluginLeftMenu() {
// return (
// <div className={`${styles.pluginLeftMenu} col-md-3`}>
// <PluginLeftMenuHeader />
// </div>
// );
// }
export default PluginLeftMenu;

View File

@ -0,0 +1,20 @@
/**
*
* PluginLeftMenuSection
*
*/
import React from 'react';
import styles from './styles.scss';
class PluginLeftMenuSection extends React.Component { // eslint-disable-line react/prefer-stateless-function
render() {
return (
<div className={styles.pluginLeftMenuSection}>
</div>
);
}
}
export default PluginLeftMenuSection;

View File

@ -0,0 +1,3 @@
.pluginLeftMenuSection { /* stylelint-disable */
}

View File

@ -0,0 +1,11 @@
// import PluginLeftMenuSection from '../index';
import expect from 'expect';
// import { shallow } from 'enzyme';
// import React from 'react';
describe('<PluginLeftMenuSection />', () => {
it('Expect to have unit tests specified', () => {
expect(true).toEqual(false);
});
});

View File

@ -3,3 +3,22 @@
* App actions
*
*/
import {
MENU_FETCH,
MENU_FETCH_SUCCEEDED,
} from './constants';
export function menuFetch() {
return {
type: MENU_FETCH,
};
}
export function fetchMenuSucceeded(menu) {
return {
type: MENU_FETCH_SUCCEEDED,
menu,
};
}

View File

@ -3,3 +3,6 @@
* App constants
*
*/
export const MENU_FETCH = 'SettingsManager/App/MENU_FETCH';
export const MENU_FETCH_SUCCEEDED = 'SettingsManager/App/MENU_FETCH_SUCCEEDED';

View File

@ -8,9 +8,32 @@
import React from 'react';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { bindActionCreators } from 'redux';
import { pluginId } from 'app';
import PluginLeftMenu from 'components/PluginLeftMenu';
import { menuFetch } from './actions';
import selectGlobalDomain from './selectors';
import styles from './styles.scss';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: false,
value1: null,
}
}
componentDidMount() {
this.props.menuFetch();
}
handleChange = ({ target }) => {
this.setState({ value: target.value});
}
render() {
// Assign plugin component to children
const content = React.Children.map(this.props.children, child =>
@ -18,9 +41,14 @@ class App extends React.Component {
exposedComponents: this.props.exposedComponents,
})
);
return (
<div className={pluginId}>
<div className={`${pluginId} ${styles.app}`}>
<div className={styles.baseline}></div>
<div className="container-fluid">
<div className="row">
<PluginLeftMenu sections={this.props.app.sections} />
</div>
</div>
{React.Children.toArray(content)}
</div>
);
@ -32,17 +60,22 @@ App.contextTypes = {
};
App.propTypes = {
children: React.PropTypes.node.isRequired,
children: React.PropTypes.node,
exposedComponents: React.PropTypes.object.isRequired,
};
export function mapDispatchToProps(dispatch) {
return {
dispatch,
};
return bindActionCreators(
{
menuFetch,
},
dispatch
);
}
const mapStateToProps = createStructuredSelector({});
const mapStateToProps = createStructuredSelector({
app: selectGlobalDomain(),
});
// Wrap the component to inject dispatch and state into it
export default connect(mapStateToProps, mapDispatchToProps)(App);

View File

@ -4,12 +4,19 @@
*
*/
import { fromJS } from 'immutable';
import { fromJS, List } from 'immutable';
import {
MENU_FETCH_SUCCEEDED,
} from './constants';
const initialState = fromJS({});
const initialState = fromJS({
sections: List(),
});
function appReducer(state = initialState, action) {
switch (action.type) {
case MENU_FETCH_SUCCEEDED:
return state.set('menuSections', action.menu.sections);
default:
return state;
}

View File

@ -0,0 +1,30 @@
import { takeLatest } from 'redux-saga';
import { LOCATION_CHANGE } from 'react-router-redux';
import { put, fork } from 'redux-saga/effects';
import { fetchMenuSucceeded } from './actions';
import { MENU_FETCH } from './constants';
export function* fetchMenu() {
try {
const opts = {
method: 'GET',
};
const response = yield fetch('/settings-manager/menu', opts);
const data = yield response.json();
yield put(fetchMenuSucceeded(data));
} catch(err) {
window.Strapi.notification.error(
'An error occurred.'
);
}
}
function* defaultSaga() {
yield fork(takeLatest, MENU_FETCH, fetchMenu);
}
export default [defaultSaga];

View File

@ -1,10 +1,10 @@
// import { createSelector } from 'reselect';
import { createSelector } from 'reselect';
/**
* Direct selector to the list state domain
*/
// const selectGlobalDomain = () => state => state.get('global');
const selectGlobalDomain = () => state => state.get('global');
const selectLocationState = () => {
let prevRoutingState;
@ -23,3 +23,4 @@ const selectLocationState = () => {
};
export { selectLocationState };
export default selectGlobalDomain;

View File

@ -0,0 +1,17 @@
.app { /* stylelint-disable */
min-height: calc(100vh - 6rem); // TODO should be variable
background: rgba(14,22,34,0.02);
}
.baseline {
// display: none;
z-index: 100001;
opacity: .2;
position: absolute;
top:0; left:0;
width: 100%;
height: 500%;
min-height: 100%;
background: url('../../assets/images/baseline-18.png');
// background: url('../../assets/images/baseline-20.png');
pointer-events: none;
}

View File

@ -8,24 +8,40 @@ import React from 'react';
import { connect } from 'react-redux';
import Helmet from 'react-helmet';
import PluginLeftMenu from 'components/PluginLeftMenu';
import InputToggle from 'components/InputToggle';
import selectHome from './selectors';
import styles from './styles.scss';
export class Home extends React.Component { // eslint-disable-line react/prefer-stateless-function
// constructor(props) {
// super(props);
// // this.leftMenuItems = [
// // {
// // header: 'global settings',
// // items: [
// // general, 'languages', 'advanced'],
// // }
// // ]
// }
constructor(props) {
super(props);
this.state = {
value: false,
value1: null,
}
}
handleChange = ({ target }) => {
console.log('ok');
console.log(target);
this.setState({ value: !this.state.value});
}
render() {
const test = {
"name": "bame",
"slug": "name",
"target": "general.name",
"type": "text",
"value": "ExperienceApp",
"validations" : {
"maxLength": 12,
"required": true,
"regex": /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
}
};
return (
<div className={styles.home}>
<div className={styles.baseline}></div>
@ -39,7 +55,20 @@ export class Home extends React.Component { // eslint-disable-line react/prefer-
<div className="row">
<PluginLeftMenu />
<div className="col-md-9">
f
<div className="form-group">
<InputToggle
handleChange={this.handleChange}
isChecked={this.state.value}
name={test.name}
validations={test.validations}
customBootstrapClass={'col-lg-2 offset-lg-4'}
errors={[]}
/>
</div>
</div>
</div>
</div>

View File

@ -1,6 +1,6 @@
{
"/": {
"name": "home",
"container": "Home"
"name": "app",
"container": "App"
}
}