commit
a4c44fb9ab
|
@ -88,6 +88,7 @@ class TicketEvent extends React.Component {
|
|||
<span className="ticket-event__comment-badge-container">
|
||||
<span className="ticket-event__comment-badge">{i18n((this.props.author.staff) ? 'STAFF' : 'CUSTOMER')}</span>
|
||||
</span>
|
||||
{this.props.author.customfields.map(this.renderCustomFieldValue.bind(this))}
|
||||
{(this.props.private*1) ? this.renderPrivateBadge() : null}
|
||||
</div>
|
||||
<div className="ticket-event__comment-date">{DateTransformer.transformToString(this.props.date)}</div>
|
||||
|
@ -198,7 +199,17 @@ class TicketEvent extends React.Component {
|
|||
<div className="ticket-event__file">
|
||||
{node}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
renderCustomFieldValue(customField) {
|
||||
return (
|
||||
<span className="ticket-event__comment-badge-container">
|
||||
<span className="ticket-event__comment-badge">
|
||||
{customField.customfield}: <span className="ticket-event__comment-badge-value">{customField.value}</span>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
getClass() {
|
||||
|
|
|
@ -96,7 +96,7 @@
|
|||
border-top: none;
|
||||
padding: 20px 10px;
|
||||
text-align: left;
|
||||
|
||||
|
||||
img {
|
||||
max-width:100%;
|
||||
}
|
||||
|
@ -111,6 +111,10 @@
|
|||
font-size: 12px;
|
||||
}
|
||||
|
||||
&__comment-badge-value {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
&_staff {
|
||||
.ticket-event__icon {
|
||||
background-color: $primary-blue;
|
||||
|
|
|
@ -39,6 +39,7 @@ import AdminPanelCustomResponses from 'app/admin/panel/tickets/admin-panel-custo
|
|||
import AdminPanelListUsers from 'app/admin/panel/users/admin-panel-list-users';
|
||||
import AdminPanelViewUser from 'app/admin/panel/users/admin-panel-view-user';
|
||||
import AdminPanelBanUsers from 'app/admin/panel/users/admin-panel-ban-users';
|
||||
import AdminPanelCustomFields from 'app/admin/panel/users/admin-panel-custom-fields';
|
||||
|
||||
import AdminPanelListArticles from 'app/admin/panel/articles/admin-panel-list-articles';
|
||||
import AdminPanelViewArticle from 'app/admin/panel/articles/admin-panel-view-article';
|
||||
|
@ -120,6 +121,7 @@ export default (
|
|||
<Route path="list-users" component={AdminPanelListUsers} />
|
||||
<Route path="view-user/:userId" component={AdminPanelViewUser} />
|
||||
<Route path="ban-users" component={AdminPanelBanUsers} />
|
||||
<Route path="custom-fields" component={AdminPanelCustomFields} />
|
||||
</Route>
|
||||
|
||||
<Route path="articles">
|
||||
|
|
|
@ -153,6 +153,11 @@ class AdminPanelMenu extends React.Component {
|
|||
name: i18n('BAN_USERS'),
|
||||
path: '/admin/panel/users/ban-users',
|
||||
level: 1
|
||||
},
|
||||
{
|
||||
name: i18n('CUSTOM_FIELDS'),
|
||||
path: '/admin/panel/users/custom-fields',
|
||||
level: 1
|
||||
}
|
||||
])
|
||||
},
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import {connect} from 'react-redux';
|
||||
import randomString from 'random-string';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
|
@ -55,6 +56,7 @@ class AdminPanelEmailSettings extends React.Component {
|
|||
['imap-host']: '',
|
||||
['imap-user']: '',
|
||||
['imap-pass']: 'HIDDEN',
|
||||
['imap-token']: '',
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -135,7 +137,7 @@ class AdminPanelEmailSettings extends React.Component {
|
|||
<SubmitButton className="admin-panel-email-settings__submit" type="secondary"
|
||||
size="small">{i18n('SAVE')}</SubmitButton>
|
||||
<SubmitButton type="tertiary" size="small" onClick={this.testSMTP.bind(this)}>
|
||||
Test
|
||||
{i18n('TEST')}
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</Form>
|
||||
|
@ -148,11 +150,12 @@ class AdminPanelEmailSettings extends React.Component {
|
|||
<FormField name="imap-host" label={i18n('IMAP_SERVER')} fieldProps={{size: 'large'}}/>
|
||||
<FormField name="imap-user" label={i18n('IMAP_USER')} fieldProps={{size: 'large'}}/>
|
||||
<FormField name="imap-pass" label={i18n('IMAP_PASSWORD')} fieldProps={{size: 'large'}}/>
|
||||
<FormField name="imap-token" label={i18n('IMAP_TOKEN')} infoMessage={i18n('IMAP_TOKEN_DESCRIPTION')} fieldProps={{size: 'large', icon: 'refresh', onIconClick: this.generateImapToken.bind(this)}}/>
|
||||
<div className="admin-panel-email-settings__server-form-buttons">
|
||||
<SubmitButton className="admin-panel-email-settings__submit" type="secondary"
|
||||
size="small">{i18n('SAVE')}</SubmitButton>
|
||||
<SubmitButton type="tertiary" size="small" onClick={this.testIMAP.bind(this)}>
|
||||
Test
|
||||
{i18n('TEST')}
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</Form>
|
||||
|
@ -334,6 +337,15 @@ class AdminPanelEmailSettings extends React.Component {
|
|||
AreYouSure.openModal(i18n('WILL_RECOVER_EMAIL_TEMPLATE'), this.recoverEmailTemplate.bind(this));
|
||||
}
|
||||
|
||||
generateImapToken() {
|
||||
this.setState({
|
||||
imapForm: {
|
||||
...this.state.imapForm,
|
||||
['imap-token']: randomString({length: 20}),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
submitEmailAddress(form) {
|
||||
this.editSettings(form, 'EMAIL_SUCCESS');
|
||||
}
|
||||
|
@ -481,6 +493,7 @@ class AdminPanelEmailSettings extends React.Component {
|
|||
['imap-host']: result.data['imap-host'],
|
||||
['imap-user']: result.data['imap-user'],
|
||||
['imap-pass']: 'HIDDEN',
|
||||
['imap-token']: result.data['imap-token'],
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
|
||||
import Header from 'core-components/header';
|
||||
import Button from 'core-components/button';
|
||||
import SubmitButton from 'core-components/submit-button';
|
||||
import Form from 'core-components/form';
|
||||
import FormField from 'core-components/form-field';
|
||||
import Message from 'core-components/message';
|
||||
|
||||
class AdminPanelCustomFieldForm extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
onClose: React.PropTypes.func,
|
||||
onChange: React.PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
loading: false,
|
||||
error: null,
|
||||
addForm: {},
|
||||
addFormOptions: [],
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="admin-panel-custom-field-form">
|
||||
<Header title={i18n('NEW_CUSTOM_FIELD')} description={i18n('NEW_CUSTOM_FIELD_DESCRIPTION')} />
|
||||
<div className="admin-panel-custom-field-form__form-container">
|
||||
<Form
|
||||
className="admin-panel-custom-field-form__form"
|
||||
loading={this.state.loading}
|
||||
values={this.state.addForm}
|
||||
onChange={this.onAddFormChange.bind(this)}
|
||||
onSubmit={this.onSubmit.bind(this)}>
|
||||
<FormField name="name" validation="NAME" label={i18n('NAME')} field="input" fieldProps={{size: 'large'}} required/>
|
||||
<FormField name="description" label={i18n('FIELD_DESCRIPTION')} field="input" fieldProps={{size: 'large'}}/>
|
||||
<FormField name="type" label={i18n('TYPE')} field="select" fieldProps={{size: 'large', items: [{content: i18n('TEXT_INPUT')}, {content: i18n('SELECT_INPUT')}]}} required/>
|
||||
{this.state.addForm.type ? this.renderOptionFormFields() : null}
|
||||
{this.state.error ? this.renderErrorMessage() : null}
|
||||
<div className="admin-panel-custom-field-form__buttons">
|
||||
<SubmitButton>{i18n('SUBMIT')}</SubmitButton>
|
||||
<Button onClick={this.props.onClose} type="link">{i18n('CLOSE')}</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderErrorMessage() {
|
||||
return (
|
||||
<Message type="error">
|
||||
{this.state.error}
|
||||
</Message>
|
||||
);
|
||||
}
|
||||
|
||||
renderOptionFormFields() {
|
||||
return (
|
||||
<div className="admin-panel-custom-field-form__options">
|
||||
<div className="admin-panel-custom-field-form__options-title">{i18n('OPTIONS')}</div>
|
||||
{this.state.addFormOptions.map(this.renderFormOption.bind(this))}
|
||||
<Button className="admin-panel-custom-field-form__option-add-button" iconName="plus" size="medium" type="secondary" onClick={this.onAddOptionClick.bind(this)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderFormOption(option, index) {
|
||||
return (
|
||||
<div key={index} className="admin-panel-custom-field-form__option">
|
||||
<FormField className="admin-panel-custom-field-form__option-field" name={`option_${index}`} label={i18n('OPTION', {index: index+1})} type="input"/>
|
||||
<Button className="admin-panel-custom-field-form__option-delete-button" size="medium" iconName="times" onClick={this.onDeleteOptionClick.bind(this, index)}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onAddOptionClick(event) {
|
||||
event.preventDefault();
|
||||
|
||||
let addFormOptions = _.clone(this.state.addFormOptions);
|
||||
|
||||
addFormOptions.push("");
|
||||
|
||||
this.setState({ addFormOptions });
|
||||
}
|
||||
|
||||
onDeleteOptionClick(index, event) {
|
||||
event.preventDefault();
|
||||
|
||||
let addForm = _.clone(this.state.addForm);
|
||||
let addFormOptions = this.state.addFormOptions.filter((option, idx) => idx != index);
|
||||
|
||||
Object.keys(addForm).forEach(key => _.includes(key, 'option_') ? delete addForm[key] : null);
|
||||
addFormOptions.forEach((option, _index) => addForm[`option_${_index}`] = option);
|
||||
|
||||
this.setState({addForm, addFormOptions});
|
||||
}
|
||||
|
||||
onAddFormChange(addForm) {
|
||||
const addFormOptions = this.state.addFormOptions.map((option, index) => addForm[`option_${index}`]);
|
||||
|
||||
this.setState({addForm, addFormOptions});
|
||||
}
|
||||
|
||||
onSubmit(form) {
|
||||
this.setState({loading: true, message: null});
|
||||
API.call({
|
||||
path: '/system/add-custom-field',
|
||||
data: {
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
type: form.type ? 'select' : 'text',
|
||||
options: form.type ? JSON.stringify(this.state.addFormOptions) : []
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
this.setState({loading: false, message: null});
|
||||
if(this.props.onChange) this.props.onChange();
|
||||
})
|
||||
.catch(result => this.setState({loading: false, error: result.message}));
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminPanelCustomFieldForm;
|
|
@ -0,0 +1,30 @@
|
|||
.admin-panel-custom-field-form {
|
||||
min-width: 400px;
|
||||
|
||||
&__options {
|
||||
|
||||
&-title {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
|
||||
&-add-button {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
&-delete-button {
|
||||
margin-top: 7px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
|
||||
import AdminPanelCustomFieldForm from 'app/admin/panel/users/admin-panel-custom-field-form';
|
||||
import ModalContainer from 'app-components/modal-container';
|
||||
import AreYouSure from 'app-components/are-you-sure';
|
||||
|
||||
import Header from 'core-components/header';
|
||||
import Button from 'core-components/button';
|
||||
import Icon from 'core-components/icon';
|
||||
import InfoTooltip from 'core-components/info-tooltip';
|
||||
import Table from 'core-components/table';
|
||||
|
||||
class AdminPanelCustomFields extends React.Component {
|
||||
|
||||
state = {
|
||||
customFields: [],
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.retrieveCustomFields();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="admin-panel-custom-fields">
|
||||
<Header title={i18n('CUSTOM_FIELDS')} description={i18n('CUSTOM_FIELDS_DESCRIPTION')} />
|
||||
{this.renderCustomFieldList()}
|
||||
<div className="admin-panel-custom-fields__add-button">
|
||||
<Button type="secondary" onClick={this.onNewCustomFieldClick.bind(this)}>
|
||||
<Icon name="plus"/> {i18n('NEW_CUSTOM_FIELD')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderCustomFieldList() {
|
||||
return (
|
||||
<Table
|
||||
className="admin-panel-custom-fields__list"
|
||||
headers={[
|
||||
{key: 'name', value: 'Name'},
|
||||
{key: 'type', value: 'Type'},
|
||||
{key: 'options', value: 'Options'},
|
||||
{key: 'actions', value: ''},
|
||||
]}
|
||||
rows={this.state.customFields.map(this.getCustomField.bind(this))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
getCustomField(customField, index) {
|
||||
const {id, description, name, type, options} = customField;
|
||||
let descriptionInfoTooltip = null;
|
||||
|
||||
if(description) {
|
||||
descriptionInfoTooltip = <InfoTooltip text={description} />;
|
||||
}
|
||||
|
||||
return {
|
||||
name: <div>{name} {descriptionInfoTooltip}</div>,
|
||||
type,
|
||||
options: JSON.stringify(options.map(option => option.name)),
|
||||
actions: <Button size="medium" onClick={this.onDeleteCustomField.bind(this, id)}>Remove</Button>,
|
||||
}
|
||||
}
|
||||
|
||||
onNewCustomFieldClick() {
|
||||
ModalContainer.openModal(
|
||||
<AdminPanelCustomFieldForm
|
||||
onClose={ModalContainer.closeModal}
|
||||
onChange={() => {
|
||||
this.retrieveCustomFields();
|
||||
ModalContainer.closeModal();
|
||||
}}/>
|
||||
);
|
||||
}
|
||||
|
||||
onDeleteCustomField(id) {
|
||||
AreYouSure.openModal(i18n('DELETE_CUSTOM_FIELD_SURE'), this.deleteCustomField.bind(this, id));
|
||||
}
|
||||
|
||||
deleteCustomField(id) {
|
||||
API.call({
|
||||
path: '/system/delete-custom-field',
|
||||
data: {id}
|
||||
})
|
||||
.catch(() => this.setState({}))
|
||||
.then(() => this.retrieveCustomFields());
|
||||
}
|
||||
|
||||
retrieveCustomFields() {
|
||||
API.call({
|
||||
path: '/system/get-custom-fields',
|
||||
data: {}
|
||||
})
|
||||
.catch(() => this.setState({}))
|
||||
.then(result => this.setState({
|
||||
customFields: result.data
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminPanelCustomFields;
|
|
@ -0,0 +1,11 @@
|
|||
.admin-panel-custom-fields {
|
||||
|
||||
&__list {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&__add-button {
|
||||
text-align: left;
|
||||
margin-top: 14px;
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ class AdminPanelViewUser extends React.Component {
|
|||
email: '',
|
||||
verified: true,
|
||||
tickets: [],
|
||||
customfields: [],
|
||||
invalid: false,
|
||||
loading: true,
|
||||
disabled: false
|
||||
|
@ -64,6 +65,7 @@ class AdminPanelViewUser extends React.Component {
|
|||
{(!this.state.verified) ? this.renderNotVerified() : null}
|
||||
</div>
|
||||
</div>
|
||||
{this.state.customfields.map(this.renderCustomField.bind(this))}
|
||||
<div className="admin-panel-view-user__action-buttons">
|
||||
<Button
|
||||
className="admin-panel-view-user__action-button"
|
||||
|
@ -98,6 +100,17 @@ class AdminPanelViewUser extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
renderCustomField(customfield) {
|
||||
return (
|
||||
<div className="admin-panel-view-user__info-item">
|
||||
{customfield.customfield}
|
||||
<div className="admin-panel-view-user__info-box">
|
||||
{customfield.value}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getTicketListProps() {
|
||||
return {
|
||||
type: 'secondary',
|
||||
|
@ -115,6 +128,7 @@ class AdminPanelViewUser extends React.Component {
|
|||
verified: result.data.verified,
|
||||
tickets: result.data.tickets,
|
||||
disabled: result.data.disabled,
|
||||
customfields: result.data.customfields,
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import _ from 'lodash';
|
||||
|
||||
import API from 'lib-app/api-call';
|
||||
import i18n from 'lib-app/i18n';
|
||||
|
||||
import SessionActions from 'actions/session-actions';
|
||||
import AreYouSure from 'app-components/are-you-sure';
|
||||
|
||||
import Header from 'core-components/header';
|
||||
|
@ -13,17 +16,41 @@ import Message from 'core-components/message';
|
|||
|
||||
class DashboardEditProfilePage extends React.Component {
|
||||
|
||||
state= {
|
||||
static propTypes = {
|
||||
userCustomFields: React.PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
userCustomFields: {},
|
||||
};
|
||||
|
||||
state = {
|
||||
loadingEmail: false,
|
||||
loadingPass: false,
|
||||
messageEmail:'',
|
||||
messagePass:''
|
||||
messageEmail: '',
|
||||
messagePass: '',
|
||||
customFields: [],
|
||||
customFieldsFrom: {},
|
||||
loadingCustomFields: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.retrieveCustomFields();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="edit-profile-page">
|
||||
<Header title={i18n('EDIT_PROFILE')} description={i18n('EDIT_PROFILE_VIEW_DESCRIPTION')} />
|
||||
<div className="edit-profile-page__title">{i18n('ADDITIONAL_FIELDS')}</div>
|
||||
<Form loading={this.state.loadingCustomFields} values={this.state.customFieldsFrom} onChange={form => this.setState({customFieldsFrom: form})} onSubmit={this.onCustomFieldsSubmit.bind(this)}>
|
||||
<div className="edit-profile-page__custom-fields">
|
||||
{this.state.customFields.map(this.renderCustomField.bind(this))}
|
||||
</div>
|
||||
<div className="row">
|
||||
<SubmitButton>{i18n('SAVE')}</SubmitButton>
|
||||
</div>
|
||||
</Form>
|
||||
<div className="edit-profile-page__title">{i18n('EDIT_EMAIL')}</div>
|
||||
<Form loading={this.state.loadingEmail} onSubmit={this.onSubmitEditEmail.bind(this)}>
|
||||
<FormField name="newEmail" label={i18n('NEW_EMAIL')} field="input" validation="EMAIL" fieldProps={{size:'large'}} required/>
|
||||
|
@ -41,6 +68,25 @@ class DashboardEditProfilePage extends React.Component {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderCustomField(customField, key) {
|
||||
if(customField.type === 'text') {
|
||||
return (
|
||||
<div className="edit-profile-page__custom-field" key={key}>
|
||||
<FormField name={customField.name} label={customField.name} infoMessage={customField.description} field="input" fieldProps={{size:'small'}}/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const items = customField.options.map(option => ({content: option.name, value: option.name}));
|
||||
|
||||
return (
|
||||
<div className="edit-profile-page__custom-field" key={key}>
|
||||
<FormField name={customField.name} label={customField.name} infoMessage={customField.description} field="select" fieldProps={{size:'small', items}}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderMessageEmail() {
|
||||
switch (this.state.messageEmail) {
|
||||
case 'success':
|
||||
|
@ -52,6 +98,7 @@ class DashboardEditProfilePage extends React.Component {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
renderMessagePass() {
|
||||
switch (this.state.messagePass) {
|
||||
case 'success':
|
||||
|
@ -62,10 +109,37 @@ class DashboardEditProfilePage extends React.Component {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
onCustomFieldsSubmit(form) {
|
||||
const {customFields} = this.state;
|
||||
const parsedFrom = {}
|
||||
|
||||
customFields.forEach(customField => {
|
||||
if(customField.type === 'select') {
|
||||
parsedFrom[`customfield_${customField.name}`] = customField.options[form[customField.name]].name;
|
||||
} else {
|
||||
parsedFrom[`customfield_${customField.name}`] = form[customField.name];
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({
|
||||
loadingCustomFields: true,
|
||||
});
|
||||
|
||||
API.call({
|
||||
path: '/user/edit-custom-fields',
|
||||
data: parsedFrom
|
||||
}).then(() => {
|
||||
this.setState({loadingCustomFields: false});
|
||||
this.props.dispatch(SessionActions.getUserData());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
onSubmitEditEmail(formState) {
|
||||
AreYouSure.openModal(i18n('EMAIL_WILL_CHANGE'), this.callEditEmailAPI.bind(this, formState));
|
||||
}
|
||||
|
||||
|
||||
onSubmitEditPassword(formState) {
|
||||
AreYouSure.openModal(i18n('PASSWORD_WILL_CHANGE'), this.callEditPassAPI.bind(this, formState));
|
||||
}
|
||||
|
@ -115,6 +189,39 @@ class DashboardEditProfilePage extends React.Component {
|
|||
}.bind(this));
|
||||
}
|
||||
|
||||
retrieveCustomFields() {
|
||||
API.call({
|
||||
path: '/system/get-custom-fields',
|
||||
data: {}
|
||||
})
|
||||
.then(result => {
|
||||
const customFieldsFrom = {};
|
||||
const {userCustomFields} = this.props;
|
||||
result.data.forEach(customField => {
|
||||
if(customField.type === 'select') {
|
||||
const index = _.indexOf(customField.options.map(option => option.name), userCustomFields[customField.name]);
|
||||
customFieldsFrom[customField.name] = (index === -1 ? 0 : index);
|
||||
} else {
|
||||
customFieldsFrom[customField.name] = userCustomFields[customField.name] || '';
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({
|
||||
customFields: result.data,
|
||||
customFieldsFrom,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default DashboardEditProfilePage;
|
||||
export default connect((store) => {
|
||||
const userCustomFields = {};
|
||||
|
||||
store.session.userCustomFields.forEach(customField => {
|
||||
userCustomFields[customField.customfield] = customField.value;
|
||||
});
|
||||
|
||||
return {
|
||||
userCustomFields: userCustomFields || {},
|
||||
};
|
||||
})(DashboardEditProfilePage);
|
||||
|
|
|
@ -13,4 +13,13 @@
|
|||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&__custom-fields {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&__custom-field {
|
||||
display: inline-block;
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,18 +16,28 @@ import Header from 'core-components/header';
|
|||
|
||||
class MainSignUpWidget extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
onSuccess: React.PropTypes.func,
|
||||
className: React.PropTypes.string
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
loading: false,
|
||||
email: null
|
||||
email: null,
|
||||
customFields: []
|
||||
};
|
||||
}
|
||||
static propTypes = {
|
||||
onSuccess: React.PropTypes.func,
|
||||
className: React.PropTypes.string
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
API.call({
|
||||
path: '/system/get-custom-fields',
|
||||
data: {}
|
||||
})
|
||||
.then(result => this.setState({customFields: result.data}));
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
|
@ -39,6 +49,7 @@ class MainSignUpWidget extends React.Component {
|
|||
<FormField {...this.getInputProps()} label={i18n('EMAIL')} name="email" validation="EMAIL" required/>
|
||||
<FormField {...this.getInputProps(true)} label={i18n('PASSWORD')} name="password" validation="PASSWORD" required/>
|
||||
<FormField {...this.getInputProps(true)} label={i18n('REPEAT_PASSWORD')} name="repeated-password" validation="REPEAT_PASSWORD" required/>
|
||||
{this.state.customFields.map(this.renderCustomField.bind(this))}
|
||||
</div>
|
||||
<div className="signup-widget__captcha">
|
||||
<Captcha ref="captcha"/>
|
||||
|
@ -51,6 +62,31 @@ class MainSignUpWidget extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
renderCustomField(customField, key) {
|
||||
if(customField.type === 'text') {
|
||||
return (
|
||||
<FormField {...this.getInputProps()}
|
||||
name={`customfield_${customField.name}`}
|
||||
key={key}
|
||||
label={customField.name}
|
||||
infoMessage={customField.description}
|
||||
field="input"/>
|
||||
);
|
||||
} else {
|
||||
const items = customField.options.map(option => ({content: option.name, value: option.name}));
|
||||
|
||||
return (
|
||||
<FormField
|
||||
name={`customfield_${customField.name}`}
|
||||
key={key}
|
||||
label={customField.name}
|
||||
infoMessage={customField.description}
|
||||
field="select"
|
||||
fieldProps={{size:'medium', items}}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderMessage() {
|
||||
switch (this.state.message) {
|
||||
case 'success':
|
||||
|
@ -98,9 +134,17 @@ class MainSignUpWidget extends React.Component {
|
|||
loading: true
|
||||
});
|
||||
|
||||
const form = _.clone(formState);
|
||||
|
||||
this.state.customFields.forEach(customField => {
|
||||
if(customField.type === 'select') {
|
||||
form[`customfield_${customField.name}`] = customField.options[form[`customfield_${customField.name}`]].name;
|
||||
}
|
||||
})
|
||||
|
||||
API.call({
|
||||
path: '/user/signup',
|
||||
data: _.extend({captcha: captcha.getValue()}, formState)
|
||||
data: _.extend({captcha: captcha.getValue()}, form)
|
||||
}).then(this.onSignupSuccess.bind(this)).catch(this.onSignupFail.bind(this));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,8 @@ class Input extends React.Component {
|
|||
password: React.PropTypes.bool,
|
||||
required: React.PropTypes.bool,
|
||||
icon: React.PropTypes.string,
|
||||
error: React.PropTypes.string
|
||||
error: React.PropTypes.string,
|
||||
onIconClick: React.PropTypes.func
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -38,7 +39,7 @@ class Input extends React.Component {
|
|||
let icon = null;
|
||||
|
||||
if (this.props.icon) {
|
||||
icon = <span className="input__icon"><Icon name={this.props.icon} /></span>
|
||||
icon = <span className="input__icon" onClick={this.onIconClick.bind(this)}><Icon name={this.props.icon} /></span>
|
||||
}
|
||||
|
||||
return icon;
|
||||
|
@ -66,6 +67,7 @@ class Input extends React.Component {
|
|||
'input': true,
|
||||
'input_with-icon': (this.props.icon),
|
||||
'input_errored': (this.props.errored),
|
||||
'input_icon-clickable': (this.props.onIconClick),
|
||||
['input_' + this.props.size]: true,
|
||||
|
||||
[this.props.className]: (this.props.className)
|
||||
|
@ -74,6 +76,14 @@ class Input extends React.Component {
|
|||
return classNames(classes);
|
||||
}
|
||||
|
||||
onIconClick(event) {
|
||||
if(this.props.onIconClick) {
|
||||
event.preventDefault();
|
||||
this.focus();
|
||||
this.props.onIconClick(event);
|
||||
}
|
||||
}
|
||||
|
||||
focus() {
|
||||
if (this.refs.nativeInput) {
|
||||
this.refs.nativeInput.focus();
|
||||
|
|
|
@ -58,4 +58,11 @@
|
|||
border: 1px solid $primary-red;
|
||||
}
|
||||
}
|
||||
|
||||
&_icon-clickable {
|
||||
|
||||
.input__icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -61,7 +61,7 @@ export default {
|
|||
'HIGH': 'Hoch',
|
||||
'MEDIUM': 'Mittel',
|
||||
'LOW': 'Niedrig',
|
||||
'TITLE': 'Titel',
|
||||
'TITLE': 'Betreff',
|
||||
'CONTENT': 'Inhalt',
|
||||
'SAVE': 'Speichern',
|
||||
'DISCARD_CHANGES': 'Änderungen verwerfen',
|
||||
|
|
|
@ -144,6 +144,8 @@ export default {
|
|||
'IMAP_USER': 'IMAP User',
|
||||
'IMAP_SERVER': 'IMAP Server',
|
||||
'IMAP_PASSWORD': 'IMAP Password',
|
||||
'IMAP_TOKEN': 'IMAP Token',
|
||||
'IMAP_TOKEN_DESCRIPTION': 'Use this token to authenticate the polling request.',
|
||||
'PORT': 'Port',
|
||||
'RECAPTCHA_PUBLIC_KEY': 'Recaptcha Public Key',
|
||||
'RECAPTCHA_PRIVATE_KEY': 'Recaptcha Private Key',
|
||||
|
@ -196,6 +198,15 @@ export default {
|
|||
'IMAGE_HEADER_URL': 'Image header URL',
|
||||
'IMAGE_HEADER_DESCRIPTION': 'Image that will be used as header of the email',
|
||||
'EMAIL_SETTINGS': 'Email Settings',
|
||||
'ADDITIONAL_FIELDS': 'Additonal Fields',
|
||||
'NEW_CUSTOM_FIELD': 'New Custom field',
|
||||
'TYPE': 'Type',
|
||||
'SELECT_INPUT': 'Select input',
|
||||
'TEXT_INPUT': 'Text input',
|
||||
'OPTION': 'Option {index}',
|
||||
'OPTIONS': 'Options',
|
||||
'FIELD_DESCRIPTION': 'Field description (Optional)',
|
||||
'CUSTOM_FIELDS': 'Custom fields',
|
||||
|
||||
'CHART_CREATE_TICKET': 'Tickets created',
|
||||
'CHART_CLOSE': 'Tickets closed',
|
||||
|
@ -316,6 +327,8 @@ export default {
|
|||
'PRIVATE_DEPARTMENT_DESCRIPTION': 'This department will only be seen by staff members',
|
||||
'EMAIL_SETTINGS_DESCRIPTION': 'Here you can edit the settings for receiving and sending email to your customers.',
|
||||
'IMAP_POLLING_DESCRIPTION': 'Inbox checking will not be done automatically by OpenSupports. You have to make POST requests periodically to this url to process the emails: {url}',
|
||||
'NEW_CUSTOM_FIELD_DESCRIPTION': 'Here you can create a custom field for an user, it can be a blank text box or a fixed set of options.',
|
||||
'CUSTOM_FIELDS_DESCRIPTION': 'Custom fields are defined additional fields the users are able to fill to provide more information about them.',
|
||||
|
||||
//ERRORS
|
||||
'EMAIL_OR_PASSWORD': 'Email or password invalid',
|
||||
|
@ -376,12 +389,14 @@ export default {
|
|||
'SUCCESSFUL_CONNECTION': 'Successful connection',
|
||||
'UNSUCCESSFUL_CONNECTION': 'Unsuccessful connection',
|
||||
'SERVER_CREDENTIALS_WORKING': 'Server credentials are working correctly',
|
||||
'DELETE_CUSTOM_FIELD_SURE': 'Some users may be using this field. Are you sure you want to delete it?',
|
||||
|
||||
'LAST_7_DAYS': 'Last 7 days',
|
||||
'LAST_30_DAYS': 'Last 30 days',
|
||||
'LAST_90_DAYS': 'Last 90 days',
|
||||
'LAST_365_DAYS': 'Last 365 days',
|
||||
|
||||
'TEST': 'Test',
|
||||
'ACTIVITY_COMMENT_THIS': 'commented this ticket',
|
||||
'ACTIVITY_ASSIGN_THIS': 'assigned this ticket to',
|
||||
'ACTIVITY_UN_ASSIGN_THIS': 'unassigned this ticket to',
|
||||
|
|
|
@ -113,7 +113,8 @@ class SessionReducer extends Reducer {
|
|||
userLevel: userData.level,
|
||||
userDepartments: userData.departments,
|
||||
userTickets: userData.tickets,
|
||||
userSendEmailOnNewTicket: userData.sendEmailOnNewTicket * 1
|
||||
userSendEmailOnNewTicket: userData.sendEmailOnNewTicket * 1,
|
||||
userCustomFields: userData.customfields
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -132,7 +133,8 @@ class SessionReducer extends Reducer {
|
|||
userDepartments: userData.departments,
|
||||
userTickets: userData.tickets,
|
||||
userId: userId,
|
||||
userSendEmailOnNewTicket: userData.sendEmailOnNewTicket * 1
|
||||
userSendEmailOnNewTicket: userData.sendEmailOnNewTicket * 1,
|
||||
userCustomFields: userData.customfields
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,9 @@
|
|||
"ifsnop/mysqldump-php": "2.*",
|
||||
"ezyang/htmlpurifier": "^4.8",
|
||||
"codeguy/upload": "^1.3",
|
||||
"php-imap/php-imap": "^3.0"
|
||||
"php-imap/php-imap": "^3.0",
|
||||
"willdurand/email-reply-parser": "^2.8",
|
||||
"ext-fileinfo": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^5.7"
|
||||
|
|
|
@ -69,7 +69,7 @@ class GetStaffController extends Controller {
|
|||
'level' => $user->level,
|
||||
'staff' => true,
|
||||
'departments' => $parsedDepartmentList,
|
||||
'tickets' => $user->sharedTicketList->toArray(),
|
||||
'tickets' => $user->sharedTicketList->toArray(true),
|
||||
'sendEmailOnNewTicket' => $user->sendEmailOnNewTicket
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -31,5 +31,9 @@ $systemControllerGroup->addController(new DisableUserSystemController);
|
|||
$systemControllerGroup->addController(new EnableUserSystemController);
|
||||
$systemControllerGroup->addController(new TestSMTPController);
|
||||
$systemControllerGroup->addController(new TestIMAPController);
|
||||
$systemControllerGroup->addController(new EmailPollingController);
|
||||
$systemControllerGroup->addController(new AddCustomFieldController);
|
||||
$systemControllerGroup->addController(new DeleteCustomFieldController);
|
||||
$systemControllerGroup->addController(new GetCustomFieldsController);
|
||||
|
||||
$systemControllerGroup->finalize();
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
use Respect\Validation\Validator as DataValidator;
|
||||
|
||||
/**
|
||||
* @api {post} /system/add-custom-field Add a custom field
|
||||
* @apiVersion 4.4.0
|
||||
*
|
||||
* @apiName Add Custom field
|
||||
*
|
||||
* @apiGroup System
|
||||
*
|
||||
* @apiDescription This path creates a Custom field.
|
||||
*
|
||||
* @apiPermission staff2
|
||||
*
|
||||
* @apiParam {Number} name Name of the custom field.
|
||||
* @apiParam {String} type One of 'text' and 'select'.
|
||||
* @apiParam {String} options JSON array of strings with the option names.
|
||||
*
|
||||
* @apiUse NO_PERMISSION
|
||||
* @apiUse INVALID_NAME
|
||||
* @apiUse INVALID_CUSTOM_FIELD_TYPE
|
||||
* @apiUse INVALID_CUSTOM_FIELD_OPTIONS
|
||||
* @apiUse CUSTOM_FIELD_ALREADY_EXISTS
|
||||
*
|
||||
* @apiSuccess {Object} data Empty object
|
||||
*
|
||||
*/
|
||||
|
||||
class AddCustomFieldController extends Controller {
|
||||
const PATH = '/add-custom-field';
|
||||
const METHOD = 'POST';
|
||||
|
||||
public function validations() {
|
||||
return [
|
||||
'permission' => 'staff_2',
|
||||
'requestData' => [
|
||||
'name' => [
|
||||
'validation' => DataValidator::length(2, 100),
|
||||
'error' => ERRORS::INVALID_NAME
|
||||
],
|
||||
'type' => [
|
||||
'validation' => DataValidator::oneOf(
|
||||
DataValidator::equals('text'),
|
||||
DataValidator::equals('select')
|
||||
),
|
||||
'error' => ERRORS::INVALID_CUSTOM_FIELD_TYPE
|
||||
],
|
||||
'options' => [
|
||||
'validation' => DataValidator::oneOf(
|
||||
DataValidator::json(),
|
||||
DataValidator::nullType()
|
||||
),
|
||||
'error' => ERRORS::INVALID_CUSTOM_FIELD_OPTIONS
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function handler() {
|
||||
$name = Controller::request('name');
|
||||
$type = Controller::request('type');
|
||||
$description = Controller::request('description');
|
||||
$options = Controller::request('options');
|
||||
|
||||
if(!Customfield::getDataStore($name, 'name')->isNull())
|
||||
throw new Exception(ERRORS::CUSTOM_FIELD_ALREADY_EXISTS);
|
||||
|
||||
$customField = new Customfield();
|
||||
$customField->setProperties([
|
||||
'name' => $name,
|
||||
'type' => $type,
|
||||
'description' => $description,
|
||||
'ownCustomfieldoptionList' => $this->getOptionList($options)
|
||||
]);
|
||||
|
||||
$customField->store();
|
||||
|
||||
Response::respondSuccess();
|
||||
}
|
||||
|
||||
public function getOptionList($optionNames) {
|
||||
$options = new DataStoreList();
|
||||
if(!$optionNames) return $options;
|
||||
|
||||
$optionNames = json_decode($optionNames);
|
||||
|
||||
foreach($optionNames as $optionName) {
|
||||
$option = new Customfieldoption();
|
||||
$option->setProperties([
|
||||
'name' => $optionName,
|
||||
]);
|
||||
$options->add($option);
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
use Respect\Validation\Validator as DataValidator;
|
||||
|
||||
/**
|
||||
* @api {post} /system/delete-custom-field Delete custom field
|
||||
* @apiVersion 4.4.0
|
||||
*
|
||||
* @apiName Delete a custom field
|
||||
*
|
||||
* @apiGroup System
|
||||
*
|
||||
* @apiDescription This path deletes a custom field and all its uses.
|
||||
*
|
||||
* @apiPermission staff2
|
||||
*
|
||||
* @apiParam {Number} id Id of the custom field to delete.
|
||||
*
|
||||
* @apiUse NO_PERMISSION
|
||||
* @apiUse INVALID_CUSTOM_FIELD
|
||||
*
|
||||
* @apiSuccess {Object} data Empty object
|
||||
*
|
||||
*/
|
||||
|
||||
class DeleteCustomFieldController extends Controller {
|
||||
const PATH = '/delete-custom-field';
|
||||
const METHOD = 'POST';
|
||||
|
||||
public function validations() {
|
||||
return [
|
||||
'permission' => 'staff_2',
|
||||
'requestData' => [
|
||||
'id' => [
|
||||
'validation' => DataValidator::dataStoreId('customfield'),
|
||||
'error' => ERRORS::INVALID_CUSTOM_FIELD,
|
||||
],
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function handler() {
|
||||
$customField = Customfield::getDataStore(Controller::request('id'));
|
||||
|
||||
foreach(Users::getAll() as $user) {
|
||||
$customFieldValueList = $user->xownCustomfieldvalueList || [];
|
||||
|
||||
foreach($customFieldValueList as $customFieldValue) {
|
||||
if($customFieldValue->customfield->id == $customField->id) {
|
||||
$user->xownCustomfieldvalueList->remove($customFieldValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$customField->delete();
|
||||
}
|
||||
}
|
|
@ -42,6 +42,7 @@ class EditSettingsController extends Controller {
|
|||
'imap-host',
|
||||
'imap-user',
|
||||
'imap-pass',
|
||||
'imap-token',
|
||||
'smtp-host',
|
||||
'smtp-user',
|
||||
'smtp-pass',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
use Respect\Validation\Validator as DataValidator;
|
||||
|
||||
class EmailPolling extends Controller {
|
||||
class EmailPollingController extends Controller {
|
||||
const PATH = '/email-polling';
|
||||
const METHOD = 'POST';
|
||||
|
||||
|
@ -9,7 +10,12 @@ class EmailPolling extends Controller {
|
|||
public function validations() {
|
||||
return [
|
||||
'permission' => 'any',
|
||||
'requestData' => []
|
||||
'requestData' => [
|
||||
'token' => [
|
||||
'validation' => DataValidator::length(1, 200),
|
||||
'error' => ERRORS::INVALID_TOKEN
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -19,6 +25,10 @@ class EmailPolling extends Controller {
|
|||
$defaultLanguage = Setting::getSetting('language')->getValue();
|
||||
$defaultDepartmentId = Department::getAll()->first()->id;
|
||||
|
||||
|
||||
if(Controller::request('token') !== Setting::getSetting('imap-token')->getValue())
|
||||
throw new RequestException(ERRORS::INVALID_TOKEN);
|
||||
|
||||
if(Controller::isUserSystemEnabled())
|
||||
throw new RequestException(ERRORS::USER_SYSTEM);
|
||||
|
||||
|
@ -26,12 +36,12 @@ class EmailPolling extends Controller {
|
|||
Setting::getSetting('imap-host')->getValue(),
|
||||
Setting::getSetting('imap-user')->getValue(),
|
||||
Setting::getSetting('imap-pass')->getValue(),
|
||||
__DIR__
|
||||
'files/'
|
||||
);
|
||||
|
||||
$errors = [];
|
||||
$emails = $this->getLastEmails();
|
||||
|
||||
/*
|
||||
$session = Session::getInstance();
|
||||
$oldSession = [
|
||||
'userId' => $session->getUserId(),
|
||||
|
@ -61,6 +71,17 @@ class EmailPolling extends Controller {
|
|||
return null;
|
||||
});
|
||||
|
||||
if($email->getAttachement()) {
|
||||
$attachment = $email->getAttachement();
|
||||
$_FILES['file'] = [
|
||||
'name' => $attachment->name,
|
||||
'type' => mime_content_type($attachment->filePath),
|
||||
'tmp_name' => $attachment->filePath,
|
||||
'error' => UPLOAD_ERR_OK,
|
||||
'size' => filesize($attachment->filePath),
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
if($email->isReply()) {
|
||||
if($email->getTicket()->authorToArray()['email'] === $email->getSender()) {
|
||||
|
@ -79,6 +100,8 @@ class EmailPolling extends Controller {
|
|||
'error' => $e->__toString(),
|
||||
];
|
||||
}
|
||||
|
||||
unset($_FILES['file']);
|
||||
}
|
||||
|
||||
$session->clearSessionData();
|
||||
|
@ -90,7 +113,7 @@ class EmailPolling extends Controller {
|
|||
Response::respondError(ERRORS::EMAIL_POLLING, null, $errors);
|
||||
} else {
|
||||
Response::respondSuccess();
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
public function getLastEmails() {
|
||||
|
@ -101,12 +124,14 @@ class EmailPolling extends Controller {
|
|||
foreach($mailsIds as $mailId) {
|
||||
$mail = $this->mailbox->getMail($mailId);
|
||||
$mailHeader = $this->mailbox->getMailHeader($mailId);
|
||||
$mailAttachment = count($mail->getAttachments()) ? $mail->getAttachments()[0] : null;
|
||||
|
||||
$emails[] = new Email([
|
||||
'fromAddress' => $mailHeader->fromAddress,
|
||||
'fromName' => $mailHeader->fromName,
|
||||
'subject' => $mailHeader->subject,
|
||||
'content' => $mail->textPlain,
|
||||
'file' => null,
|
||||
'file' => $mailAttachment,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
use Respect\Validation\Validator as DataValidator;
|
||||
|
||||
/**
|
||||
* @api {post} /system/get-custom-fields Get custom fields
|
||||
* @apiVersion 4.4.0
|
||||
*
|
||||
* @apiName Get all Custom field items
|
||||
*
|
||||
* @apiGroup System
|
||||
*
|
||||
* @apiDescription This path retrieves the all CustomField items.
|
||||
*
|
||||
* @apiPermission any
|
||||
*
|
||||
* @apiUse NO_PERMISSION
|
||||
*
|
||||
* @apiSuccess {[Customfield](#api-Data_Structures-ObjectCustomfield)[]} data Array of Customfield
|
||||
*
|
||||
*/
|
||||
|
||||
class GetCustomFieldsController extends Controller {
|
||||
const PATH = '/get-custom-fields';
|
||||
const METHOD = 'POST';
|
||||
|
||||
public function validations() {
|
||||
return [
|
||||
'permission' => 'any',
|
||||
'requestData' => []
|
||||
];
|
||||
}
|
||||
|
||||
public function handler() {
|
||||
$customFieldList = Customfield::getAll();
|
||||
|
||||
Response::respondSuccess($customFieldList->toArray());
|
||||
}
|
||||
}
|
|
@ -50,6 +50,7 @@ class GetSettingsController extends Controller {
|
|||
'smtp-user' => Setting::getSetting('smtp-user')->getValue(),
|
||||
'imap-host' => Setting::getSetting('imap-host')->getValue(),
|
||||
'imap-user' => Setting::getSetting('imap-user')->getValue(),
|
||||
'imap-token' => Setting::getSetting('imap-token')->getValue(),
|
||||
'registration' => Setting::getSetting('registration')->getValue(),
|
||||
'departments' => Department::getAllDepartmentNames(),
|
||||
'supportedLanguages' => Language::getSupportedLanguages(),
|
||||
|
|
|
@ -88,7 +88,8 @@ class InitSettingsController extends Controller {
|
|||
'ticket-gap' => Hashing::generateRandomPrime(100000, 999999),
|
||||
'ticket-first-number' => Hashing::generateRandomNumber(100000, 999999),
|
||||
'session-prefix' => 'opensupports-'.Hashing::generateRandomToken().'_',
|
||||
'mail-template-header-image' => 'https://s3.amazonaws.com/opensupports/logo.png'
|
||||
'mail-template-header-image' => 'https://s3.amazonaws.com/opensupports/logo.png',
|
||||
'imap-token' => '',
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ class CommentController extends Controller {
|
|||
public function handler() {
|
||||
$this->requestData();
|
||||
$ticketAuthor = $this->ticket->authorToArray();
|
||||
$isAuthor = $this->ticket->isAuthor(Controller::getLoggedUser());
|
||||
$isAuthor = $this->ticket->isAuthor(Controller::getLoggedUser()) || Session::getInstance()->isTicketSession();
|
||||
$isOwner = $this->ticket->isOwner(Controller::getLoggedUser());
|
||||
|
||||
if((Controller::isUserSystemEnabled() || Controller::isStaffLogged()) && !$isOwner && !$isAuthor) {
|
||||
|
@ -89,13 +89,13 @@ class CommentController extends Controller {
|
|||
$this->storeComment();
|
||||
|
||||
if($isAuthor && $this->ticket->owner) {
|
||||
$this->sendMail([
|
||||
'email' => $this->ticket->owner->email,
|
||||
'name' => $this->ticket->owner->name,
|
||||
'staff' => true
|
||||
]);
|
||||
} else {
|
||||
$this->sendMail($ticketAuthor);
|
||||
$this->sendMail([
|
||||
'email' => $this->ticket->owner->email,
|
||||
'name' => $this->ticket->owner->name,
|
||||
'staff' => true
|
||||
]);
|
||||
} else if($isOwner) {
|
||||
$this->sendMail($ticketAuthor);
|
||||
}
|
||||
|
||||
Log::createLog('COMMENT', $this->ticket->ticketNumber);
|
||||
|
|
|
@ -20,4 +20,6 @@ $userControllers->addController(new ListBanUserController);
|
|||
$userControllers->addController(new VerifyController);
|
||||
$userControllers->addController(new EnableUserController);
|
||||
$userControllers->addController(new DisableUserController);
|
||||
$userControllers->addController(new EditCustomFieldsController);
|
||||
|
||||
$userControllers->finalize();
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
use Respect\Validation\Validator as DataValidator;
|
||||
|
||||
/**
|
||||
* @api {post} /user/edit-custom-fields Edit custom field values
|
||||
* @apiVersion 4.4.0
|
||||
*
|
||||
* @apiName Edit custom field values
|
||||
*
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiDescription This path is for editing the custom fields of a user.
|
||||
*
|
||||
* @apiPermission user
|
||||
*
|
||||
* @apiParam {String} userId Id of the user if it is not the one logged. Optional.
|
||||
* @apiParam {String} customfield_ Custom field values for this user.
|
||||
*
|
||||
* @apiUse NO_PERMISSION
|
||||
* @apiUse INVALID_CUSTOM_FIELD_OPTION
|
||||
*
|
||||
* @apiSuccess {Object} data Empty object
|
||||
*
|
||||
*/
|
||||
|
||||
class EditCustomFieldsController extends Controller {
|
||||
const PATH = '/edit-custom-fields';
|
||||
const METHOD = 'POST';
|
||||
|
||||
public function validations() {
|
||||
return [
|
||||
'permission' => 'user',
|
||||
'requestData' => []
|
||||
];
|
||||
}
|
||||
|
||||
public function handler() {
|
||||
$userId = Controller::request('userId') * 1;
|
||||
$user = Controller::getLoggedUser();
|
||||
|
||||
if($userId && Controller::isStaffLogged(2)) {
|
||||
$user = User::getDataStore($userId);
|
||||
|
||||
if($user->isNull())
|
||||
throw new RequestException(ERRORS::INVALID_USER);
|
||||
}
|
||||
|
||||
$user->setProperties([
|
||||
'xownCustomfieldvalueList' => $this->getCustomFieldValues()
|
||||
]);
|
||||
|
||||
$user->store();
|
||||
Response::respondSuccess();
|
||||
}
|
||||
}
|
|
@ -66,9 +66,10 @@ class GetUserByIdController extends Controller {
|
|||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'signupDate' => $user->signupDate,
|
||||
'tickets' => $tickets->toArray(),
|
||||
'tickets' => $tickets->toArray(true),
|
||||
'verified' => !$user->verificationToken,
|
||||
'disabled' => !!$user->disabled
|
||||
'disabled' => !!$user->disabled,
|
||||
'customfields' => $user->xownCustomfieldvalueList->toArray(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,7 +55,8 @@ class GetUserController extends Controller {
|
|||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'verified' => !$user->verificationToken,
|
||||
'tickets' => $parsedTicketList
|
||||
'tickets' => $parsedTicketList,
|
||||
'customfields' => $user->xownCustomfieldvalueList->toArray(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ DataValidator::with('CustomValidations', true);
|
|||
* @apiParam {String} email The email of the new user.
|
||||
* @apiParam {String} password The password of the new user.
|
||||
* @apiParam {String} apiKey APIKey to sign up an user if the user system is disabled.
|
||||
* @apiParam {String} customfield_ Custom field values for this user.
|
||||
*
|
||||
* @apiUse INVALID_NAME
|
||||
* @apiUse INVALID_EMAIL
|
||||
|
@ -28,6 +29,7 @@ DataValidator::with('CustomValidations', true);
|
|||
* @apiUse USER_EXISTS
|
||||
* @apiUse ALREADY_BANNED
|
||||
* @apiUse NO_PERMISSION
|
||||
* @apiUse INVALID_CUSTOM_FIELD_OPTION
|
||||
*
|
||||
* @apiSuccess {Object} data Information about created user
|
||||
* @apiSuccess {Number} data.userId Id of the new user
|
||||
|
@ -131,7 +133,8 @@ class SignUpController extends Controller {
|
|||
'tickets' => 0,
|
||||
'email' => $this->userEmail,
|
||||
'password' => Hashing::hashPassword($this->userPassword),
|
||||
'verificationToken' => (MailSender::getInstance()->isConnected()) ? $this->verificationToken : null
|
||||
'verificationToken' => (MailSender::getInstance()->isConnected()) ? $this->verificationToken : null,
|
||||
'xownCustomfieldvalueList' => $this->getCustomFieldValues()
|
||||
]);
|
||||
|
||||
return $userInstance->store();
|
||||
|
|
|
@ -209,7 +209,31 @@
|
|||
*/
|
||||
/**
|
||||
* @apiDefine EMAIL_POLLING
|
||||
* @apiError {String} EMAIL_POLLING Email polling
|
||||
* @apiError {String} EMAIL_POLLING Email polling was unsuccesful
|
||||
*/
|
||||
/**
|
||||
* @apiDefine IMAP_CONNECTION
|
||||
* @apiError {String} IMAP_CONNECTION Imap connection was unsuccesfull
|
||||
*/
|
||||
/**
|
||||
* @apiDefine CUSTOM_FIELD_ALREADY_EXISTS
|
||||
* @apiError {String} CUSTOM_FIELD_ALREADY_EXISTS Custom field already exists
|
||||
*/
|
||||
/**
|
||||
* @apiDefine INVALID_CUSTOM_FIELD
|
||||
* @apiError {String} INVALID_CUSTOM_FIELD Custom field id is invalid
|
||||
*/
|
||||
/**
|
||||
* @apiDefine INVALID_CUSTOM_FIELD_TYPE
|
||||
* @apiError {String} INVALID_CUSTOM_FIELD_TYPE The type is invalid
|
||||
*/
|
||||
/**
|
||||
* @apiDefine INVALID_CUSTOM_FIELD_OPTIONS
|
||||
* @apiError {String} INVALID_CUSTOM_FIELD_OPTIONS Options are not a json array
|
||||
*/
|
||||
/**
|
||||
* @apiDefine INVALID_CUSTOM_FIELD_OPTION
|
||||
* @apiError {String} INVALID_CUSTOM_FIELD_OPTION Option is not in the list of possibles
|
||||
*/
|
||||
|
||||
class ERRORS {
|
||||
|
@ -268,4 +292,9 @@ class ERRORS {
|
|||
const INVALID_TEXT_3 = 'INVALID_TEXT_3';
|
||||
const DEPARTMENT_PRIVATE_TICKETS = 'DEPARTMENT_PRIVATE_TICKETS';
|
||||
const EMAIL_POLLING = 'EMAIL_POLLING';
|
||||
const CUSTOM_FIELD_ALREADY_EXISTS = 'CUSTOM_FIELD_ALREADY_EXISTS';
|
||||
const INVALID_CUSTOM_FIELD = 'INVALID_CUSTOM_FIELD';
|
||||
const INVALID_CUSTOM_FIELD_TYPE = 'INVALID_CUSTOM_FIELD_TYPE';
|
||||
const INVALID_CUSTOM_FIELD_OPTIONS = 'INVALID_CUSTOM_FIELD_OPTIONS';
|
||||
const INVALID_CUSTOM_FIELD_OPTION = 'INVALID_CUSTOM_FIELD_OPTION';
|
||||
}
|
||||
|
|
|
@ -149,4 +149,42 @@ abstract class Controller {
|
|||
public static function isUserSystemEnabled() {
|
||||
return Setting::getSetting('user-system-enabled')->getValue();
|
||||
}
|
||||
|
||||
public static function getCustomFieldValues() {
|
||||
$customFields = Customfield::getAll();
|
||||
$customFieldValues = new DataStoreList();
|
||||
|
||||
foreach($customFields as $customField) {
|
||||
$value = Controller::request('customfield_' . $customField->name);
|
||||
if($value !== null) {
|
||||
$customFieldValue = new Customfieldvalue();
|
||||
$customFieldValue->setProperties([
|
||||
'customfield' => $customField,
|
||||
]);
|
||||
|
||||
if($customField->type == 'select') {
|
||||
$ok = false;
|
||||
foreach($customField->ownCustomfieldoptionList as $option) {
|
||||
if($option->name == $value) {
|
||||
$customFieldValue->setProperties([
|
||||
'customfieldoption' => $option,
|
||||
'value' => $option->name,
|
||||
]);
|
||||
$ok = true;
|
||||
}
|
||||
}
|
||||
if(!$ok)
|
||||
throw new RequestException(ERRORS::INVALID_CUSTOM_FIELD_OPTION);
|
||||
} else {
|
||||
$customFieldValue->setProperties([
|
||||
'value' => $value,
|
||||
]);
|
||||
}
|
||||
|
||||
$customFieldValues->add($customFieldValue);
|
||||
}
|
||||
}
|
||||
|
||||
return $customFieldValues;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
use EmailReplyParser\Parser\EmailParser;
|
||||
|
||||
class Email {
|
||||
private $sender;
|
||||
|
@ -49,7 +50,9 @@ class Email {
|
|||
}
|
||||
|
||||
private function parseContent($data) {
|
||||
return $data['content'];
|
||||
$emailParser = new EmailParser();
|
||||
|
||||
return $emailParser->parse($data['content'])->getVisibleText();
|
||||
}
|
||||
|
||||
private function parseAttachment($data) {
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
/**
|
||||
* @api {OBJECT} Customfield Customfield
|
||||
* @apiVersion 4.4.0
|
||||
* @apiGroup Data Structures
|
||||
* @apiParam {Number} id Id of the custom filed.
|
||||
* @apiParam {String} name Name of the custom filed.
|
||||
* @apiParam {String} description Description of the custom field,
|
||||
* @apiParam {String} Type Type of custom field (select or text)
|
||||
* @apiParam {Customfieldoption[]} options List of possible values if it is select
|
||||
*/
|
||||
|
||||
class Customfield extends DataStore {
|
||||
const TABLE = 'customfield';
|
||||
|
||||
public static function getProps() {
|
||||
return [
|
||||
'name',
|
||||
'description',
|
||||
'type',
|
||||
'ownCustomfieldoptionList'
|
||||
];
|
||||
}
|
||||
|
||||
public function toArray() {
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'type' => $this->type,
|
||||
'options' => $this->ownCustomfieldoptionList->toArray()
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
/**
|
||||
* @api {OBJECT} Customfieldoption Customfieldoption
|
||||
* @apiVersion 4.4.0
|
||||
* @apiGroup Data Structures
|
||||
* @apiParam {Number} id Id of the option.
|
||||
* @apiParam {String} name Name of the option.
|
||||
*/
|
||||
|
||||
class Customfieldoption extends DataStore {
|
||||
const TABLE = 'customfieldoption';
|
||||
|
||||
public static function getProps() {
|
||||
return [
|
||||
'name'
|
||||
];
|
||||
}
|
||||
|
||||
public function toArray() {
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
/**
|
||||
* @api {OBJECT} Customfieldvalue Customfieldvalue
|
||||
* @apiVersion 4.4.0
|
||||
* @apiGroup Data Structures
|
||||
* @apiParam {Number} id Id of the value.
|
||||
* @apiParam {Customfield} customfield Customfield of the value.
|
||||
* @apiParam {String} value Content of the value..
|
||||
*/
|
||||
|
||||
class Customfieldvalue extends DataStore {
|
||||
const TABLE = 'customfieldvalue';
|
||||
|
||||
public static function getProps() {
|
||||
return [
|
||||
'customfield',
|
||||
'value',
|
||||
'customfieldoption'
|
||||
];
|
||||
}
|
||||
|
||||
public function toArray() {
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'customfield' => $this->customfield->name,
|
||||
'value' => $this->value,
|
||||
'customfieldoption' => $this->customfieldoption ? $this->customfieldoption->toArray() : null,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -173,6 +173,7 @@ abstract class DataStore {
|
|||
|
||||
$listType = str_replace('List', '', $listType);
|
||||
$listType = str_replace('shared', '', $listType);
|
||||
$listType = str_replace('xown', '', $listType);
|
||||
$listType = str_replace('own', '', $listType);
|
||||
|
||||
return $listType;
|
||||
|
|
|
@ -41,6 +41,10 @@ class Session {
|
|||
$this->store('token', Hashing::generateRandomToken());
|
||||
}
|
||||
|
||||
public function isTicketSession() {
|
||||
return $this->getStoredData('ticketNumber') && $this->getStoredData('token');
|
||||
}
|
||||
|
||||
public function getTicketNumber() {
|
||||
return $this->getStoredData('ticketNumber');
|
||||
}
|
||||
|
|
|
@ -141,7 +141,8 @@ class Ticket extends DataStore {
|
|||
'name' => $author->name,
|
||||
'staff' => $author instanceof Staff,
|
||||
'profilePic' => ($author instanceof Staff) ? $author->profilePic : null,
|
||||
'email' => $author->email
|
||||
'email' => $author->email,
|
||||
'customfields' => $author->xownCustomfieldvalueList ? $author->xownCustomfieldvalueList->toArray() : [],
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
|
|
|
@ -85,7 +85,8 @@ class Ticketevent extends DataStore {
|
|||
'author' => [
|
||||
'name' => $user ? $user->name : null,
|
||||
'staff' => $user instanceOf Staff,
|
||||
'id' => $user ? $user->id : null
|
||||
'id' => $user ? $user->id : null,
|
||||
'customfields' => $user->xownCustomfieldvalueList ? $user->xownCustomfieldvalueList->toArray() : [],
|
||||
]
|
||||
];
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ use RedBeanPHP\Facade as RedBean;
|
|||
* @apiParam {Number} id The id of the user.
|
||||
* @apiParam {String} name The name of the user.
|
||||
* @apiParam {Boolean} verified Indicates if the user has verified the email.
|
||||
* @apiParam {[CustomField](#api-Data_Structures-ObjectCustomfield)[]} customfields Indicates the values for custom fields.
|
||||
*/
|
||||
|
||||
class User extends DataStore {
|
||||
|
@ -29,7 +30,8 @@ class User extends DataStore {
|
|||
'tickets',
|
||||
'sharedTicketList',
|
||||
'verificationToken',
|
||||
'disabled'
|
||||
'disabled',
|
||||
'xownCustomfieldvalueList'
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -47,7 +49,8 @@ class User extends DataStore {
|
|||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'verified' => !$this->verificationToken,
|
||||
'disabled' => $this->disabled
|
||||
'disabled' => $this->disabled,
|
||||
'customfields' => $this->xownCustomfieldvalueList->toArray(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue