mirror of
https://github.com/opensupports/opensupports.git
synced 2025-07-31 01:35:15 +02:00
Merged in OS-92-custom-responses-view (pull request #60)
OS-92 custom responses view
This commit is contained in:
commit
f94ac300bd
14
client/src/actions/admin-data-actions.js
Normal file
14
client/src/actions/admin-data-actions.js
Normal 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: {}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
|
|
||||||
import i18n from 'lib-app/i18n';
|
import i18n from 'lib-app/i18n';
|
||||||
import Button from 'core-components/button';
|
import Button from 'core-components/button';
|
||||||
|
import ModalContainer from 'app-components/modal-container';
|
||||||
|
|
||||||
class AreYouSure extends React.Component {
|
class AreYouSure extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -12,6 +13,12 @@ class AreYouSure extends React.Component {
|
|||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
closeModal: React.PropTypes.func
|
closeModal: React.PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static openModal(description, onYes) {
|
||||||
|
ModalContainer.openModal(
|
||||||
|
<AreYouSure description={description} onYes={onYes} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.refs.yesButton && this.refs.yesButton.focus();
|
this.refs.yesButton && this.refs.yesButton.focus();
|
||||||
|
90
client/src/app-components/language-selector.js
Normal file
90
client/src/app-components/language-selector.js
Normal 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;
|
@ -4,7 +4,7 @@ import classNames from 'classnames';
|
|||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { browserHistory } from 'react-router';
|
import { browserHistory } from 'react-router';
|
||||||
|
|
||||||
import ModalContainer from 'app/modal-container';
|
import ModalContainer from 'app-components/modal-container';
|
||||||
|
|
||||||
class App extends React.Component {
|
class App extends React.Component {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
|
@ -1,14 +1,222 @@
|
|||||||
import React from 'react';
|
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 {
|
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() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="admin-panel-custom-responses">
|
||||||
/admin/panel/tickets/custom-responses
|
<Header title={i18n('CUSTOM_RESPONSES')} description={i18n('CUSTOM_RESPONSES_DESCRIPTION')} />
|
||||||
|
{(this.props.loaded) ? this.renderContent() : this.renderLoading()}
|
||||||
</div>
|
</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 {
|
.application {
|
||||||
padding: $half-space;
|
padding: $half-space;
|
||||||
|
|
||||||
&_modal-opened {
|
|
||||||
.application__content {
|
|
||||||
position: fixed;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,9 +1,10 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
const _ = require('lodash');
|
||||||
const DocumentTitle = require('react-document-title');
|
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 AreYouSure = require('app-components/are-you-sure');
|
||||||
|
|
||||||
const Button = require('core-components/button');
|
const Button = require('core-components/button');
|
||||||
@ -123,6 +124,20 @@ let DemoPage = React.createClass({
|
|||||||
</Button>
|
</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',
|
title: 'Table',
|
||||||
render: (
|
render: (
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import API from 'lib-app/api-call';
|
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 Header from 'core-components/header';
|
||||||
import Form from 'core-components/form';
|
import Form from 'core-components/form';
|
||||||
import FormField from 'core-components/form-field';
|
import FormField from 'core-components/form-field';
|
||||||
import SubmitButton from 'core-components/submit-button';
|
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 Message from 'core-components/message';
|
||||||
import i18n from 'lib-app/i18n';
|
|
||||||
|
|
||||||
class DashboardEditProfilePage extends React.Component {
|
class DashboardEditProfilePage extends React.Component {
|
||||||
|
|
||||||
|
@ -2,21 +2,10 @@ import React from 'react';
|
|||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
|
|
||||||
import i18n from 'lib-app/i18n';
|
import i18n from 'lib-app/i18n';
|
||||||
import SessionActions from 'actions/session-actions';
|
|
||||||
import ConfigActions from 'actions/config-actions';
|
import ConfigActions from 'actions/config-actions';
|
||||||
|
|
||||||
|
import LanguageSelector from 'app-components/language-selector';
|
||||||
import Button from 'core-components/button';
|
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 {
|
class MainLayoutHeader extends React.Component {
|
||||||
|
|
||||||
@ -24,7 +13,7 @@ class MainLayoutHeader extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div className="main-layout-header">
|
<div className="main-layout-header">
|
||||||
{this.renderAccessLinks()}
|
{this.renderAccessLinks()}
|
||||||
<DropDown {...this.getLanguageSelectorProps()}/>
|
<LanguageSelector {...this.getLanguageSelectorProps()} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -43,7 +32,7 @@ class MainLayoutHeader extends React.Component {
|
|||||||
result = (
|
result = (
|
||||||
<div className="main-layout-header__login-links">
|
<div className="main-layout-header__login-links">
|
||||||
<Button type="clean" route={{to:'/'}}>{i18n('LOG_IN')}</Button>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -54,39 +43,13 @@ class MainLayoutHeader extends React.Component {
|
|||||||
getLanguageSelectorProps() {
|
getLanguageSelectorProps() {
|
||||||
return {
|
return {
|
||||||
className: 'main-layout-header__languages',
|
className: 'main-layout-header__languages',
|
||||||
items: this.getLanguageList(),
|
value: this.props.config.language,
|
||||||
selectedIndex: Object.keys(codeLanguages).map((key) => codeLanguages[key]).indexOf(this.getPropLanguage()),
|
|
||||||
onChange: this.changeLanguage.bind(this)
|
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) {
|
changeLanguage(event) {
|
||||||
let language = codeLanguages[Object.keys(codeLanguages)[event.index]];
|
this.props.dispatch(ConfigActions.changeLanguage(event.target.value));
|
||||||
|
|
||||||
if (language === 'us') {
|
|
||||||
language = 'en';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.dispatch(ConfigActions.changeLanguage(language));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,18 +20,31 @@
|
|||||||
background-color: lighten($primary-red, 5%);
|
background-color: lighten($primary-red, 5%);
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
&.button_disabled,
|
||||||
|
&.button_disabled:hover {
|
||||||
|
background-color: lighten($primary-red, 15%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&_secondary {
|
&_secondary {
|
||||||
background-color: $primary-green;
|
background-color: $primary-green;
|
||||||
|
|
||||||
|
|
||||||
|
&.button_disabled,
|
||||||
|
&.button_disabled:hover {
|
||||||
|
background-color: lighten($primary-green, 15%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&_tertiary {
|
&_tertiary {
|
||||||
background-color: $secondary-blue;
|
background-color: $secondary-blue;
|
||||||
}
|
|
||||||
|
|
||||||
&_disabled {
|
&.button_disabled,
|
||||||
background-color: #ec9696;
|
&.button_disabled:hover {
|
||||||
|
background-color: lighten($secondary-blue, 15%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&_small {
|
&_small {
|
||||||
|
@ -14,6 +14,7 @@ class FormField extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
decorator: React.PropTypes.func,
|
||||||
validation: React.PropTypes.string,
|
validation: React.PropTypes.string,
|
||||||
onChange: React.PropTypes.func,
|
onChange: React.PropTypes.func,
|
||||||
onBlur: React.PropTypes.func,
|
onBlur: React.PropTypes.func,
|
||||||
@ -62,13 +63,17 @@ class FormField extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderField() {
|
renderField() {
|
||||||
const Field = {
|
let Field = {
|
||||||
'input': Input,
|
'input': Input,
|
||||||
'textarea': TextEditor,
|
'textarea': TextEditor,
|
||||||
'select': DropDown,
|
'select': DropDown,
|
||||||
'checkbox': Checkbox
|
'checkbox': Checkbox
|
||||||
}[this.props.field];
|
}[this.props.field];
|
||||||
|
|
||||||
|
if(this.props.decorator) {
|
||||||
|
Field = this.props.decorator;
|
||||||
|
}
|
||||||
|
|
||||||
return <Field {...this.getFieldProps()} />;
|
return <Field {...this.getFieldProps()} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,8 @@ class Form extends React.Component {
|
|||||||
loading: React.PropTypes.bool,
|
loading: React.PropTypes.bool,
|
||||||
errors: React.PropTypes.object,
|
errors: React.PropTypes.object,
|
||||||
onValidateErrors: React.PropTypes.func,
|
onValidateErrors: React.PropTypes.func,
|
||||||
|
onChange: React.PropTypes.func,
|
||||||
|
values: React.PropTypes.object,
|
||||||
onSubmit: React.PropTypes.func
|
onSubmit: React.PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -58,6 +60,8 @@ class Form extends React.Component {
|
|||||||
delete props.errors;
|
delete props.errors;
|
||||||
delete props.loading;
|
delete props.loading;
|
||||||
delete props.onValidateErrors;
|
delete props.onValidateErrors;
|
||||||
|
delete props.values;
|
||||||
|
delete props.onChange;
|
||||||
|
|
||||||
return props;
|
return props;
|
||||||
}
|
}
|
||||||
@ -80,7 +84,7 @@ class Form extends React.Component {
|
|||||||
|
|
||||||
additionalProps = {
|
additionalProps = {
|
||||||
ref: fieldName,
|
ref: fieldName,
|
||||||
value: this.state.form[fieldName] || props.value,
|
value: this.getFormValue()[fieldName],
|
||||||
error: this.getFieldError(fieldName),
|
error: this.getFieldError(fieldName),
|
||||||
onChange: this.handleFieldChange.bind(this, fieldName),
|
onChange: this.handleFieldChange.bind(this, fieldName),
|
||||||
onBlur: this.validateField.bind(this, fieldName)
|
onBlur: this.validateField.bind(this, fieldName)
|
||||||
@ -111,8 +115,8 @@ class Form extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAllFieldErrors() {
|
getAllFieldErrors() {
|
||||||
let form = this.state.form;
|
let form = this.getFormValue();
|
||||||
let fields = Object.keys(this.state.form);
|
let fields = Object.keys(form);
|
||||||
let errors = {};
|
let errors = {};
|
||||||
|
|
||||||
_.each(fields, (fieldName) => {
|
_.each(fields, (fieldName) => {
|
||||||
@ -122,7 +126,7 @@ class Form extends React.Component {
|
|||||||
return errors;
|
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);
|
let newErrors = _.clone(errors);
|
||||||
|
|
||||||
if (this.state.validations[fieldName]) {
|
if (this.state.validations[fieldName]) {
|
||||||
@ -156,7 +160,7 @@ class Form extends React.Component {
|
|||||||
handleSubmit(event) {
|
handleSubmit(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const form = _.mapValues(this.state.form, (field) => {
|
const form = _.mapValues(this.getFormValue(), (field) => {
|
||||||
if (field instanceof RichTextEditor.EditorValue) {
|
if (field instanceof RichTextEditor.EditorValue) {
|
||||||
return field.toString('html');
|
return field.toString('html');
|
||||||
} else {
|
} else {
|
||||||
@ -172,13 +176,18 @@ class Form extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleFieldChange(fieldName, event) {
|
handleFieldChange(fieldName, event) {
|
||||||
let form = _.clone(this.state.form);
|
let form = _.clone(this.getFormValue());
|
||||||
|
|
||||||
form[fieldName] = event.target.value;
|
form[fieldName] = event.target.value;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
form: form
|
form: form
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (this.props.onChange) {
|
||||||
|
this.props.onChange(form);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isValidField(node) {
|
isValidField(node) {
|
||||||
@ -203,6 +212,10 @@ class Form extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFormValue() {
|
||||||
|
return this.props.values || this.state.form;
|
||||||
|
}
|
||||||
|
|
||||||
focusFirstErrorField() {
|
focusFirstErrorField() {
|
||||||
let firstErrorField = this.getFirstErrorField();
|
let firstErrorField = this.getFirstErrorField();
|
||||||
|
|
||||||
|
47
client/src/core-components/listing.js
Normal file
47
client/src/core-components/listing.js
Normal 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;
|
25
client/src/core-components/listing.scss
Normal file
25
client/src/core-components/listing.scss
Normal 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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
client/src/core-components/loading.js
Normal file
36
client/src/core-components/loading.js
Normal 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;
|
73
client/src/core-components/loading.scss
Normal file
73
client/src/core-components/loading.scss
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,9 +11,9 @@ class Menu extends React.Component {
|
|||||||
id: React.PropTypes.string,
|
id: React.PropTypes.string,
|
||||||
itemsRole: React.PropTypes.string,
|
itemsRole: React.PropTypes.string,
|
||||||
header: 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({
|
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
|
icon: React.PropTypes.string
|
||||||
})).isRequired,
|
})).isRequired,
|
||||||
selectedIndex: React.PropTypes.number,
|
selectedIndex: React.PropTypes.number,
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
.modal {
|
.modal {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
background-color: rgba(0, 0, 0, 0.8);
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
padding: 50px 0;
|
||||||
|
overflow: auto;
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -5,6 +5,7 @@ import classNames from 'classnames';
|
|||||||
|
|
||||||
// CORE LIBS
|
// CORE LIBS
|
||||||
import Button from 'core-components/button';
|
import Button from 'core-components/button';
|
||||||
|
import Loading from 'core-components/loading';
|
||||||
|
|
||||||
class SubmitButton extends React.Component {
|
class SubmitButton extends React.Component {
|
||||||
|
|
||||||
@ -30,7 +31,7 @@ class SubmitButton extends React.Component {
|
|||||||
|
|
||||||
renderLoading() {
|
renderLoading() {
|
||||||
return (
|
return (
|
||||||
<div className="submit-button__loader"></div>
|
<Loading className="submit-button__loader" />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,46 +1,3 @@
|
|||||||
.submit-button {
|
.submit-button {
|
||||||
position: relative;
|
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 classNames from 'classnames';
|
||||||
|
|
||||||
import Menu from 'core-components/menu';
|
import Menu from 'core-components/menu';
|
||||||
|
import Loading from 'core-components/loading';
|
||||||
|
|
||||||
class Table extends React.Component {
|
class Table extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -13,6 +14,7 @@ class Table extends React.Component {
|
|||||||
})),
|
})),
|
||||||
rows: React.PropTypes.arrayOf(React.PropTypes.object),
|
rows: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||||
pageSize: React.PropTypes.number,
|
pageSize: React.PropTypes.number,
|
||||||
|
loading: React.PropTypes.bool,
|
||||||
type: React.PropTypes.oneOf(['default'])
|
type: React.PropTypes.oneOf(['default'])
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -34,9 +36,10 @@ class Table extends React.Component {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{this.props.rows.map(this.renderRow.bind(this))}
|
{(!this.props.loading) ? this.props.rows.map(this.renderRow.bind(this)) : null}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
{(this.props.loading) ? this.renderLoading() : null}
|
||||||
{(this.props.pageSize && this.props.rows.length > this.props.pageSize) ? this.renderNavigation() : null}
|
{(this.props.pageSize && this.props.rows.length > this.props.pageSize) ? this.renderNavigation() : null}
|
||||||
</div>
|
</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) {
|
onNavigationItemClick(index) {
|
||||||
this.setState({
|
this.setState({
|
||||||
page: index + 1
|
page: index + 1
|
||||||
|
@ -48,4 +48,15 @@
|
|||||||
&__navigation {
|
&__navigation {
|
||||||
margin-top: 10px;
|
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() {
|
getEditorProps() {
|
||||||
return {
|
return {
|
||||||
className: 'text-editor__editor',
|
className: 'text-editor__editor',
|
||||||
value: this.state.value,
|
value: this.props.value || this.state.value,
|
||||||
ref: 'editor',
|
ref: 'editor',
|
||||||
onChange: this.onEditorChange.bind(this),
|
onChange: this.onEditorChange.bind(this),
|
||||||
onFocus: this.onEditorFocus.bind(this),
|
onFocus: this.onEditorFocus.bind(this),
|
||||||
|
@ -31,5 +31,51 @@ module.exports = [
|
|||||||
data: {}
|
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) {
|
const i18nData = function (key, lang) {
|
||||||
return languages[lang][key];
|
return (languages[lang] && languages[lang][key]) || key;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default i18nData
|
export default i18nData
|
||||||
|
@ -18,14 +18,8 @@ export default {
|
|||||||
'EDIT_PROFILE': 'Edit Profile',
|
'EDIT_PROFILE': 'Edit Profile',
|
||||||
'CLOSE_SESSION': 'Close session',
|
'CLOSE_SESSION': 'Close session',
|
||||||
'CREATE_TICKET': 'Create Ticket',
|
'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': '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': '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',
|
'DEPARTMENT': 'Department',
|
||||||
'AUTHOR': 'Author',
|
'AUTHOR': 'Author',
|
||||||
'DATE': 'Date',
|
'DATE': 'Date',
|
||||||
@ -61,6 +55,20 @@ export default {
|
|||||||
'MEDIUM': 'Medium',
|
'MEDIUM': 'Medium',
|
||||||
'LOW': 'Low',
|
'LOW': 'Low',
|
||||||
'TITLE': 'Title',
|
'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
|
//ERRORS
|
||||||
'EMAIL_OR_PASSWORD': 'Email or password invalid',
|
'EMAIL_OR_PASSWORD': 'Email or password invalid',
|
||||||
@ -83,5 +91,7 @@ export default {
|
|||||||
'ARE_YOU_SURE': 'Are you sure?',
|
'ARE_YOU_SURE': 'Are you sure?',
|
||||||
'EMAIL_CHANGED': 'Email has been changed successfully',
|
'EMAIL_CHANGED': 'Email has been changed successfully',
|
||||||
'PASSWORD_CHANGED': 'Password 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) {
|
validate(value, form) {
|
||||||
let alphaMatch = /^[-\sa-zA-Z.]+$/;
|
let alphaMatch = /^[ A-Za-z0-9_@./#&+-äöüÄÖÜß]*$/;
|
||||||
|
|
||||||
if (!alphaMatch.test(value)) return this.getError(this.errorKey);
|
if (!alphaMatch.test(value)) return this.getError(this.errorKey);
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ class LengthValidator extends Validator {
|
|||||||
this.errorKey = errorKey;
|
this.errorKey = errorKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
validate(value, form) {
|
validate(value = '', form = {}) {
|
||||||
if (value instanceof RichTextEditor.EditorValue) {
|
if (value instanceof RichTextEditor.EditorValue) {
|
||||||
value = value.getEditorState().getCurrentContent().getPlainText();
|
value = value.getEditorState().getCurrentContent().getPlainText();
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,12 @@ import { routerReducer } from 'react-router-redux';
|
|||||||
import sessionReducer from 'reducers/session-reducer';
|
import sessionReducer from 'reducers/session-reducer';
|
||||||
import configReducer from 'reducers/config-reducer';
|
import configReducer from 'reducers/config-reducer';
|
||||||
import modalReducer from 'reducers/modal-reducer';
|
import modalReducer from 'reducers/modal-reducer';
|
||||||
|
import adminDataReducer from 'reducers/admin-data-reducer';
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
session: sessionReducer,
|
session: sessionReducer,
|
||||||
config: configReducer,
|
config: configReducer,
|
||||||
modal: modalReducer,
|
modal: modalReducer,
|
||||||
|
adminData: adminDataReducer,
|
||||||
routing: routerReducer
|
routing: routerReducer
|
||||||
});
|
});
|
28
client/src/reducers/admin-data-reducer.js
Normal file
28
client/src/reducers/admin-data-reducer.js
Normal 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();
|
@ -19,6 +19,8 @@ class ModalReducer extends Reducer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onOpenModal(state, payload) {
|
onOpenModal(state, payload) {
|
||||||
|
document.body.setAttribute('style', 'overflow:hidden');
|
||||||
|
|
||||||
return _.extend({}, state, {
|
return _.extend({}, state, {
|
||||||
opened: true,
|
opened: true,
|
||||||
content: payload
|
content: payload
|
||||||
@ -26,6 +28,8 @@ class ModalReducer extends Reducer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onCloseModal(state) {
|
onCloseModal(state) {
|
||||||
|
document.body.setAttribute('style', '');
|
||||||
|
|
||||||
return _.extend({}, state, {
|
return _.extend({}, state, {
|
||||||
opened: false,
|
opened: false,
|
||||||
content: null
|
content: null
|
||||||
|
@ -3,6 +3,7 @@ include 'ticket/create.php';
|
|||||||
include 'ticket/comment.php';
|
include 'ticket/comment.php';
|
||||||
include 'ticket/get.php';
|
include 'ticket/get.php';
|
||||||
include 'ticket/add-custom-response.php';
|
include 'ticket/add-custom-response.php';
|
||||||
|
include 'ticket/delete-custom-response.php';
|
||||||
include 'ticket/edit-custom-response.php';
|
include 'ticket/edit-custom-response.php';
|
||||||
include 'ticket/get-custom-responses.php';
|
include 'ticket/get-custom-responses.php';
|
||||||
|
|
||||||
@ -13,6 +14,7 @@ $ticketControllers->addController(new CreateController);
|
|||||||
$ticketControllers->addController(new CommentController);
|
$ticketControllers->addController(new CommentController);
|
||||||
$ticketControllers->addController(new TicketGetController);
|
$ticketControllers->addController(new TicketGetController);
|
||||||
$ticketControllers->addController(new AddCustomResponseController);
|
$ticketControllers->addController(new AddCustomResponseController);
|
||||||
|
$ticketControllers->addController(new DeleteCustomResponseController);
|
||||||
$ticketControllers->addController(new EditCustomResponseController);
|
$ticketControllers->addController(new EditCustomResponseController);
|
||||||
$ticketControllers->addController(new GetCustomResponsesController);
|
$ticketControllers->addController(new GetCustomResponsesController);
|
||||||
|
|
||||||
|
26
server/controllers/ticket/delete-custom-response.php
Normal file
26
server/controllers/ticket/delete-custom-response.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -120,6 +120,10 @@ abstract class DataStore {
|
|||||||
return RedBean::store($this->getBeanInstance());
|
return RedBean::store($this->getBeanInstance());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function trash() {
|
||||||
|
RedBean::trash($this->getBeanInstance());
|
||||||
|
}
|
||||||
|
|
||||||
public function delete() {
|
public function delete() {
|
||||||
RedBean::trash($this->getBeanInstance());
|
RedBean::trash($this->getBeanInstance());
|
||||||
unset($this);
|
unset($this);
|
||||||
|
@ -53,4 +53,18 @@ describe 'CustomResponses' do
|
|||||||
(result['data'][0]['language']).should.equal('en')
|
(result['data'][0]['language']).should.equal('en')
|
||||||
end
|
end
|
||||||
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
|
end
|
Loading…
x
Reference in New Issue
Block a user