Merged in OS-92-custom-responses-view (pull request #60)

OS-92 custom responses view
This commit is contained in:
Ivan Diaz 2016-10-14 16:06:36 +00:00
commit f94ac300bd
37 changed files with 779 additions and 126 deletions

View File

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

View File

@ -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 = {
@ -12,6 +13,12 @@ class AreYouSure extends React.Component {
static contextTypes = {
closeModal: React.PropTypes.func
};
static openModal(description, onYes) {
ModalContainer.openModal(
<AreYouSure description={description} onYes={onYes} />
);
}
componentDidMount() {
this.refs.yesButton && this.refs.yesButton.focus();

View File

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

View File

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

View File

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

View File

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

View File

@ -2,11 +2,4 @@
.application {
padding: $half-space;
&_modal-opened {
.application__content {
position: fixed;
overflow: hidden;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -48,4 +48,15 @@
&__navigation {
margin-top: 10px;
}
&__loading-wrapper {
min-height: 200px;
}
&__loading {
position: initial;
width: initial;
height: initial;
margin: auto;
}
}

View File

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

View File

@ -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: {}
};
}
}
];

View File

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

View File

@ -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.'
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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