Merged in OS-92-custom-responses-view (pull request #60)
OS-92 custom responses view
This commit is contained in:
commit
f94ac300bd
|
@ -0,0 +1,14 @@
|
|||
import API from 'lib-app/api-call';
|
||||
|
||||
export default {
|
||||
|
||||
retrieveCustomResponses() {
|
||||
return {
|
||||
type: 'CUSTOM_RESPONSES',
|
||||
payload: API.call({
|
||||
path: '/ticket/get-custom-responses',
|
||||
data: {}
|
||||
})
|
||||
};
|
||||
}
|
||||
};
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import Button from 'core-components/button';
|
||||
import ModalContainer from 'app-components/modal-container';
|
||||
|
||||
class AreYouSure extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -13,6 +14,12 @@ class AreYouSure extends React.Component {
|
|||
closeModal: React.PropTypes.func
|
||||
};
|
||||
|
||||
static openModal(description, onYes) {
|
||||
ModalContainer.openModal(
|
||||
<AreYouSure description={description} onYes={onYes} />
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.refs.yesButton && this.refs.yesButton.focus();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import DropDown from 'core-components/drop-down';
|
||||
|
||||
const codeLanguages = {
|
||||
'English': 'us',
|
||||
'Spanish': 'es',
|
||||
'German': 'de',
|
||||
'French': 'fr',
|
||||
'Chinese': 'cn',
|
||||
'Turkish': 'tr',
|
||||
'Indian': 'in'
|
||||
};
|
||||
const languages = Object.keys(codeLanguages);
|
||||
const languageCodes = Object.values(codeLanguages).concat(['en']);
|
||||
|
||||
class LanguageSelector extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.oneOf(languageCodes)
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<DropDown {...this.getProps()}/>
|
||||
);
|
||||
}
|
||||
|
||||
getProps() {
|
||||
return {
|
||||
className: this.getClass(),
|
||||
items: this.getLanguageList(),
|
||||
selectedIndex: this.getSelectedIndex(),
|
||||
onChange: this.changeLanguage.bind(this),
|
||||
size: this.props.size
|
||||
};
|
||||
}
|
||||
|
||||
getClass() {
|
||||
let classes = {
|
||||
'language-selector': true
|
||||
};
|
||||
|
||||
classes[this.props.className] = (this.props.className);
|
||||
|
||||
return classNames(classes);
|
||||
}
|
||||
|
||||
getLanguageList() {
|
||||
return languages.map((language) => {
|
||||
return {
|
||||
content: language,
|
||||
icon: codeLanguages[language]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getSelectedIndex() {
|
||||
let selectedIndex = languages.map((key) => codeLanguages[key]).indexOf(this.getPropLanguage());
|
||||
|
||||
return (selectedIndex != -1) ? selectedIndex : undefined;
|
||||
}
|
||||
|
||||
getPropLanguage() {
|
||||
let language = this.props.value;
|
||||
|
||||
if (language === 'en') {
|
||||
language = 'us';
|
||||
}
|
||||
|
||||
return language;
|
||||
}
|
||||
|
||||
changeLanguage(event) {
|
||||
let language = codeLanguages[languages[event.index]];
|
||||
|
||||
if (language === 'us') {
|
||||
language = 'en';
|
||||
}
|
||||
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange({
|
||||
target: {
|
||||
value: language
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default LanguageSelector;
|
|
@ -4,7 +4,7 @@ import classNames from 'classnames';
|
|||
import { connect } from 'react-redux'
|
||||
import { browserHistory } from 'react-router';
|
||||
|
||||
import ModalContainer from 'app/modal-container';
|
||||
import ModalContainer from 'app-components/modal-container';
|
||||
|
||||
class App extends React.Component {
|
||||
static contextTypes = {
|
||||
|
|
|
@ -1,14 +1,222 @@
|
|||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import {connect} from 'react-redux';
|
||||
import RichTextEditor from 'react-rte-browserify';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
import AdminDataActions from 'actions/admin-data-actions';
|
||||
|
||||
import AreYouSure from 'app-components/are-you-sure';
|
||||
import LanguageSelector from 'app-components/language-selector';
|
||||
|
||||
import Icon from 'core-components/icon';
|
||||
import Button from 'core-components/button';
|
||||
import Header from 'core-components/header';
|
||||
import Listing from 'core-components/listing';
|
||||
import Loading from 'core-components/loading';
|
||||
import Form from 'core-components/form';
|
||||
import FormField from 'core-components/form-field';
|
||||
import SubmitButton from 'core-components/submit-button';
|
||||
|
||||
class AdminPanelCustomResponses extends React.Component {
|
||||
static defaultProps = {
|
||||
items: []
|
||||
};
|
||||
|
||||
state = {
|
||||
formLoading: false,
|
||||
selectedIndex: -1,
|
||||
edited: false,
|
||||
errors: {},
|
||||
form: {
|
||||
title: '',
|
||||
content: RichTextEditor.createEmptyValue(),
|
||||
language: 'en'
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.loaded) {
|
||||
this.retrieveCustomResponses();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
/admin/panel/tickets/custom-responses
|
||||
<div className="admin-panel-custom-responses">
|
||||
<Header title={i18n('CUSTOM_RESPONSES')} description={i18n('CUSTOM_RESPONSES_DESCRIPTION')} />
|
||||
{(this.props.loaded) ? this.renderContent() : this.renderLoading()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-md-3">
|
||||
<Listing {...this.getListingProps()}/>
|
||||
</div>
|
||||
<div className="col-md-9">
|
||||
<Form {...this.getFormProps()}>
|
||||
<div className="row">
|
||||
<div className="col-md-7">
|
||||
<FormField label={i18n('TITLE')} name="title" validation="TITLE" required fieldProps={{size: 'large'}}/>
|
||||
</div>
|
||||
<div className="col-md-5">
|
||||
<FormField label={i18n('LANGUAGE')} name="language" field="input" decorator={LanguageSelector} fieldProps={{size: 'medium'}} />
|
||||
</div>
|
||||
</div>
|
||||
<FormField label={i18n('CONTENT')} name="content" validation="TEXT_AREA" required field="textarea" />
|
||||
<div className="admin-panel-custom-responses__actions">
|
||||
<div className="admin-panel-custom-responses__save-button">
|
||||
<SubmitButton type="secondary" size="small">{i18n('SAVE')}</SubmitButton>
|
||||
</div>
|
||||
{(this.state.selectedIndex !== -1) ? this.renderOptionalButtons() : null}
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderLoading() {
|
||||
return (
|
||||
<div className="admin-panel-custom-responses__loading">
|
||||
<Loading backgrounded size="large"/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderOptionalButtons() {
|
||||
return (
|
||||
<div className="admin-panel-custom-responses__optional-buttons">
|
||||
<div className="admin-panel-custom-responses__discard-button">
|
||||
<Button onClick={this.onDiscardChangesClick.bind(this)}>{i18n('DISCARD_CHANGES')}</Button>
|
||||
</div>
|
||||
<div className="admin-panel-custom-responses__delete-button">
|
||||
<Button onClick={this.onDeleteClick.bind(this)}>{i18n('DELETE')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getListingProps() {
|
||||
return {
|
||||
title: i18n('CUSTOM_RESPONSES'),
|
||||
items: this.getItems(),
|
||||
selectedIndex: this.state.selectedIndex,
|
||||
enableAddNew: true,
|
||||
onChange: this.onItemChange.bind(this),
|
||||
onAddClick: this.onItemChange.bind(this, -1)
|
||||
};
|
||||
}
|
||||
|
||||
getFormProps() {
|
||||
return {
|
||||
values: this.state.form,
|
||||
errors: this.state.errors,
|
||||
loading: this.state.formLoading,
|
||||
onChange: (form) => {this.setState({form, edited: true})},
|
||||
onValidateErrors: (errors) => {this.setState({errors})},
|
||||
onSubmit: this.onFormSubmit.bind(this)
|
||||
}
|
||||
}
|
||||
|
||||
getItems() {
|
||||
return this.props.items.map((item) => {
|
||||
return {
|
||||
content: (
|
||||
<span>
|
||||
{item.name}
|
||||
<span className="admin-panel-custom-responses__item-flag">
|
||||
<Icon name={(item.language != 'en') ? item.language : 'us'}/>
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
onItemChange(index) {
|
||||
if(this.state.edited) {
|
||||
AreYouSure.openModal(i18n('WILL_LOSE_CHANGES'), this.updateForm.bind(this, index));
|
||||
} else {
|
||||
this.updateForm(index);
|
||||
}
|
||||
}
|
||||
|
||||
onFormSubmit(form) {
|
||||
this.setState({formLoading: true});
|
||||
|
||||
if(this.state.selectedIndex !== -1) {
|
||||
API.call({
|
||||
path: '/ticket/edit-custom-response',
|
||||
data: {
|
||||
id: this.state.selectedIndex,
|
||||
name: form.name,
|
||||
content: form.content,
|
||||
language: form.language
|
||||
}
|
||||
}).then(() => {
|
||||
this.setState({formLoading: false});
|
||||
this.retrieveCustomResponses();
|
||||
});
|
||||
} else {
|
||||
API.call({
|
||||
path: '/ticket/add-custom-response',
|
||||
data: {
|
||||
name: form.title,
|
||||
content: form.content,
|
||||
language: form.language
|
||||
}
|
||||
}).then(() => {
|
||||
this.setState({formLoading: false});
|
||||
this.retrieveCustomResponses();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onDiscardChangesClick() {
|
||||
this.onItemChange(this.state.selectedIndex);
|
||||
}
|
||||
|
||||
onDeleteClick() {
|
||||
AreYouSure.openModal(i18n('WILL_DELETE_CUSTOM_RESPONSE'), this.deleteCustomResponse.bind(this));
|
||||
}
|
||||
|
||||
deleteCustomResponse() {
|
||||
API.call({
|
||||
path: '/ticket/delete-custom-response',
|
||||
data: {
|
||||
id: this.state.selectedIndex
|
||||
}
|
||||
}).then(this.retrieveCustomResponses.bind(this));
|
||||
}
|
||||
|
||||
updateForm(index) {
|
||||
let form = _.clone(this.state.form);
|
||||
|
||||
form.title = (this.props.items[index] && this.props.items[index].name) || '';
|
||||
form.content = RichTextEditor.createValueFromString((this.props.items[index] && this.props.items[index].content) || '', 'html');
|
||||
form.language = (this.props.items[index] && this.props.items[index].language) || 'en';
|
||||
|
||||
this.setState({
|
||||
selectedIndex: index,
|
||||
edited: false,
|
||||
form: form,
|
||||
errors: {}
|
||||
});
|
||||
}
|
||||
|
||||
retrieveCustomResponses() {
|
||||
this.props.dispatch(AdminDataActions.retrieveCustomResponses());
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminPanelCustomResponses;
|
||||
export default connect((store) => {
|
||||
return {
|
||||
loaded: store.adminData.customResponsesLoaded,
|
||||
items: store.adminData.customResponses
|
||||
};
|
||||
})(AdminPanelCustomResponses);
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
.admin-panel-custom-responses {
|
||||
&__loading {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
&__item-flag {
|
||||
float: right;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&__save-button {
|
||||
display: inline-block;
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
&__optional-buttons {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
&__discard-button {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&__delete-button {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
}
|
||||
}
|
|
@ -2,11 +2,4 @@
|
|||
|
||||
.application {
|
||||
padding: $half-space;
|
||||
|
||||
&_modal-opened {
|
||||
.application__content {
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
const _ = require('lodash');
|
||||
const DocumentTitle = require('react-document-title');
|
||||
|
||||
const ModalContainer = require('app/modal-container');
|
||||
const ModalContainer = require('app-components/modal-container');
|
||||
const AreYouSure = require('app-components/are-you-sure');
|
||||
|
||||
const Button = require('core-components/button');
|
||||
|
@ -123,6 +124,20 @@ let DemoPage = React.createClass({
|
|||
</Button>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'ModalTrigger Large',
|
||||
render: (
|
||||
<Button onClick={function () {
|
||||
ModalContainer.openModal(
|
||||
<div>
|
||||
{_.range(1, 60).map(() => <div>Some modal content</div>)}
|
||||
</div>
|
||||
);
|
||||
}}>
|
||||
Open Large Modal
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Table',
|
||||
render: (
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import React from 'react';
|
||||
|
||||
import API from 'lib-app/api-call';
|
||||
import i18n from 'lib-app/i18n';
|
||||
|
||||
import ModalContainer from 'app-components/modal-container';
|
||||
import AreYouSure from 'app-components/are-you-sure';
|
||||
|
||||
import Header from 'core-components/header';
|
||||
import Form from 'core-components/form';
|
||||
import FormField from 'core-components/form-field';
|
||||
import SubmitButton from 'core-components/submit-button';
|
||||
import ModalContainer from 'app/modal-container';
|
||||
import AreYouSure from 'app-components/are-you-sure';
|
||||
import Message from 'core-components/message';
|
||||
import i18n from 'lib-app/i18n';
|
||||
|
||||
class DashboardEditProfilePage extends React.Component {
|
||||
|
||||
|
|
|
@ -2,21 +2,10 @@ import React from 'react';
|
|||
import { connect } from 'react-redux'
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import SessionActions from 'actions/session-actions';
|
||||
import ConfigActions from 'actions/config-actions';
|
||||
|
||||
import LanguageSelector from 'app-components/language-selector';
|
||||
import Button from 'core-components/button';
|
||||
import DropDown from 'core-components/drop-down';
|
||||
|
||||
let codeLanguages = {
|
||||
'English': 'us',
|
||||
'Spanish': 'es',
|
||||
'German': 'de',
|
||||
'French': 'fr',
|
||||
'Chinese': 'cn',
|
||||
'Turkish': 'tr',
|
||||
'Indian': 'in'
|
||||
};
|
||||
|
||||
class MainLayoutHeader extends React.Component {
|
||||
|
||||
|
@ -24,7 +13,7 @@ class MainLayoutHeader extends React.Component {
|
|||
return (
|
||||
<div className="main-layout-header">
|
||||
{this.renderAccessLinks()}
|
||||
<DropDown {...this.getLanguageSelectorProps()}/>
|
||||
<LanguageSelector {...this.getLanguageSelectorProps()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -43,7 +32,7 @@ class MainLayoutHeader extends React.Component {
|
|||
result = (
|
||||
<div className="main-layout-header__login-links">
|
||||
<Button type="clean" route={{to:'/'}}>{i18n('LOG_IN')}</Button>
|
||||
<Button type="clean" route={{to:'/signup'}}>Sign up</Button>
|
||||
<Button type="clean" route={{to:'/signup'}}>{i18n('SIGN_UP')}</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -54,39 +43,13 @@ class MainLayoutHeader extends React.Component {
|
|||
getLanguageSelectorProps() {
|
||||
return {
|
||||
className: 'main-layout-header__languages',
|
||||
items: this.getLanguageList(),
|
||||
selectedIndex: Object.keys(codeLanguages).map((key) => codeLanguages[key]).indexOf(this.getPropLanguage()),
|
||||
value: this.props.config.language,
|
||||
onChange: this.changeLanguage.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
getLanguageList() {
|
||||
return Object.keys(codeLanguages).map((language) => {
|
||||
return {
|
||||
content: language,
|
||||
icon: codeLanguages[language]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getPropLanguage() {
|
||||
let language = this.props.config.language;
|
||||
|
||||
if (language === 'en') {
|
||||
language = 'us';
|
||||
}
|
||||
|
||||
return language;
|
||||
}
|
||||
|
||||
changeLanguage(event) {
|
||||
let language = codeLanguages[Object.keys(codeLanguages)[event.index]];
|
||||
|
||||
if (language === 'us') {
|
||||
language = 'en';
|
||||
}
|
||||
|
||||
this.props.dispatch(ConfigActions.changeLanguage(language));
|
||||
this.props.dispatch(ConfigActions.changeLanguage(event.target.value));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,18 +20,31 @@
|
|||
background-color: lighten($primary-red, 5%);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
&.button_disabled,
|
||||
&.button_disabled:hover {
|
||||
background-color: lighten($primary-red, 15%);
|
||||
}
|
||||
}
|
||||
|
||||
&_secondary {
|
||||
background-color: $primary-green;
|
||||
|
||||
|
||||
&.button_disabled,
|
||||
&.button_disabled:hover {
|
||||
background-color: lighten($primary-green, 15%);
|
||||
}
|
||||
}
|
||||
|
||||
&_tertiary {
|
||||
background-color: $secondary-blue;
|
||||
}
|
||||
|
||||
&_disabled {
|
||||
background-color: #ec9696;
|
||||
&.button_disabled,
|
||||
&.button_disabled:hover {
|
||||
background-color: lighten($secondary-blue, 15%);
|
||||
}
|
||||
}
|
||||
|
||||
&_small {
|
||||
|
|
|
@ -14,6 +14,7 @@ class FormField extends React.Component {
|
|||
};
|
||||
|
||||
static propTypes = {
|
||||
decorator: React.PropTypes.func,
|
||||
validation: React.PropTypes.string,
|
||||
onChange: React.PropTypes.func,
|
||||
onBlur: React.PropTypes.func,
|
||||
|
@ -62,13 +63,17 @@ class FormField extends React.Component {
|
|||
}
|
||||
|
||||
renderField() {
|
||||
const Field = {
|
||||
let Field = {
|
||||
'input': Input,
|
||||
'textarea': TextEditor,
|
||||
'select': DropDown,
|
||||
'checkbox': Checkbox
|
||||
}[this.props.field];
|
||||
|
||||
if(this.props.decorator) {
|
||||
Field = this.props.decorator;
|
||||
}
|
||||
|
||||
return <Field {...this.getFieldProps()} />;
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@ class Form extends React.Component {
|
|||
loading: React.PropTypes.bool,
|
||||
errors: React.PropTypes.object,
|
||||
onValidateErrors: React.PropTypes.func,
|
||||
onChange: React.PropTypes.func,
|
||||
values: React.PropTypes.object,
|
||||
onSubmit: React.PropTypes.func
|
||||
};
|
||||
|
||||
|
@ -58,6 +60,8 @@ class Form extends React.Component {
|
|||
delete props.errors;
|
||||
delete props.loading;
|
||||
delete props.onValidateErrors;
|
||||
delete props.values;
|
||||
delete props.onChange;
|
||||
|
||||
return props;
|
||||
}
|
||||
|
@ -80,7 +84,7 @@ class Form extends React.Component {
|
|||
|
||||
additionalProps = {
|
||||
ref: fieldName,
|
||||
value: this.state.form[fieldName] || props.value,
|
||||
value: this.getFormValue()[fieldName],
|
||||
error: this.getFieldError(fieldName),
|
||||
onChange: this.handleFieldChange.bind(this, fieldName),
|
||||
onBlur: this.validateField.bind(this, fieldName)
|
||||
|
@ -111,8 +115,8 @@ class Form extends React.Component {
|
|||
}
|
||||
|
||||
getAllFieldErrors() {
|
||||
let form = this.state.form;
|
||||
let fields = Object.keys(this.state.form);
|
||||
let form = this.getFormValue();
|
||||
let fields = Object.keys(form);
|
||||
let errors = {};
|
||||
|
||||
_.each(fields, (fieldName) => {
|
||||
|
@ -122,7 +126,7 @@ class Form extends React.Component {
|
|||
return errors;
|
||||
}
|
||||
|
||||
getErrorsWithValidatedField(fieldName, form = this.state.form, errors = this.state.errors) {
|
||||
getErrorsWithValidatedField(fieldName, form = this.getFormValue(), errors = this.state.errors) {
|
||||
let newErrors = _.clone(errors);
|
||||
|
||||
if (this.state.validations[fieldName]) {
|
||||
|
@ -156,7 +160,7 @@ class Form extends React.Component {
|
|||
handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const form = _.mapValues(this.state.form, (field) => {
|
||||
const form = _.mapValues(this.getFormValue(), (field) => {
|
||||
if (field instanceof RichTextEditor.EditorValue) {
|
||||
return field.toString('html');
|
||||
} else {
|
||||
|
@ -172,13 +176,18 @@ class Form extends React.Component {
|
|||
}
|
||||
|
||||
handleFieldChange(fieldName, event) {
|
||||
let form = _.clone(this.state.form);
|
||||
let form = _.clone(this.getFormValue());
|
||||
|
||||
form[fieldName] = event.target.value;
|
||||
|
||||
this.setState({
|
||||
form: form
|
||||
});
|
||||
|
||||
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(form);
|
||||
}
|
||||
}
|
||||
|
||||
isValidField(node) {
|
||||
|
@ -203,6 +212,10 @@ class Form extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
getFormValue() {
|
||||
return this.props.values || this.state.form;
|
||||
}
|
||||
|
||||
focusFirstErrorField() {
|
||||
let firstErrorField = this.getFirstErrorField();
|
||||
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
import React from 'react';
|
||||
|
||||
import Menu from 'core-components/menu';
|
||||
import Button from 'core-components/button';
|
||||
import Icon from 'core-components/icon';
|
||||
|
||||
class Listing extends React.Component {
|
||||
static propTypes = {
|
||||
title: React.PropTypes.string,
|
||||
enableAddNew: React.PropTypes.bool,
|
||||
items: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
|
||||
selectedIndex: React.PropTypes.number,
|
||||
onChange: React.PropTypes.func,
|
||||
onAddClick: React.PropTypes.func
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
addNewText: 'Add new'
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="listing">
|
||||
<div className="listing__header">
|
||||
{this.props.title}
|
||||
</div>
|
||||
<div className="listing__menu">
|
||||
<Menu tabbable type="secondary" selectedIndex={this.props.selectedIndex} items={this.props.items} onItemClick={this.props.onChange}/>
|
||||
</div>
|
||||
{(this.props.enableAddNew) ? this.renderAddButton() : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderAddButton() {
|
||||
return (
|
||||
<div className="listing__add">
|
||||
<Button type="secondary" size="auto" className="listing__add-button" onClick={this.props.onAddClick}>
|
||||
<Icon name="plus-circle"/> {this.props.addNewText}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Listing;
|
|
@ -0,0 +1,25 @@
|
|||
@import "../scss/vars";
|
||||
|
||||
.listing {
|
||||
border: 2px solid $grey;
|
||||
min-height: 300px;
|
||||
|
||||
&__header {
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
&__menu {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&__add {
|
||||
|
||||
&-button {
|
||||
border-radius: 0;
|
||||
height: 36px;
|
||||
font-size: $font-size--sm;
|
||||
text-transform: none;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
class Loading extends React.Component {
|
||||
static propTypes = {
|
||||
backgrounded: React.PropTypes.bool,
|
||||
size: React.PropTypes.oneOf(['small', 'medium', 'large'])
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
size: 'small',
|
||||
backgrounded: false
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={this.getClass()}>
|
||||
<span className="loading__icon" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getClass() {
|
||||
let classes = {
|
||||
'loading': true,
|
||||
'loading_backgrounded': (this.props.backgrounded),
|
||||
'loading_large': (this.props.size === 'large')
|
||||
};
|
||||
|
||||
classes[this.props.className] = (this.props.className);
|
||||
|
||||
return classNames(classes);
|
||||
}
|
||||
}
|
||||
|
||||
export default Loading;
|
|
@ -0,0 +1,73 @@
|
|||
$color: white;
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
&__icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
font-size: 4px;
|
||||
border-top: 1.1em solid rgba($color, 0.2);
|
||||
border-right: 1.1em solid rgba($color, 0.2);
|
||||
border-bottom: 1.1em solid rgba($color, 0.2);
|
||||
border-left: 1.1em solid $color;
|
||||
-webkit-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
-webkit-animation: turnAnimation 1.1s infinite linear;
|
||||
animation: turnAnimation 1.1s infinite linear;
|
||||
|
||||
&,
|
||||
&:after {
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
&_large {
|
||||
|
||||
.loading__icon,
|
||||
.loading__icon:after {
|
||||
font-size: 7px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
&_backgrounded {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(black, 0.2);
|
||||
}
|
||||
|
||||
@-webkit-keyframes turnAnimation {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes turnAnimation {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,9 +11,9 @@ class Menu extends React.Component {
|
|||
id: React.PropTypes.string,
|
||||
itemsRole: React.PropTypes.string,
|
||||
header: React.PropTypes.string,
|
||||
type: React.PropTypes.oneOf(['primary', 'secondary', 'navigation']),
|
||||
type: React.PropTypes.oneOf(['primary', 'secondary', 'navigation', 'horizontal', 'horizontal-list']),
|
||||
items: React.PropTypes.arrayOf(React.PropTypes.shape({
|
||||
content: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number]),
|
||||
content: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number, React.PropTypes.node]),
|
||||
icon: React.PropTypes.string
|
||||
})).isRequired,
|
||||
selectedIndex: React.PropTypes.number,
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
.modal {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
padding: 50px 0;
|
||||
overflow: auto;
|
||||
z-index: 1000;
|
||||
|
||||
&__content {
|
||||
position: relative;
|
||||
|
|
|
@ -5,6 +5,7 @@ import classNames from 'classnames';
|
|||
|
||||
// CORE LIBS
|
||||
import Button from 'core-components/button';
|
||||
import Loading from 'core-components/loading';
|
||||
|
||||
class SubmitButton extends React.Component {
|
||||
|
||||
|
@ -30,7 +31,7 @@ class SubmitButton extends React.Component {
|
|||
|
||||
renderLoading() {
|
||||
return (
|
||||
<div className="submit-button__loader"></div>
|
||||
<Loading className="submit-button__loader" />
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,46 +1,3 @@
|
|||
.submit-button {
|
||||
position: relative;
|
||||
|
||||
&__loader {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 103px;
|
||||
|
||||
font-size: 4px;
|
||||
border-top: 1.1em solid rgba(255, 255, 255, 0.2);
|
||||
border-right: 1.1em solid rgba(255, 255, 255, 0.2);
|
||||
border-bottom: 1.1em solid rgba(255, 255, 255, 0.2);
|
||||
border-left: 1.1em solid #ffffff;
|
||||
-webkit-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
-webkit-animation: turnAnimation 1.1s infinite linear;
|
||||
animation: turnAnimation 1.1s infinite linear;
|
||||
}
|
||||
&__loader,
|
||||
&__loader:after {
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
@-webkit-keyframes turnAnimation {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes turnAnimation {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ import _ from 'lodash';
|
|||
import classNames from 'classnames';
|
||||
|
||||
import Menu from 'core-components/menu';
|
||||
import Loading from 'core-components/loading';
|
||||
|
||||
class Table extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -13,6 +14,7 @@ class Table extends React.Component {
|
|||
})),
|
||||
rows: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||
pageSize: React.PropTypes.number,
|
||||
loading: React.PropTypes.bool,
|
||||
type: React.PropTypes.oneOf(['default'])
|
||||
};
|
||||
|
||||
|
@ -34,9 +36,10 @@ class Table extends React.Component {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.props.rows.map(this.renderRow.bind(this))}
|
||||
{(!this.props.loading) ? this.props.rows.map(this.renderRow.bind(this)) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
{(this.props.loading) ? this.renderLoading() : null}
|
||||
{(this.props.pageSize && this.props.rows.length > this.props.pageSize) ? this.renderNavigation() : null}
|
||||
</div>
|
||||
);
|
||||
|
@ -86,6 +89,14 @@ class Table extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
renderLoading() {
|
||||
return (
|
||||
<div className="table__loading-wrapper">
|
||||
<Loading className="table__loading" backgrounded size="large"/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
onNavigationItemClick(index) {
|
||||
this.setState({
|
||||
page: index + 1
|
||||
|
|
|
@ -48,4 +48,15 @@
|
|||
&__navigation {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
&__loading-wrapper {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
&__loading {
|
||||
position: initial;
|
||||
width: initial;
|
||||
height: initial;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
|
@ -37,7 +37,7 @@ class TextEditor extends React.Component {
|
|||
getEditorProps() {
|
||||
return {
|
||||
className: 'text-editor__editor',
|
||||
value: this.state.value,
|
||||
value: this.props.value || this.state.value,
|
||||
ref: 'editor',
|
||||
onChange: this.onEditorChange.bind(this),
|
||||
onFocus: this.onEditorFocus.bind(this),
|
||||
|
|
|
@ -31,5 +31,51 @@ module.exports = [
|
|||
data: {}
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/ticket/get-custom-responses',
|
||||
time: 1000,
|
||||
response: function () {
|
||||
return {
|
||||
status: 'success',
|
||||
data: [
|
||||
{name: 'Common issue #1', language: 'en', content: 'some content'},
|
||||
{name: 'Common issue #2', language: 'en', content: 'some content'},
|
||||
{name: 'Common issue #3', language: 'en', content: 'some content'},
|
||||
{name: 'Häufiges Problem #1', language: 'de', content: 'einige Inhalte'},
|
||||
{name: 'Häufiges Problem #2', language: 'de', content: 'einige Inhalte'}
|
||||
]
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/ticket/add-custom-response',
|
||||
time: 1000,
|
||||
response: function () {
|
||||
return {
|
||||
status: 'success',
|
||||
data: {}
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/ticket/edit-custom-response',
|
||||
time: 1000,
|
||||
response: function () {
|
||||
return {
|
||||
status: 'success',
|
||||
data: {}
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/ticket/delete-custom-response',
|
||||
time: 1000,
|
||||
response: function () {
|
||||
return {
|
||||
status: 'success',
|
||||
data: {}
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
|
@ -17,7 +17,7 @@ const languages = {
|
|||
};
|
||||
|
||||
const i18nData = function (key, lang) {
|
||||
return languages[lang][key];
|
||||
return (languages[lang] && languages[lang][key]) || key;
|
||||
};
|
||||
|
||||
export default i18nData
|
||||
|
|
|
@ -18,14 +18,8 @@ export default {
|
|||
'EDIT_PROFILE': 'Edit Profile',
|
||||
'CLOSE_SESSION': 'Close session',
|
||||
'CREATE_TICKET': 'Create Ticket',
|
||||
'CREATE_TICKET_DESCRIPTION': 'This is a form for creating tickets. Fill the form and send us your issues/doubts/suggestions. Our support system will answer it as soon as possible.',
|
||||
'TICKET_LIST': 'Ticket List',
|
||||
'TICKET_LIST_DESCRIPTION': 'Here you can find a list of all tickets you have sent to our support team.',
|
||||
'TICKETS_DESCRIPTION': 'Send ticket through our support center and get response of your doubts, suggestions and issues.',
|
||||
'ARTICLES_DESCRIPTION': 'Take a look to our articles about common issues, guides and documentation.',
|
||||
'ACCOUNT_DESCRIPTION': 'All your tickets are stored in your accounts\'s profile. Keep track off all your tickets you send to our staff team.',
|
||||
'SUPPORT_CENTER': 'Support Center',
|
||||
'SUPPORT_CENTER_DESCRIPTION': 'Welcome to our support center. You can contact us through a tickets system. Your tickets will be answered by our staff.',
|
||||
'DEPARTMENT': 'Department',
|
||||
'AUTHOR': 'Author',
|
||||
'DATE': 'Date',
|
||||
|
@ -61,6 +55,20 @@ export default {
|
|||
'MEDIUM': 'Medium',
|
||||
'LOW': 'Low',
|
||||
'TITLE': 'Title',
|
||||
'CONTENT': 'Content',
|
||||
'SAVE': 'Save',
|
||||
'DISCARD_CHANGES': 'Discard changes',
|
||||
'DELETE': 'Delete',
|
||||
'LANGUAGE': 'Language',
|
||||
|
||||
//VIEW DESCRIPTIONS
|
||||
'CREATE_TICKET_DESCRIPTION': 'This is a form for creating tickets. Fill the form and send us your issues/doubts/suggestions. Our support system will answer it as soon as possible.',
|
||||
'TICKET_LIST_DESCRIPTION': 'Here you can find a list of all tickets you have sent to our support team.',
|
||||
'TICKETS_DESCRIPTION': 'Send ticket through our support center and get response of your doubts, suggestions and issues.',
|
||||
'ARTICLES_DESCRIPTION': 'Take a look to our articles about common issues, guides and documentation.',
|
||||
'ACCOUNT_DESCRIPTION': 'All your tickets are stored in your accounts\'s profile. Keep track off all your tickets you send to our staff team.',
|
||||
'SUPPORT_CENTER_DESCRIPTION': 'Welcome to our support center. You can contact us through a tickets system. Your tickets will be answered by our staff.',
|
||||
'CUSTOM_RESPONSES_DESCRIPTION': 'Custom responses are automated responses for common problems',
|
||||
|
||||
//ERRORS
|
||||
'EMAIL_OR_PASSWORD': 'Email or password invalid',
|
||||
|
@ -83,5 +91,7 @@ export default {
|
|||
'ARE_YOU_SURE': 'Are you sure?',
|
||||
'EMAIL_CHANGED': 'Email has been changed successfully',
|
||||
'PASSWORD_CHANGED': 'Password has been changed successfully',
|
||||
'OLD_PASSWORD_INCORRECT': 'Old password is incorrect'
|
||||
'OLD_PASSWORD_INCORRECT': 'Old password is incorrect',
|
||||
'WILL_LOSE_CHANGES': 'You haven\'t save. Your changes will be lost.',
|
||||
'WILL_DELETE_CUSTOM_RESPONSE': 'The custom response will be deleted.'
|
||||
};
|
|
@ -8,7 +8,7 @@ class AlphaNumericValidator extends Validator {
|
|||
}
|
||||
|
||||
validate(value, form) {
|
||||
let alphaMatch = /^[-\sa-zA-Z.]+$/;
|
||||
let alphaMatch = /^[ A-Za-z0-9_@./#&+-äöüÄÖÜß]*$/;
|
||||
|
||||
if (!alphaMatch.test(value)) return this.getError(this.errorKey);
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ class LengthValidator extends Validator {
|
|||
this.errorKey = errorKey;
|
||||
}
|
||||
|
||||
validate(value, form) {
|
||||
validate(value = '', form = {}) {
|
||||
if (value instanceof RichTextEditor.EditorValue) {
|
||||
value = value.getEditorState().getCurrentContent().getPlainText();
|
||||
}
|
||||
|
|
|
@ -4,10 +4,12 @@ import { routerReducer } from 'react-router-redux';
|
|||
import sessionReducer from 'reducers/session-reducer';
|
||||
import configReducer from 'reducers/config-reducer';
|
||||
import modalReducer from 'reducers/modal-reducer';
|
||||
import adminDataReducer from 'reducers/admin-data-reducer';
|
||||
|
||||
export default combineReducers({
|
||||
session: sessionReducer,
|
||||
config: configReducer,
|
||||
modal: modalReducer,
|
||||
adminData: adminDataReducer,
|
||||
routing: routerReducer
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import Reducer from 'reducers/reducer';
|
||||
|
||||
class AdminDataReducer extends Reducer {
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
customResponses: [],
|
||||
customResponsesLoaded: false
|
||||
};
|
||||
}
|
||||
|
||||
getTypeHandlers() {
|
||||
return {
|
||||
'CUSTOM_RESPONSES_FULFILLED': this.onCustomResponses
|
||||
};
|
||||
}
|
||||
|
||||
onCustomResponses(state, payload) {
|
||||
return _.extend({}, state, {
|
||||
customResponses: payload.data,
|
||||
customResponsesLoaded: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminDataReducer.getInstance();
|
|
@ -19,6 +19,8 @@ class ModalReducer extends Reducer {
|
|||
}
|
||||
|
||||
onOpenModal(state, payload) {
|
||||
document.body.setAttribute('style', 'overflow:hidden');
|
||||
|
||||
return _.extend({}, state, {
|
||||
opened: true,
|
||||
content: payload
|
||||
|
@ -26,6 +28,8 @@ class ModalReducer extends Reducer {
|
|||
}
|
||||
|
||||
onCloseModal(state) {
|
||||
document.body.setAttribute('style', '');
|
||||
|
||||
return _.extend({}, state, {
|
||||
opened: false,
|
||||
content: null
|
||||
|
|
|
@ -3,6 +3,7 @@ include 'ticket/create.php';
|
|||
include 'ticket/comment.php';
|
||||
include 'ticket/get.php';
|
||||
include 'ticket/add-custom-response.php';
|
||||
include 'ticket/delete-custom-response.php';
|
||||
include 'ticket/edit-custom-response.php';
|
||||
include 'ticket/get-custom-responses.php';
|
||||
|
||||
|
@ -13,6 +14,7 @@ $ticketControllers->addController(new CreateController);
|
|||
$ticketControllers->addController(new CommentController);
|
||||
$ticketControllers->addController(new TicketGetController);
|
||||
$ticketControllers->addController(new AddCustomResponseController);
|
||||
$ticketControllers->addController(new DeleteCustomResponseController);
|
||||
$ticketControllers->addController(new EditCustomResponseController);
|
||||
$ticketControllers->addController(new GetCustomResponsesController);
|
||||
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
use Respect\Validation\Validator as DataValidator;
|
||||
DataValidator::with('CustomValidations', true);
|
||||
|
||||
class DeleteCustomResponseController extends Controller {
|
||||
const PATH = '/delete-custom-response';
|
||||
|
||||
public function validations() {
|
||||
return [
|
||||
'permission' => 'staff_2',
|
||||
'requestData' => [
|
||||
'id' => [
|
||||
'validation' => DataValidator::dataStoreId('customresponse'),
|
||||
'error' => ERRORS::INVALID_NAME
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function handler() {
|
||||
$customResponse = CustomResponse::getDataStore(Controller::request('id'));
|
||||
$customResponse->trash();
|
||||
|
||||
Response::respondSuccess();
|
||||
}
|
||||
}
|
|
@ -120,6 +120,10 @@ abstract class DataStore {
|
|||
return RedBean::store($this->getBeanInstance());
|
||||
}
|
||||
|
||||
public function trash() {
|
||||
RedBean::trash($this->getBeanInstance());
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
RedBean::trash($this->getBeanInstance());
|
||||
unset($this);
|
||||
|
|
|
@ -53,4 +53,18 @@ describe 'CustomResponses' do
|
|||
(result['data'][0]['language']).should.equal('en')
|
||||
end
|
||||
end
|
||||
|
||||
describe '/ticket/delete-custom-responses/' do
|
||||
it 'should delete custom response' do
|
||||
result = request('/ticket/delete-custom-response', {
|
||||
csrf_userid: $csrf_userid,
|
||||
csrf_token: $csrf_token,
|
||||
id: 1
|
||||
})
|
||||
|
||||
(result['status']).should.equal('success')
|
||||
customResponse = $database.getRow('customresponse', 1)
|
||||
(customResponse).should.equal(nil)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue