From 623a81b51dad60f9ea0c9206b5a489c2fda4614b Mon Sep 17 00:00:00 2001 From: Ivan Diaz Date: Sun, 3 Feb 2019 16:47:29 -0300 Subject: [PATCH] Add Custom Fields feature --- client/src/app-components/ticket-event.js | 13 +- client/src/app-components/ticket-event.scss | 6 +- client/src/app/Routes.js | 2 + .../src/app/admin/panel/admin-panel-menu.js | 5 + .../users/admin-panel-custom-field-form.js | 128 ++++++++++++++++++ .../users/admin-panel-custom-field-form.scss | 30 ++++ .../panel/users/admin-panel-custom-fields.js | 108 +++++++++++++++ .../users/admin-panel-custom-fields.scss | 11 ++ .../panel/users/admin-panel-view-user.js | 14 ++ .../dashboard-edit-profile-page.js | 117 +++++++++++++++- .../dashboard-edit-profile-page.scss | 11 +- .../main/main-signup/main-signup-widget.js | 56 +++++++- client/src/data/languages/de.js | 2 +- client/src/data/languages/en.js | 12 ++ client/src/reducers/session-reducer.js | 6 +- server/controllers/staff/get.php | 2 +- server/controllers/system.php | 3 + .../controllers/system/add-custom-field.php | 98 ++++++++++++++ .../system/delete-custom-field.php | 56 ++++++++ .../controllers/system/get-custom-fields.php | 38 ++++++ server/controllers/user.php | 2 + .../controllers/user/edit-custom-fields.php | 55 ++++++++ server/controllers/user/get-user.php | 5 +- server/controllers/user/get.php | 3 +- server/controllers/user/signup.php | 5 +- server/data/ERRORS.php | 31 ++++- server/libs/Controller.php | 38 ++++++ server/models/CustomField.php | 34 +++++ server/models/CustomFieldOption.php | 25 ++++ server/models/CustomFieldValue.php | 30 ++++ server/models/DataStore.php | 1 + server/models/Ticket.php | 3 +- server/models/Ticketevent.php | 3 +- server/models/User.php | 7 +- 34 files changed, 933 insertions(+), 27 deletions(-) create mode 100644 client/src/app/admin/panel/users/admin-panel-custom-field-form.js create mode 100644 client/src/app/admin/panel/users/admin-panel-custom-field-form.scss create mode 100644 client/src/app/admin/panel/users/admin-panel-custom-fields.js create mode 100644 client/src/app/admin/panel/users/admin-panel-custom-fields.scss create mode 100644 server/controllers/system/add-custom-field.php create mode 100644 server/controllers/system/delete-custom-field.php create mode 100644 server/controllers/system/get-custom-fields.php create mode 100644 server/controllers/user/edit-custom-fields.php create mode 100644 server/models/CustomField.php create mode 100644 server/models/CustomFieldOption.php create mode 100644 server/models/CustomFieldValue.php diff --git a/client/src/app-components/ticket-event.js b/client/src/app-components/ticket-event.js index a284ba00..ebab5f20 100644 --- a/client/src/app-components/ticket-event.js +++ b/client/src/app-components/ticket-event.js @@ -88,6 +88,7 @@ class TicketEvent extends React.Component { {i18n((this.props.author.staff) ? 'STAFF' : 'CUSTOMER')} + {this.props.author.customfields.map(this.renderCustomFieldValue.bind(this))} {(this.props.private*1) ? this.renderPrivateBadge() : null}
{DateTransformer.transformToString(this.props.date)}
@@ -198,7 +199,17 @@ class TicketEvent extends React.Component {
{node}
- ) + ); + } + + renderCustomFieldValue(customField) { + return ( + + + {customField.customfield}: {customField.value} + + + ); } getClass() { diff --git a/client/src/app-components/ticket-event.scss b/client/src/app-components/ticket-event.scss index 8c59d3d9..26ccbf89 100644 --- a/client/src/app-components/ticket-event.scss +++ b/client/src/app-components/ticket-event.scss @@ -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; diff --git a/client/src/app/Routes.js b/client/src/app/Routes.js index 84b17ded..849c780d 100644 --- a/client/src/app/Routes.js +++ b/client/src/app/Routes.js @@ -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 ( + diff --git a/client/src/app/admin/panel/admin-panel-menu.js b/client/src/app/admin/panel/admin-panel-menu.js index 50fcaf88..5481538c 100644 --- a/client/src/app/admin/panel/admin-panel-menu.js +++ b/client/src/app/admin/panel/admin-panel-menu.js @@ -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 } ]) }, diff --git a/client/src/app/admin/panel/users/admin-panel-custom-field-form.js b/client/src/app/admin/panel/users/admin-panel-custom-field-form.js new file mode 100644 index 00000000..5f582b04 --- /dev/null +++ b/client/src/app/admin/panel/users/admin-panel-custom-field-form.js @@ -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 ( +
+
+
+
+ + + + {this.state.addForm.type ? this.renderOptionFormFields() : null} + {this.state.error ? this.renderErrorMessage() : null} +
+ {i18n('SUBMIT')} + +
+ +
+
+ ); + } + + renderErrorMessage() { + return ( + + {this.state.error} + + ); + } + + renderOptionFormFields() { + return ( +
+
{i18n('OPTIONS')}
+ {this.state.addFormOptions.map(this.renderFormOption.bind(this))} +
+ ); + } + + renderFormOption(option, index) { + return ( +
+ +
+ ); + } + + 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; diff --git a/client/src/app/admin/panel/users/admin-panel-custom-field-form.scss b/client/src/app/admin/panel/users/admin-panel-custom-field-form.scss new file mode 100644 index 00000000..b3d2916d --- /dev/null +++ b/client/src/app/admin/panel/users/admin-panel-custom-field-form.scss @@ -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; + } +} diff --git a/client/src/app/admin/panel/users/admin-panel-custom-fields.js b/client/src/app/admin/panel/users/admin-panel-custom-fields.js new file mode 100644 index 00000000..0b342289 --- /dev/null +++ b/client/src/app/admin/panel/users/admin-panel-custom-fields.js @@ -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 ( +
+
+ {this.renderCustomFieldList()} +
+ +
+
+ ); + } + + renderCustomFieldList() { + return ( + + ); + } + + getCustomField(customField, index) { + const {id, description, name, type, options} = customField; + let descriptionInfoTooltip = null; + + if(description) { + descriptionInfoTooltip = ; + } + + return { + name:
{name} {descriptionInfoTooltip}
, + type, + options: JSON.stringify(options.map(option => option.name)), + actions: , + } + } + + onNewCustomFieldClick() { + ModalContainer.openModal( + { + 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; diff --git a/client/src/app/admin/panel/users/admin-panel-custom-fields.scss b/client/src/app/admin/panel/users/admin-panel-custom-fields.scss new file mode 100644 index 00000000..bcd85ce0 --- /dev/null +++ b/client/src/app/admin/panel/users/admin-panel-custom-fields.scss @@ -0,0 +1,11 @@ +.admin-panel-custom-fields { + + &__list { + text-align: left; + } + + &__add-button { + text-align: left; + margin-top: 14px; + } +} diff --git a/client/src/app/admin/panel/users/admin-panel-view-user.js b/client/src/app/admin/panel/users/admin-panel-view-user.js index b713923b..6a843bec 100644 --- a/client/src/app/admin/panel/users/admin-panel-view-user.js +++ b/client/src/app/admin/panel/users/admin-panel-view-user.js @@ -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} + {this.state.customfields.map(this.renderCustomField.bind(this))}
+ ); + } + 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 }); } diff --git a/client/src/app/main/dashboard/dashboard-edit-profile/dashboard-edit-profile-page.js b/client/src/app/main/dashboard/dashboard-edit-profile/dashboard-edit-profile-page.js index d07e5191..6bf28bfb 100644 --- a/client/src/app/main/dashboard/dashboard-edit-profile/dashboard-edit-profile-page.js +++ b/client/src/app/main/dashboard/dashboard-edit-profile/dashboard-edit-profile-page.js @@ -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 (
+
{i18n('ADDITIONAL_FIELDS')}
+
this.setState({customFieldsFrom: form})} onSubmit={this.onCustomFieldsSubmit.bind(this)}> +
+ {this.state.customFields.map(this.renderCustomField.bind(this))} +
+
+ {i18n('SAVE')} +
+
{i18n('EDIT_EMAIL')}
@@ -41,6 +68,25 @@ class DashboardEditProfilePage extends React.Component {
); } + + renderCustomField(customField, key) { + if(customField.type === 'text') { + return ( +
+ +
+ ); + } else { + const items = customField.options.map(option => ({content: option.name, value: option.name})); + + return ( +
+ +
+ ); + } + } + 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); diff --git a/client/src/app/main/dashboard/dashboard-edit-profile/dashboard-edit-profile-page.scss b/client/src/app/main/dashboard/dashboard-edit-profile/dashboard-edit-profile-page.scss index 8e25de49..2e34fc98 100644 --- a/client/src/app/main/dashboard/dashboard-edit-profile/dashboard-edit-profile-page.scss +++ b/client/src/app/main/dashboard/dashboard-edit-profile/dashboard-edit-profile-page.scss @@ -13,4 +13,13 @@ margin-top: 20px; margin-bottom: 20px; } -} \ No newline at end of file + + &__custom-fields { + text-align: left; + } + + &__custom-field { + display: inline-block; + margin-right: 20px; + } +} diff --git a/client/src/app/main/main-signup/main-signup-widget.js b/client/src/app/main/main-signup/main-signup-widget.js index 0b0ab98f..ce99dc3d 100644 --- a/client/src/app/main/main-signup/main-signup-widget.js +++ b/client/src/app/main/main-signup/main-signup-widget.js @@ -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 { + {this.state.customFields.map(this.renderCustomField.bind(this))}
@@ -51,6 +62,31 @@ class MainSignUpWidget extends React.Component { ); } + renderCustomField(customField, key) { + if(customField.type === 'text') { + return ( + + ); + } else { + const items = customField.options.map(option => ({content: option.name, value: option.name})); + + return ( + + ); + } + } + 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)); } } diff --git a/client/src/data/languages/de.js b/client/src/data/languages/de.js index 9ba1a30c..cb07f007 100644 --- a/client/src/data/languages/de.js +++ b/client/src/data/languages/de.js @@ -61,7 +61,7 @@ export default { 'HIGH': 'Hoch', 'MEDIUM': 'Mittel', 'LOW': 'Niedrig', - 'TITLE': 'Titel', + 'TITLE': 'Betreff', 'CONTENT': 'Inhalt', 'SAVE': 'Speichern', 'DISCARD_CHANGES': 'Ă„nderungen verwerfen', diff --git a/client/src/data/languages/en.js b/client/src/data/languages/en.js index 8879e773..19496747 100644 --- a/client/src/data/languages/en.js +++ b/client/src/data/languages/en.js @@ -198,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', @@ -318,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', @@ -378,6 +389,7 @@ 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', diff --git a/client/src/reducers/session-reducer.js b/client/src/reducers/session-reducer.js index 53ec58ab..4f8104dd 100644 --- a/client/src/reducers/session-reducer.js +++ b/client/src/reducers/session-reducer.js @@ -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 }); } diff --git a/server/controllers/staff/get.php b/server/controllers/staff/get.php index 9506e887..891f8a9a 100755 --- a/server/controllers/staff/get.php +++ b/server/controllers/staff/get.php @@ -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 ]); } diff --git a/server/controllers/system.php b/server/controllers/system.php index a0cb981c..51aca2bc 100755 --- a/server/controllers/system.php +++ b/server/controllers/system.php @@ -32,5 +32,8 @@ $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(); diff --git a/server/controllers/system/add-custom-field.php b/server/controllers/system/add-custom-field.php new file mode 100644 index 00000000..4a6d6233 --- /dev/null +++ b/server/controllers/system/add-custom-field.php @@ -0,0 +1,98 @@ + '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; + } +} diff --git a/server/controllers/system/delete-custom-field.php b/server/controllers/system/delete-custom-field.php new file mode 100644 index 00000000..9049d44e --- /dev/null +++ b/server/controllers/system/delete-custom-field.php @@ -0,0 +1,56 @@ + '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(); + } +} diff --git a/server/controllers/system/get-custom-fields.php b/server/controllers/system/get-custom-fields.php new file mode 100644 index 00000000..01d70c80 --- /dev/null +++ b/server/controllers/system/get-custom-fields.php @@ -0,0 +1,38 @@ + 'any', + 'requestData' => [] + ]; + } + + public function handler() { + $customFieldList = Customfield::getAll(); + + Response::respondSuccess($customFieldList->toArray()); + } +} diff --git a/server/controllers/user.php b/server/controllers/user.php index d65541f7..d5d08020 100755 --- a/server/controllers/user.php +++ b/server/controllers/user.php @@ -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(); diff --git a/server/controllers/user/edit-custom-fields.php b/server/controllers/user/edit-custom-fields.php new file mode 100644 index 00000000..d8eab39e --- /dev/null +++ b/server/controllers/user/edit-custom-fields.php @@ -0,0 +1,55 @@ + '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(); + } +} diff --git a/server/controllers/user/get-user.php b/server/controllers/user/get-user.php index 987456b4..3cc021a1 100755 --- a/server/controllers/user/get-user.php +++ b/server/controllers/user/get-user.php @@ -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(), ]); } } diff --git a/server/controllers/user/get.php b/server/controllers/user/get.php index 37c92e8d..74dc1a16 100755 --- a/server/controllers/user/get.php +++ b/server/controllers/user/get.php @@ -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(), ]); } } diff --git a/server/controllers/user/signup.php b/server/controllers/user/signup.php index 9371e49b..a322baaa 100755 --- a/server/controllers/user/signup.php +++ b/server/controllers/user/signup.php @@ -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(); diff --git a/server/data/ERRORS.php b/server/data/ERRORS.php index 241cd148..198c804a 100755 --- a/server/data/ERRORS.php +++ b/server/data/ERRORS.php @@ -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'; } diff --git a/server/libs/Controller.php b/server/libs/Controller.php index f634c1ba..de16fff4 100755 --- a/server/libs/Controller.php +++ b/server/libs/Controller.php @@ -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; + } } diff --git a/server/models/CustomField.php b/server/models/CustomField.php new file mode 100644 index 00000000..5495bab1 --- /dev/null +++ b/server/models/CustomField.php @@ -0,0 +1,34 @@ + $this->id, + 'name' => $this->name, + 'description' => $this->description, + 'type' => $this->type, + 'options' => $this->ownCustomfieldoptionList->toArray() + ]; + } +} diff --git a/server/models/CustomFieldOption.php b/server/models/CustomFieldOption.php new file mode 100644 index 00000000..5dd60ad1 --- /dev/null +++ b/server/models/CustomFieldOption.php @@ -0,0 +1,25 @@ + $this->id, + 'name' => $this->name + ]; + } +} diff --git a/server/models/CustomFieldValue.php b/server/models/CustomFieldValue.php new file mode 100644 index 00000000..d87ae0ff --- /dev/null +++ b/server/models/CustomFieldValue.php @@ -0,0 +1,30 @@ + $this->id, + 'customfield' => $this->customfield->name, + 'value' => $this->value, + 'customfieldoption' => $this->customfieldoption ? $this->customfieldoption->toArray() : null, + ]; + } +} diff --git a/server/models/DataStore.php b/server/models/DataStore.php index b13f7294..8d458398 100755 --- a/server/models/DataStore.php +++ b/server/models/DataStore.php @@ -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; diff --git a/server/models/Ticket.php b/server/models/Ticket.php index 720e4d0e..bf927c81 100755 --- a/server/models/Ticket.php +++ b/server/models/Ticket.php @@ -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 [ diff --git a/server/models/Ticketevent.php b/server/models/Ticketevent.php index 54fdea72..db48f206 100755 --- a/server/models/Ticketevent.php +++ b/server/models/Ticketevent.php @@ -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() : [], ] ]; } diff --git a/server/models/User.php b/server/models/User.php index 94afa0ad..15ff0787 100755 --- a/server/models/User.php +++ b/server/models/User.php @@ -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(), ]; } }