From 60a078c3db7cd394721e85f01161aaac30b8233c Mon Sep 17 00:00:00 2001 From: OldHawk Date: Mon, 16 Apr 2018 18:20:22 +0800 Subject: [PATCH] feat(tickets): list message tickets and reply, edit, delete --- config/env/default.js | 1 - config/env/torrents.js | 10 +- config/lib/multer.js | 39 ++ gulpfile.js | 109 +++-- modules/core/client/app/trans-string-en.js | 15 +- modules/core/client/app/trans-string-zh.js | 11 +- .../forums-topic.client.controller.js | 2 +- .../controllers/forums.server.controller.js | 1 - .../config/tickets-admin.client.routes.js | 8 + .../message-tickets.client.controller.js | 5 +- .../view-message-tickets.client.controller.js | 447 ++++++++++++++++++ modules/tickets/client/less/tickets.less | 42 ++ .../client/services/tickets.client.service.js | 7 +- .../views/support-message.client.view.html | 7 +- .../support-view-message.client.view.html | 136 ++++++ .../client/views/support.client.view.html | 10 +- .../mailTickets.server.controller.js | 16 + .../messageTickets.server.controller.js | 274 +++++++++-- .../server/policies/tickets.server.policy.js | 5 +- .../server/routes/tickets.server.routes.js | 15 +- 20 files changed, 1058 insertions(+), 102 deletions(-) create mode 100644 modules/tickets/client/controllers/view-message-tickets.client.controller.js create mode 100644 modules/tickets/client/views/support-view-message.client.view.html diff --git a/config/env/default.js b/config/env/default.js index 449b5ace..21a5b148 100644 --- a/config/env/default.js +++ b/config/env/default.js @@ -108,7 +108,6 @@ module.exports = { tickets: { image: { dest: './modules/tickets/client/uploads/image/', - crop: './modules/tickets/client/uploads/image/crop/', temp: './modules/tickets/client/uploads/temp/', limits: { fileSize: 2 * 1024 * 1024 // Max file size in bytes (2 MB) diff --git a/config/env/torrents.js b/config/env/torrents.js index 71390045..8064e86f 100644 --- a/config/env/torrents.js +++ b/config/env/torrents.js @@ -412,8 +412,10 @@ module.exports = { * * @userSignatureLength: user signature of forum string length limit * @chatMessageMaxLength: chat room send message string length limit + * @messageTitleLength: user message send title length limit * @messageBoxContentLength: user message send content length limit * @messageBoxReplyLength: user message send reply content length limit + * @ticketContentLength: ticket content length limit * @torrentCommentLength: torrent comment send content length limit * @forumTopicTitleLength: forum topic title length limit * @forumTopicContentLength: forum topic content length limit @@ -425,8 +427,10 @@ module.exports = { userSignatureLength: 512, chatMessageMaxLength: 512, + messageTitleLength: 128, messageBoxContentLength: 1024, messageBoxReplyLength: 512, + ticketContentLength: 2048, torrentCommentLength: 512, @@ -1143,7 +1147,9 @@ module.exports = { * * @examinationUserListPerPage: users item number of examination result page * @messageTicketsListPerPage: message tickets items number of support list page + * @messageTicketRepliesPerPage: message ticket replies list page settings * @mailTicketsListPerPage: mail tickets items number of support list page + * @mailTicketRepliesPerPage: mail ticket replies list page settings */ itemsPerPage: { topicsPerPage: 10, @@ -1174,7 +1180,9 @@ module.exports = { examinationUserListPerPage: 20, messageTicketsListPerPage: 15, - mailTicketsListPerPage: 15 + messageTicketRepliesPerPage: 10, + mailTicketsListPerPage: 15, + mailTicketRepliesPerPage: 10 }, /** diff --git a/config/lib/multer.js b/config/lib/multer.js index 5227fc10..545ba9ef 100644 --- a/config/lib/multer.js +++ b/config/lib/multer.js @@ -202,3 +202,42 @@ module.exports.createUploadTorrentImageFilename = function (req, file, cb) { module.exports.getUploadTorrentImageDestination = function (req, file, cb) { cb(null, config.uploads.torrent.image.temp); }; + + +module.exports.createUploadTicketImageFilename = function (req, file, cb) { + var RexStr = /\(|\)|\[|\]|\,/g; + var filename = file.originalname.replace(RexStr, function (MatchStr) { + switch (MatchStr) { + case '(': + return '<'; + case ')': + return '>'; + case '[': + return '{'; + case ']': + return '}'; + case ',': + return ' '; + default: + break; + } + }); + + if (fs.existsSync(config.uploads.tickets.image.temp + filename)) { + fs.unlinkSync(config.uploads.tickets.image.temp + filename); + } + + if (fs.existsSync(config.uploads.tickets.image.dest + filename)) { + var ext = file.originalname.replace(/^.+\./, ''); + var regex = new RegExp(ext, 'g'); + filename = filename.replace(regex, Date.now() + '.' + ext); + + cb(null, filename); + } else { + cb(null, filename); + } +}; + +module.exports.getUploadTicketImageDestination = function (req, file, cb) { + cb(null, config.uploads.tickets.image.temp); +}; diff --git a/gulpfile.js b/gulpfile.js index 6fcb7adf..c52a0b29 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -117,8 +117,8 @@ gulp.task('csslint', function () { return gulp.src(defaultAssets.client.css) .pipe(plugins.csslint('.csslintrc')) .pipe(plugins.csslint.formatter()); - // Don't fail CSS issues yet - // .pipe(plugins.csslint.failFormatter()); + // Don't fail CSS issues yet + // .pipe(plugins.csslint.failFormatter()); }); // ESLint JS linting task @@ -187,7 +187,7 @@ gulp.task('imagemin', function () { return gulp.src(defaultAssets.client.img) .pipe(plugins.imagemin({ progressive: true, - svgoPlugins: [{ removeViewBox: false }], + svgoPlugins: [{removeViewBox: false}], use: [pngquant()] })) .pipe(gulp.dest('public/dist/img')); @@ -260,61 +260,66 @@ gulp.task('makeUploadsDir', function () { fs.mkdir('modules/torrents/client/uploads', function (err) { if (err && err.code !== 'EEXIST') { console.error(err); - } - }); - fs.mkdir('modules/torrents/client/uploads/temp', function (err) { - if (err && err.code !== 'EEXIST') { - console.error(err); + } else { + fs.mkdir('modules/torrents/client/uploads/temp', function (err) { + if (err && err.code !== 'EEXIST') { + console.error(err); + } + }); + fs.mkdir('modules/torrents/client/uploads/subtitles', function (err) { + if (err && err.code !== 'EEXIST') { + console.error(err); + } + }); + fs.mkdir('modules/torrents/client/uploads/cover', function (err) { + if (err && err.code !== 'EEXIST') { + console.error(err); + } else { + fs.mkdir('modules/torrents/client/uploads/cover/crop', function (err) { + if (err && err.code !== 'EEXIST') { + console.error(err); + } + }); + } + }); + fs.mkdir('modules/torrents/client/uploads/image', function (err) { + if (err && err.code !== 'EEXIST') { + console.error(err); + } else { + fs.mkdir('modules/torrents/client/uploads/image/crop', function (err) { + if (err && err.code !== 'EEXIST') { + console.error(err); + } + }); + } + }); } }); fs.mkdir('modules/forums/client/attach', function (err) { if (err && err.code !== 'EEXIST') { console.error(err); + } else { + fs.mkdir('modules/forums/client/attach/temp', function (err) { + if (err && err.code !== 'EEXIST') { + console.error(err); + } + }); } }); - fs.mkdir('modules/forums/client/attach/temp', function (err) { - if (err && err.code !== 'EEXIST') { - console.error(err); - } - }); - fs.mkdir('modules/torrents/client/uploads/subtitles', function (err) { - if (err && err.code !== 'EEXIST') { - console.error(err); - } - }); - fs.mkdir('modules/torrents/client/uploads/cover', function (err) { - if (err && err.code !== 'EEXIST') { - console.error(err); - } - }); - fs.mkdir('modules/torrents/client/uploads/cover/crop', function (err) { - if (err && err.code !== 'EEXIST') { - console.error(err); - } - }); - fs.mkdir('modules/torrents/client/uploads/image', function (err) { - if (err && err.code !== 'EEXIST') { - console.error(err); - } - }); - fs.mkdir('modules/torrents/client/uploads/image/crop', function (err) { - if (err && err.code !== 'EEXIST') { - console.error(err); - } - }); - fs.mkdir('modules/tickets/client/uploads/temp', function (err) { - if (err && err.code !== 'EEXIST') { - console.error(err); - } - }); - fs.mkdir('modules/tickets/client/uploads/image', function (err) { - if (err && err.code !== 'EEXIST') { - console.error(err); - } - }); - fs.mkdir('modules/tickets/client/uploads/image/crop', function (err) { + fs.mkdir('modules/tickets/client/uploads', function (err) { if (err && err.code !== 'EEXIST') { console.error(err); + } else { + fs.mkdir('modules/tickets/client/uploads/temp', function (err) { + if (err && err.code !== 'EEXIST') { + console.error(err); + } + }); + fs.mkdir('modules/tickets/client/uploads/image', function (err) { + if (err && err.code !== 'EEXIST') { + console.error(err); + } + }); } }); @@ -372,7 +377,7 @@ gulp.task('pre-test', function () { // Display coverage for all server JavaScript files return gulp.src(defaultAssets.server.allJS) - // Covering files + // Covering files .pipe(plugins.istanbul()) // Force `require` to return covered files .pipe(plugins.istanbul.hookRequire()); @@ -384,7 +389,7 @@ gulp.task('mocha:coverage', ['pre-test', 'mocha'], function () { return gulp.src(testSuites) .pipe(plugins.istanbul.writeReports({ - reportOpts: { dir: './coverage/server' } + reportOpts: {dir: './coverage/server'} })); }); @@ -415,7 +420,7 @@ gulp.task('karma:coverage', function (done) { coverageReporter: { dir: 'coverage/client', reporters: [ - { type: 'lcov', subdir: '.' } + {type: 'lcov', subdir: '.'} // printing summary to console currently weirdly causes gulp to hang so disabled for now // https://github.com/karma-runner/karma-coverage/issues/209 // { type: 'text-summary' } diff --git a/modules/core/client/app/trans-string-en.js b/modules/core/client/app/trans-string-en.js index 05c9ce48..719a3fda 100644 --- a/modules/core/client/app/trans-string-en.js +++ b/modules/core/client/app/trans-string-en.js @@ -1184,8 +1184,8 @@ POST_TOPIC_FAILED: 'Post new topic failed', POST_REPLY_SUCCESSFULLY: 'Post new reply successfully', POST_REPLY_FAILED: 'Post new reply failed', - TOPIC_EDIT_SUCCESSFULLY: 'Reply content modify successfully', - TOPIC_EDIT_FAILED: 'Reply content modify failed', + TOPIC_EDIT_SUCCESSFULLY: 'Topic content modify successfully', + TOPIC_EDIT_FAILED: 'Topic content modify failed', TOPIC_TOGGLE_READONLY_SUCCESSFULLY: 'Toggle set topic readonly successfully', TOPIC_TOGGLE_READONLY_FAILED: 'Toggle set topic readonly failed', TOPIC_SEARCH_FAILED: 'Topics search failed', @@ -1363,7 +1363,16 @@ FIELDS_REPLIES: 'Replies', FIELDS_STATUS: 'Status', FIELDS_CREATEDAT: 'Created At', - FIELDS_UPDATEDAT: 'Updated At' + FIELDS_UPDATEDAT: 'Updated At', + LINK_HANDLE: 'Handle', + TICKET_EDIT_SUCCESSFULLY: 'Ticket content modify successfully', + TICKET_EDIT_FAILED: 'Ticket content modify failed', + DELETE_TICKET_CONFIRM_OK: 'Delete', + DELETE_TICKET_CONFIRM_CANCEL: 'Cancel', + DELETE_TICKET_CONFIRM_HEADER_TEXT: 'Delete Ticket', + DELETE_TICKET_CONFIRM_BODY_TEXT: 'Are you sure want to delete this ticket?', + DELETE_TICKET_SUCCESSFULLY: 'Ticket deleted successfully', + DELETE_TICKET_FAILED: 'Ticket deleted failed' }, /////////////////////////resources tag fields/////////////////////////////////// diff --git a/modules/core/client/app/trans-string-zh.js b/modules/core/client/app/trans-string-zh.js index b791820f..cc3a509a 100644 --- a/modules/core/client/app/trans-string-zh.js +++ b/modules/core/client/app/trans-string-zh.js @@ -1363,7 +1363,16 @@ FIELDS_REPLIES: '回复数', FIELDS_STATUS: '状态', FIELDS_CREATEDAT: '创建日期', - FIELDS_UPDATEDAT: '更新日期' + FIELDS_UPDATEDAT: '更新日期', + LINK_HANDLE: '接手服务', + TICKET_EDIT_SUCCESSFULLY: '客服内容修改成功', + TICKET_EDIT_FAILED: '客服内容修改失败', + DELETE_TICKET_CONFIRM_OK: '删除', + DELETE_TICKET_CONFIRM_CANCEL: '取消', + DELETE_TICKET_CONFIRM_HEADER_TEXT: '删除客服条目', + DELETE_TICKET_CONFIRM_BODY_TEXT: '您确定要删除此客户服务内容条目?', + DELETE_TICKET_SUCCESSFULLY: '客服条目删除成功', + DELETE_TICKET_FAILED: '客服条目删除失败' }, /////////////////////////resources tag fields/////////////////////////////////// diff --git a/modules/forums/client/controllers/forums-topic.client.controller.js b/modules/forums/client/controllers/forums-topic.client.controller.js index 37371a11..6892b9be 100644 --- a/modules/forums/client/controllers/forums-topic.client.controller.js +++ b/modules/forums/client/controllers/forums-topic.client.controller.js @@ -326,7 +326,7 @@ vm.figureOutItemsToDisplay(); NotifycationService.showSuccessNotify('FORUMS.REPLY_EDIT_SUCCESSFULLY'); }, function (res) { - NotifycationService.showErrorNotify(res.data.message, 'FORUMS.FORUMS'); + NotifycationService.showErrorNotify(res.data.message, 'FORUMS.REPLY_EDIT_FAILED'); }); e.$options.hideable = true; diff --git a/modules/forums/server/controllers/forums.server.controller.js b/modules/forums/server/controllers/forums.server.controller.js index fbf7449f..0a6b890e 100644 --- a/modules/forums/server/controllers/forums.server.controller.js +++ b/modules/forums/server/controllers/forums.server.controller.js @@ -979,7 +979,6 @@ exports.updateReply = function (req, res) { }); }; - /** * deleteReply * @param req diff --git a/modules/tickets/client/config/tickets-admin.client.routes.js b/modules/tickets/client/config/tickets-admin.client.routes.js index 98b11be2..04d61bd9 100644 --- a/modules/tickets/client/config/tickets-admin.client.routes.js +++ b/modules/tickets/client/config/tickets-admin.client.routes.js @@ -31,6 +31,14 @@ .state('admin.tickets.support.mail', { url: '/mail', templateUrl: '/modules/tickets/client/views/support-mail.client.view.html' + }) + .state('admin.tickets.support.viewMessage', { + url: '/viewMessage/:messageTicketId', + templateUrl: '/modules/tickets/client/views/support-view-message.client.view.html' + }) + .state('admin.tickets.support.viewMail', { + url: '/viewMail/:mailTicketId', + templateUrl: '/modules/tickets/client/views/support-view-mail.client.view.html' }); } diff --git a/modules/tickets/client/controllers/message-tickets.client.controller.js b/modules/tickets/client/controllers/message-tickets.client.controller.js index 7448be01..59473d99 100644 --- a/modules/tickets/client/controllers/message-tickets.client.controller.js +++ b/modules/tickets/client/controllers/message-tickets.client.controller.js @@ -6,11 +6,12 @@ .controller('MessageTicketController', MessageTicketController); MessageTicketController.$inject = ['$scope', '$state', '$timeout', '$translate', 'Authentication', 'MessageTicketsService', 'ModalConfirmService', 'NotifycationService', 'marked', - 'DebugConsoleService', 'MeanTorrentConfig', '$filter']; + 'DebugConsoleService', 'MeanTorrentConfig', '$filter', '$rootScope']; function MessageTicketController($scope, $state, $timeout, $translate, Authentication, MessageTicketsService, ModalConfirmService, NotifycationService, marked, - mtDebug, MeanTorrentConfig, $filter) { + mtDebug, MeanTorrentConfig, $filter, $rootScope) { var vm = this; + $rootScope.$state = $state; vm.user = Authentication.user; vm.itemsPerPageConfig = MeanTorrentConfig.meanTorrentConfig.itemsPerPage; vm.supportConfig = MeanTorrentConfig.meanTorrentConfig.support; diff --git a/modules/tickets/client/controllers/view-message-tickets.client.controller.js b/modules/tickets/client/controllers/view-message-tickets.client.controller.js new file mode 100644 index 00000000..3d5c35af --- /dev/null +++ b/modules/tickets/client/controllers/view-message-tickets.client.controller.js @@ -0,0 +1,447 @@ +(function () { + 'use strict'; + + angular + .module('tickets') + .controller('ViewMessageTicketController', ViewMessageTicketController); + + ViewMessageTicketController.$inject = ['$scope', '$state', '$timeout', '$translate', 'Authentication', 'MessageTicketsService', 'ModalConfirmService', 'NotifycationService', 'marked', + 'DebugConsoleService', 'MeanTorrentConfig', '$filter', '$stateParams', 'Upload', 'localStorageService', '$compile']; + + function ViewMessageTicketController($scope, $state, $timeout, $translate, Authentication, MessageTicketsService, ModalConfirmService, NotifycationService, marked, + mtDebug, MeanTorrentConfig, $filter, $stateParams, Upload, localStorageService, $compile) { + var vm = this; + vm.user = Authentication.user; + vm.itemsPerPageConfig = MeanTorrentConfig.meanTorrentConfig.itemsPerPage; + vm.supportConfig = MeanTorrentConfig.meanTorrentConfig.support; + vm.inputLengthConfig = MeanTorrentConfig.meanTorrentConfig.inputLength; + + /** + * init + */ + vm.init = function () { + MessageTicketsService.get({ + messageTicketId: $stateParams.messageTicketId + }, function (ticket) { + mtDebug.info(ticket); + vm.ticket = ticket; + vm.buildPager(); + }); + }; + + /** + * buildPager + * pagination init + */ + vm.buildPager = function () { + vm.pagedItems = []; + vm.itemsPerPage = vm.itemsPerPageConfig.messageTicketRepliesPerPage; + vm.currentPage = 1; + vm.figureOutItemsToDisplay(); + }; + + /** + * figureOutItemsToDisplay + * @param callback + */ + vm.figureOutItemsToDisplay = function (callback) { + vm.filterLength = vm.ticket._replies.length; + var begin = ((vm.currentPage - 1) * vm.itemsPerPage); + var end = begin + vm.itemsPerPage; + vm.pagedItems = vm.ticket._replies.slice(begin, end); + + if (callback) callback(); + }; + + /** + * pageChanged + */ + vm.pageChanged = function () { + var element = angular.element('#top_of_reply_list'); + + vm.figureOutItemsToDisplay(function () { + $timeout(function () { + $('html,body').animate({scrollTop: element[0].offsetTop - 60}, 200); + }, 10); + }); + }; + + /** + * getTicketContent + * @param t + * @returns {*} + */ + vm.getTicketContent = function (t) { + if (t) { + return marked(t.content, {sanitize: true}); + } + }; + + /** + * canEdit + * @returns {boolean} + */ + vm.canEdit = function () { + if (vm.user.isOper) { + return true; + } else { + return false; + } + }; + + /** + * isOwner + * @param o, ticket + * @returns {boolean} + */ + vm.isOwner = function (o) { + if (o) { + if (o.from._id === vm.user._id) { + return true; + } else { + return false; + } + } else { + return false; + } + }; + + /** + * onTopicTitleEdited + */ + $scope.onTopicTitleEdited = function (modifyed) { + if (vm.ticket && modifyed) { + vm.ticket.$update(function (res) { + vm.ticket = res; + NotifycationService.showSuccessNotify('SUPPORT.TICKET_EDIT_SUCCESSFULLY'); + }, function (res) { + NotifycationService.showErrorNotify(res.data.message, 'SUPPORT.TICKET_EDIT_FAILED'); + }); + } + }; + + /** + * postReply + * @param isValid + * @returns {boolean} + */ + vm.postReply = function (isValid) { + if (!isValid) { + $scope.$broadcast('show-errors-check-validity', 'vm.replyForm'); + return false; + } + + mtDebug.info($scope.uImages); + var uimg = []; + angular.forEach($scope.uImages, function (f) { + uimg.push({ + filename: f.name + }); + }); + + var reply = new MessageTicketsService(vm.postReplyFields); + reply._uImage = uimg; + + reply.$save({ + messageTicketId: vm.ticket._id + }, function (response) { + successCallback(response); + }, function (errorResponse) { + errorCallback(errorResponse); + }); + + function successCallback(res) { + console.log(res); + vm.postReplyFields = {}; + vm.currentPage = Math.ceil(res._replies.length / vm.itemsPerPage); + vm.ticket = res; + vm.pageChanged(); + + $scope.$broadcast('show-errors-reset', 'vm.replyForm'); + $scope.clearnAttach(); + $scope.hidePreview(); + NotifycationService.showSuccessNotify('FORUMS.POST_REPLY_SUCCESSFULLY'); + } + + function errorCallback(res) { + NotifycationService.showErrorNotify(res.data.message, 'FORUMS.POST_REPLY_FAILED'); + } + }; + + /** + * uploadTicketImage + * @param editor + * @param ufile + * @param progressback + * @param callback + * @param errback + */ + vm.uploadTicketImage = function (editor, ufile, progressback, callback, errback) { + Upload.upload({ + url: '/api/messageTickets/uploadTicketImage', + data: { + newTicketImageFile: ufile + } + }).then(function (res) { + if (callback) { + callback(res.data.filename); + } + }, function (res) { + if (errback && res.status > 0) { + errback(res); + } + }, function (evt) { + if (progressback) { + progressback(parseInt(100.0 * evt.loaded / evt.total, 10)); + } + }); + }; + + /** + * beginDeleteTicket + * @param t + */ + vm.beginDeleteTicket = function (t) { + var modalOptions = { + closeButtonText: $translate.instant('SUPPORT.DELETE_TICKET_CONFIRM_CANCEL'), + actionButtonText: $translate.instant('SUPPORT.DELETE_TICKET_CONFIRM_OK'), + headerText: $translate.instant('SUPPORT.DELETE_TICKET_CONFIRM_HEADER_TEXT'), + bodyText: $translate.instant('SUPPORT.DELETE_TICKET_CONFIRM_BODY_TEXT') + }; + + ModalConfirmService.showModal({}, modalOptions) + .then(function (result) { + MessageTicketsService.remove({ + messageTicketId: t._id, + replyId: undefined + }, function (res) { + console.log(res); + NotifycationService.showSuccessNotify('SUPPORT.DELETE_TICKET_SUCCESSFULLY'); + $state.go('admin.tickets.support.message'); + }, function (res) { + console.log(res); + NotifycationService.showErrorNotify(res.data.message, 'SUPPORT.DELETE_TICKET_FAILED'); + }); + }); + }; + + /** + * beginDeleteReply + * @param reply + */ + vm.beginDeleteReply = function (reply) { + var modalOptions = { + closeButtonText: $translate.instant('FORUMS.DELETE_TOPIC_CONFIRM_CANCEL'), + actionButtonText: $translate.instant('FORUMS.DELETE_TOPIC_CONFIRM_OK'), + headerText: $translate.instant('FORUMS.DELETE_REPLY_CONFIRM_HEADER_TEXT'), + bodyText: $translate.instant('FORUMS.DELETE_REPLY_CONFIRM_BODY_TEXT') + }; + + ModalConfirmService.showModal({}, modalOptions) + .then(function (result) { + MessageTicketsService.remove({ + messageTicketId: vm.ticket._id, + replyId: reply._id + }, function (res) { + console.log(res); + vm.ticket = res; + vm.figureOutItemsToDisplay(); + NotifycationService.showSuccessNotify('FORUMS.DELETE_REPLY_SUCCESSFULLY'); + }, function (res) { + NotifycationService.showErrorNotify(res.data.message, 'FORUMS.DELETE_REPLY_FAILED'); + }); + }); + }; + + /** + * beginEditTicket + * @param t + */ + vm.beginEditTicket = function (t) { + var el = $('#' + t._id); + + el.markdown({ + autofocus: true, + savable: true, + hideable: true, + iconlibrary: 'fa', + resize: 'vertical', + language: localStorageService.get('storage_user_lang'), + fullscreen: {enable: false}, + onSave: function (e) { + if (e.isDirty()) { + //save content + t.content = e.getContent(); + t.$update(function (res) { + vm.ticket = res; + vm.figureOutItemsToDisplay(); + NotifycationService.showSuccessNotify('SUPPORT.TICKET_EDIT_SUCCESSFULLY'); + }, function (res) { + NotifycationService.showErrorNotify(res.data.message, 'SUPPORT.TICKET_EDIT_FAILED'); + }); + + e.$options.hideable = true; + e.blur(); + } else { + e.$options.hideable = true; + e.blur(); + } + }, + onChange: function (e) { + e.$options.hideable = false; + }, + onShow: function (e) { + $('#' + e.$editor.attr('id') + ' .md-input').textcomplete([ + { // emoji strategy + match: /\B:([\-+\w]*)$/, + search: function (term, callback) { + callback($.map(window.emojies, function (emoji) { + return emoji.indexOf(term) === 0 ? emoji : null; + })); + }, + template: function (value) { + return '' + '' + value + ''; + }, + replace: function (value) { + return ':' + value + ': '; + }, + index: 1 + } + ]); + + e.setContent(t.content); + $('#' + e.$editor.attr('id') + ' .md-input').attr('maxlength', vm.inputLengthConfig.ticketContentLength); + + var inputInfo = angular.element(''); + inputInfo.addClass('pull-right'); + inputInfo.addClass('input-length'); + inputInfo.text(e.getContent().length + '/' + vm.inputLengthConfig.ticketContentLength); + $('#' + e.$editor.attr('id') + ' .md-header').append(inputInfo); + $('#' + e.$editor.attr('id') + ' .md-input').on('input propertychange', function (evt) { + inputInfo.text(e.getContent().length + '/' + vm.inputLengthConfig.ticketContentLength); + }); + + var ele = $('#' + e.$editor.attr('id') + ' .md-footer'); + angular.element(ele).addClass('text-right'); + angular.element(ele[0].childNodes[0]).addClass('btn-width-80'); + ele[0].childNodes[0].innerText = $translate.instant('FORUMS.BTN_SAVE'); + + var cbtn = angular.element(''); + cbtn.bind('click', function (evt) { + e.setContent(t.content); + e.$options.hideable = true; + e.blur(); + }); + + ele.append(cbtn); + $compile(e.$editor.contents())($scope); + }, + onPreview: function (e) { + $('#' + e.$editor.attr('id') + ' .md-footer').css('display', 'none'); + }, + onPreviewEnd: function (e) { + $('#' + e.$editor.attr('id') + ' .md-footer').css('display', 'block'); + } + }); + }; + + /** + * beginEditReply + * @param r + */ + vm.beginEditReply = function (r) { + var el = $('#' + r._id); + + el.markdown({ + autofocus: true, + savable: true, + hideable: true, + iconlibrary: 'fa', + resize: 'vertical', + language: localStorageService.get('storage_user_lang'), + fullscreen: {enable: false}, + onSave: function (e) { + if (e.isDirty()) { + //save content + var rep = new MessageTicketsService({ + _id: vm.ticket._id, + _rid: r._id, + content: e.getContent() + }); + + rep.$update(function (res) { + vm.ticket = res; + vm.figureOutItemsToDisplay(); + NotifycationService.showSuccessNotify('FORUMS.REPLY_EDIT_SUCCESSFULLY'); + }, function (res) { + NotifycationService.showErrorNotify(res.data.message, 'FORUMS.REPLY_EDIT_FAILED'); + }); + + e.$options.hideable = true; + e.blur(); + } else { + e.$options.hideable = true; + e.blur(); + } + }, + onChange: function (e) { + e.$options.hideable = false; + }, + onShow: function (e) { + $('#' + e.$editor.attr('id') + ' .md-input').textcomplete([ + { // emoji strategy + match: /\B:([\-+\w]*)$/, + search: function (term, callback) { + callback($.map(window.emojies, function (emoji) { + return emoji.indexOf(term) === 0 ? emoji : null; + })); + }, + template: function (value) { + return '' + '' + value + ''; + }, + replace: function (value) { + return ':' + value + ': '; + }, + index: 1 + } + ]); + + e.setContent(r.content); + $('#' + e.$editor.attr('id') + ' .md-input').attr('maxlength', vm.inputLengthConfig.ticketContentLength); + + var inputInfo = angular.element(''); + inputInfo.addClass('pull-right'); + inputInfo.addClass('input-length'); + inputInfo.text(e.getContent().length + '/' + vm.inputLengthConfig.ticketContentLength); + $('#' + e.$editor.attr('id') + ' .md-header').append(inputInfo); + $('#' + e.$editor.attr('id') + ' .md-input').on('input propertychange', function (evt) { + inputInfo.text(e.getContent().length + '/' + vm.inputLengthConfig.ticketContentLength); + }); + + var ele = $('#' + e.$editor.attr('id') + ' .md-footer'); + angular.element(ele).addClass('text-right'); + angular.element(ele[0].childNodes[0]).addClass('btn-width-80'); + ele[0].childNodes[0].innerText = $translate.instant('FORUMS.BTN_SAVE'); + + var cbtn = angular.element(''); + cbtn.bind('click', function (evt) { + e.setContent(r.content); + e.$options.hideable = true; + e.blur(); + }); + + ele.append(cbtn); + $compile(e.$editor.contents())($scope); + }, + onPreview: function (e) { + $('#' + e.$editor.attr('id') + ' .md-footer').css('display', 'none'); + }, + onPreviewEnd: function (e) { + $('#' + e.$editor.attr('id') + ' .md-footer').css('display', 'block'); + } + }); + }; + + + } +}()); diff --git a/modules/tickets/client/less/tickets.less b/modules/tickets/client/less/tickets.less index e1e05dfb..d01924f3 100644 --- a/modules/tickets/client/less/tickets.less +++ b/modules/tickets/client/less/tickets.less @@ -18,4 +18,46 @@ max-width: 450px !important; min-width: 300px !important; } +} + +.ticket-reply-list { + .reply-item { + .reply-wrapper { + &:not(:first-child) { + margin-left: 60px; + } + margin: 20px 0 0 0; + .reply-comment { + margin-left: 0; + } + .reply-avatar { + img { + cursor: pointer; + float: left; + margin-left: -60px; + border-radius: 3px; + height: 44px; + width: 44px; + } + } + .reply-comment-header-command { + margin-right: 0 !important; + } + } + } + .reply-post-title { + margin: 30px 0 5px 0; + padding: 13px; + background-color: @brand-primary; + color: #fcfcfc; + font-weight: 500; + border-top-right-radius: 5px; + border-top-left-radius: 5px; + } + .editable-line { + min-height: 30px; + } + .pagination { + margin: 20px 0 0 120px; + } } \ No newline at end of file diff --git a/modules/tickets/client/services/tickets.client.service.js b/modules/tickets/client/services/tickets.client.service.js index a336f3cb..a683d8e5 100644 --- a/modules/tickets/client/services/tickets.client.service.js +++ b/modules/tickets/client/services/tickets.client.service.js @@ -8,8 +8,9 @@ MessageTicketsService.$inject = ['$resource']; function MessageTicketsService($resource) { - return $resource('/api/messageTickets/:messageTicketId', { - requestId: '@_id' + return $resource('/api/messageTickets/:messageTicketId/:replyId', { + messageTicketId: '@_id', + replyId: '@_rid' }, { update: { method: 'PUT' @@ -25,7 +26,7 @@ function MailTicketsService($resource) { return $resource('/api/mailTickets/:mailTicketId', { - requestId: '@_id' + mailTicketId: '@_id' }, { update: { method: 'PUT' diff --git a/modules/tickets/client/views/support-message.client.view.html b/modules/tickets/client/views/support-message.client.view.html index 3443509f..9fac3a01 100644 --- a/modules/tickets/client/views/support-message.client.view.html +++ b/modules/tickets/client/views/support-message.client.view.html @@ -41,10 +41,10 @@
- {{item.title}} + {{item.title}}
- {{item.replies ? item.replies.length : 0}} + {{item._replies ? item._replies.length : 0}} {{item.createdAt | life}} @@ -60,7 +60,8 @@ ng-if="item.status=='solved'"> {{item.status}} - + + {{'SUPPORT.LINK_HANDLE' | translate}} diff --git a/modules/tickets/client/views/support-view-message.client.view.html b/modules/tickets/client/views/support-view-message.client.view.html new file mode 100644 index 00000000..20d0041f --- /dev/null +++ b/modules/tickets/client/views/support-view-message.client.view.html @@ -0,0 +1,136 @@ +
+
+ +
+
+

+
+
+ +
+ + +
+
+
+ + + + + + ({{vm.ticket.from.uploaded | bytes}} + {{vm.ticket.from.downloaded | bytes}} + ) + + {{vm.ticket.createdAt | date:'yyyy-MM-dd HH:mm:ss'}} + +
+ + +
+
+
+
+
+
+
+
+
+ + +
+ + +
+
+
+ + + + + + ({{rep.from.uploaded | bytes}} + {{rep.from.downloaded | bytes}} + ) + + {{rep.createdAt | date:'yyyy-MM-dd HH:mm:ss'}} + +
+ + +
+
+
+
+
+
+
+
+
+
+
+ +
+
    +
+
+ +
+ {{'FORUMS.BTN_POST_NEW_REPLY' | translate}} +
+ +
+
+
+
+
+ + +
+

{{ 'FORUMS.PRC_REQUIRED' | translate}}

+
+
+ + + +
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/modules/tickets/client/views/support.client.view.html b/modules/tickets/client/views/support.client.view.html index 475700ef..57b0db1f 100644 --- a/modules/tickets/client/views/support.client.view.html +++ b/modules/tickets/client/views/support.client.view.html @@ -1,14 +1,18 @@ -
+
diff --git a/modules/tickets/server/controllers/mailTickets.server.controller.js b/modules/tickets/server/controllers/mailTickets.server.controller.js index 2ecad4f5..ca1d99be 100644 --- a/modules/tickets/server/controllers/mailTickets.server.controller.js +++ b/modules/tickets/server/controllers/mailTickets.server.controller.js @@ -11,6 +11,22 @@ var path = require('path'), MailTicket = mongoose.model('MailTicket'), async = require('async'); +/** + * read Messages + * @param req + * @param res + */ +exports.read = function (req, res) { + // convert mongoose document to JSON + var ticket = req.mailTicket ? req.mailTicket.toJSON() : {}; + + // Add a custom field to the Article, for determining if the current User is the "owner". + // NOTE: This field is NOT persisted to the database, since it doesn't exist in the Article model. + ticket.isCurrentUserOwner = !!(req.user && ticket.user && ticket.user._id.toString() === req.user._id.toString()); + + res.json(ticket); +}; + /** * list Messages * @param req diff --git a/modules/tickets/server/controllers/messageTickets.server.controller.js b/modules/tickets/server/controllers/messageTickets.server.controller.js index d025b4f7..43b8fee9 100644 --- a/modules/tickets/server/controllers/messageTickets.server.controller.js +++ b/modules/tickets/server/controllers/messageTickets.server.controller.js @@ -7,10 +7,14 @@ var path = require('path'), config = require(path.resolve('./config/config')), mongoose = require('mongoose'), errorHandler = require(path.resolve('./modules/core/server/controllers/errors.server.controller')), + multer = require('multer'), + fs = require('fs'), User = mongoose.model('User'), MessageTicket = mongoose.model('MessageTicket'), async = require('async'); +var mtDebug = require(path.resolve('./config/lib/debug')); + /** * create a Message * @param req @@ -31,6 +35,22 @@ exports.create = function (req, res) { }); }; +/** + * read Messages + * @param req + * @param res + */ +exports.read = function (req, res) { + // convert mongoose document to JSON + var ticket = req.messageTicket ? req.messageTicket.toJSON() : {}; + + // Add a custom field to the Article, for determining if the current User is the "owner". + // NOTE: This field is NOT persisted to the database, since it doesn't exist in the Article model. + ticket.isCurrentUserOwner = !!(req.user && ticket.from && ticket.from._id.toString() === req.user._id.toString()); + + res.json(ticket); +}; + /** * list Messages * @param req @@ -103,35 +123,16 @@ exports.list = function (req, res) { */ exports.delete = function (req, res) { if (req.user.isOper) { - if (req.params.messageId) { - var message = req.messageTicket; - message.remove(function (err) { - if (err) { - return res.status(422).send({ - message: errorHandler.getErrorMessage(err) - }); - } else { - res.json(message); - } - }); - - } else { - if (req.query.ids) { - MessageTicket.remove({ - _id: {$in: req.query.ids} - }, function (err) { - if (err) { - return res.status(422).send({ - message: errorHandler.getErrorMessage(err) - }); - } else { - return res.status(200).send({ - message: 'delete successfully' - }); - } + var message = req.messageTicket; + message.remove(function (err) { + if (err) { + return res.status(422).send({ + message: errorHandler.getErrorMessage(err) }); + } else { + res.json(message); } - } + }); } else { return res.status(403).json({ message: 'SERVER.USER_IS_NOT_AUTHORIZED' @@ -145,6 +146,8 @@ exports.delete = function (req, res) { exports.update = function (req, res) { var message = req.messageTicket; + message.title = req.body.title; + message.content = req.body.content; message.updatedAt = Date.now(); message.save(function (err) { @@ -165,11 +168,31 @@ exports.update = function (req, res) { */ exports.createReply = function (req, res) { var reply = new MessageTicket(req.body); + reply.from = req.user._id; var message = req.messageTicket; message._replies.push(reply); message.updatedAt = Date.now(); + //replace content path + var tmp = config.uploads.tickets.image.temp.substr(1); + var dst = config.uploads.tickets.image.dest.substr(1); + + var regex = new RegExp(tmp, 'g'); + reply.content = reply.content.replace(regex, dst); + + //move temp torrent file to dest directory + req.body._uImage.forEach(function (f) { + var oldPath = config.uploads.tickets.image.temp + f.filename; + var newPath = config.uploads.tickets.image.dest + f.filename; + move(oldPath, newPath, function (err) { + if (err) { + mtDebug.debugRed(err); + } + }); + }); + + //message save message.save(function (err) { if (err) { return res.status(422).send({ @@ -192,6 +215,171 @@ exports.createReply = function (req, res) { }); }; +/** + * move file + * @param oldPath + * @param newPath + * @param callback + */ +function move(oldPath, newPath, callback) { + fs.rename(oldPath, newPath, function (err) { + if (err) { + if (err.code === 'EXDEV') { + copy(); + } else { + callback(err); + } + return; + } + callback(); + }); + + function copy() { + var readStream = fs.createReadStream(oldPath); + var writeStream = fs.createWriteStream(newPath); + + readStream.on('error', callback); + writeStream.on('error', callback); + + readStream.on('close', function () { + fs.unlink(oldPath, callback); + }); + readStream.pipe(writeStream); + } +} + +/** + * uploadTicketImage + * @param req + * @param res + */ +exports.uploadTicketImage = function (req, res) { + var user = req.user; + var createUploadTicketImageFilename = require(path.resolve('./config/lib/multer')).createUploadTicketImageFilename; + var getUploadTicketImageDestination = require(path.resolve('./config/lib/multer')).getUploadTicketImageDestination; + var fileFilter = require(path.resolve('./config/lib/multer')).imageFileFilter; + var imageInfo = {}; + + var storage = multer.diskStorage({ + destination: getUploadTicketImageDestination, + filename: createUploadTicketImageFilename + }); + + var upload = multer({ + storage: storage, + fileFilter: fileFilter, + limits: config.uploads.tickets.image.limits + }).single('newTicketImageFile'); + + if (user) { + uploadFile() + .then(function () { + res.status(200).send(imageInfo); + }) + .catch(function (err) { + res.status(422).send(err); + mtDebug.debugRed(err); + + if (req.file && req.file.filename) { + var newfile = config.uploads.tickets.image.temp + req.file.filename; + if (fs.existsSync(newfile)) { + mtDebug.debugRed(err); + mtDebug.debugRed('ERROR: DELETE TEMP TICKET IMAGE FILE: ' + newfile); + fs.unlinkSync(newfile); + } + } + }); + } else { + res.status(401).send({ + message: 'User is not signed in' + }); + } + + function uploadFile() { + return new Promise(function (resolve, reject) { + upload(req, res, function (uploadError) { + if (uploadError) { + var message = errorHandler.getErrorMessage(uploadError); + + if (uploadError.code === 'LIMIT_FILE_SIZE') { + message = 'Ticket image file too large. Maximum size allowed is ' + (config.uploads.tickets.image.limits.fileSize / (1024 * 1024)).toFixed(2) + ' Mb files.'; + } + + reject(message); + } else { + imageInfo.filename = req.file.filename; + resolve(); + } + }); + }); + } +}; + +/** + * deleteReply + * @param req + * @param res + */ +exports.deleteReply = function (req, res) { + var message = req.messageTicket; + + message._replies.forEach(function (r) { + if (r._id.equals(req.params.replyId)) { + + if (!canEdit(req.user) && !isOwner(req.user, r)) { + return res.status(403).json({ + message: 'SERVER.USER_IS_NOT_AUTHORIZED' + }); + } else { + message._replies.pull(r); + message.markModified('_replies'); + + message.save(function (err) { + if (err) { + return res.status(422).send({ + message: errorHandler.getErrorMessage(err) + }); + } else { + res.json(message); + } + }); + } + } + }); +}; + +/** + * updateReply + * @param req + * @param res + */ +exports.updateReply = function (req, res) { + var message = req.messageTicket; + + message._replies.forEach(function (r) { + if (r._id.equals(req.params.replyId)) { + + if (!canEdit(req.user) && !isOwner(req.user, r)) { + return res.status(403).json({ + message: 'SERVER.USER_IS_NOT_AUTHORIZED' + }); + } + + r.content = req.body.content; + message.markModified('_replies'); + message.save(function (err) { + if (err) { + return res.status(422).send({ + message: errorHandler.getErrorMessage(err) + }); + } else { + res.json(message); + } + }); + } + }); +}; + /** * Invitation middleware */ @@ -221,3 +409,35 @@ exports.messageTicketById = function (req, res, next, id) { }); }; +/** + * canEdit + * @param u, req.user + * @param f, forum + * @returns {boolean} + */ +function canEdit(u) { + if (u.isOper) { + return true; + } else if (u.isAdmin) { + return true; + } else { + return false; + } +} + +/** + * isOwner + * @param o, topic or reply object + * @returns {boolean} + */ +function isOwner(u, o) { + if (o) { + if (o.from._id.equals(u._id)) { + return true; + } else { + return false; + } + } else { + return false; + } +} diff --git a/modules/tickets/server/policies/tickets.server.policy.js b/modules/tickets/server/policies/tickets.server.policy.js index 508f6c59..236537ae 100644 --- a/modules/tickets/server/policies/tickets.server.policy.js +++ b/modules/tickets/server/policies/tickets.server.policy.js @@ -19,8 +19,11 @@ exports.invokeRolesPolicies = function () { allows: [ {resources: '/api/messageTickets', permissions: '*'}, {resources: '/api/messageTickets/:messageTicketId', permissions: '*'}, + {resources: '/api/messageTickets/:messageTicketId/:replyId', permissions: '*'}, {resources: '/api/mailTickets', permissions: '*'}, - {resources: '/api/mailTickets/:mailTicketId', permissions: '*'} + {resources: '/api/mailTickets/:mailTicketId', permissions: '*'}, + {resources: '/api/mailTickets/:mailTicketId/:replyId', permissions: '*'}, + {resources: '/api/messageTickets/uploadTicketImage', permissions: '*'} ] } ] diff --git a/modules/tickets/server/routes/tickets.server.routes.js b/modules/tickets/server/routes/tickets.server.routes.js index 3e724f9b..7bf5076f 100644 --- a/modules/tickets/server/routes/tickets.server.routes.js +++ b/modules/tickets/server/routes/tickets.server.routes.js @@ -10,23 +10,32 @@ var ticketsPolicy = require('../policies/tickets.server.policy'), module.exports = function (app) { app.route('/api/messageTickets').all(ticketsPolicy.isAllowed) .get(messageTickets.list) - .post(messageTickets.create) - .delete(messageTickets.delete); + .post(messageTickets.create); + + app.route('/api/messageTickets/uploadTicketImage').all(ticketsPolicy.isAllowed) + .post(messageTickets.uploadTicketImage); app.route('/api/messageTickets/:messageTicketId').all(ticketsPolicy.isAllowed) + .get(messageTickets.read) .delete(messageTickets.delete) .put(messageTickets.update) .post(messageTickets.createReply); + app.route('/api/messageTickets/:messageTicketId/:replyId').all(ticketsPolicy.isAllowed) + .delete(messageTickets.deleteReply) + .put(messageTickets.updateReply); + + app.route('/api/mailTickets').all(ticketsPolicy.isAllowed) .get(mailTickets.list) .delete(mailTickets.delete); app.route('/api/mailTickets/:mailTicketId').all(ticketsPolicy.isAllowed) + .get(mailTickets.read) .delete(mailTickets.delete) .put(mailTickets.update) .post(mailTickets.createReply); app.param('messageTicketId', messageTickets.messageTicketById); - app.param('messageTicketId', mailTickets.mailTicketById); + app.param('mailTicketId', mailTickets.mailTicketById); };