diff --git a/customize.dist/src/less2/include/support.less b/customize.dist/src/less2/include/support.less index 890ba810d..d83746b51 100644 --- a/customize.dist/src/less2/include/support.less +++ b/customize.dist/src/less2/include/support.less @@ -4,6 +4,9 @@ @msg-bg: #eee; @fromme-bg: #ddd; .cp-support-form-container { + div { + margin-bottom: 10px; + } [type="text"] { width: @sidebar_button-width; margin-bottom: 10px; @@ -15,6 +18,18 @@ height: 300px; } } + .cp-support-attachments { + display: flex; + .fa { + cursor: pointer; + margin-right: 10px; + } + &> span { + border: 1px solid #ddd; + margin-right: 5px; + padding: 10px; + } + } .cp-support-container { .cp-support-list-ticket { display: flex; diff --git a/www/admin/inner.js b/www/admin/inner.js index 2ff62cf1f..90048e76d 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -185,6 +185,20 @@ define([ var $container = makeBlock('support-list'); var $div = $(h('div.cp-support-container')).appendTo($container); + var catContainer = h('div.cp-dropdown-container'); + $div.append(catContainer); + var category = 'all'; + var $drop = APP.support.makeCategoryDropdown(catContainer, function (key) { + category = key; + if (key === 'all') { + $div.find('.cp-support-list-ticket').show(); + return; + } + $div.find('.cp-support-list-ticket').hide(); + $div.find('.cp-support-list-ticket[data-cat="'+key+'"]').show(); + }, true); + $drop.setValue('all'); + var metadataMgr = common.getMetadataMgr(); var privateData = metadataMgr.getPrivateData(); var cat = privateData.category || ''; @@ -277,6 +291,9 @@ define([ UI.alert(Messages.error); }); }); + if (category !== 'all' && $ticket.attr('data-cat') !== category) { + $ticket.hide(); + } } $ticket.append(APP.support.makeMessage(content, hash)); reorder(); diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 4f8ee71c5..d9f61ef6f 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -2203,7 +2203,9 @@ define([ if (config.isSelect && value) { var $val = $innerblock.find('[data-value="'+value+'"]'); setActive($val); - $innerblock.scrollTop($val.position().top + $innerblock.scrollTop()); + try { + $innerblock.scrollTop($val.position().top + $innerblock.scrollTop()); + } catch (e) {} } if (config.feedback) { Feedback.send(config.feedback); } }; diff --git a/www/support/app-support.less b/www/support/app-support.less index 7684c054e..41af016fd 100644 --- a/www/support/app-support.less +++ b/www/support/app-support.less @@ -18,6 +18,15 @@ display: flex; flex-flow: column; + .cp-support-form-attachments { + .fa { + cursor: pointer; + } + &> span { + padding: 10px; + } + } + .cp-support-language-list { .cp-support-language { margin-left: 5px; diff --git a/www/support/inner.js b/www/support/inner.js index d956914c6..ed1c16955 100644 --- a/www/support/inner.js +++ b/www/support/inner.js @@ -165,8 +165,6 @@ define([ var form = APP.support.makeForm(); - $div.find('button').before(form); - var id = Util.uid(); $div.find('button').click(function () { @@ -182,6 +180,7 @@ define([ $('.cp-sidebarlayout-category[data-category="tickets"]').click(); } }); + $div.find('button').before(form); return $div; }; diff --git a/www/support/ui.js b/www/support/ui.js index 04330734e..7449e2e07 100644 --- a/www/support/ui.js +++ b/www/support/ui.js @@ -6,8 +6,9 @@ define([ '/common/common-hash.js', '/common/common-util.js', '/common/clipboard.js', + '/common/common-ui-elements.js', '/customize/messages.js', -], function ($, ApiConfig, h, UI, Hash, Util, Clipboard, Messages) { +], function ($, ApiConfig, h, UI, Hash, Util, Clipboard, UIElements, Messages) { var send = function (ctx, id, type, data, dest) { var common = ctx.common; @@ -61,9 +62,15 @@ define([ }; var sendForm = function (ctx, id, form, dest) { - var $title = $(form).find('.cp-support-form-title'); - var $content = $(form).find('.cp-support-form-msg'); + var $form = $(form); + var $cat = $form.find('.cp-support-form-category'); + var $title = $form.find('.cp-support-form-title'); + var $content = $form.find('.cp-support-form-msg'); + // XXX block submission until pending uploads are complete? + var $attachments = $form.find('.cp-support-attachments'); + + var category = $cat.val().trim(); // XXX make category a required field? var title = $title.val().trim(); if (!title) { return void UI.alert(Messages.support_formTitleError); @@ -72,18 +79,60 @@ define([ if (!content) { return void UI.alert(Messages.support_formContentError); } + $cat.val(''); $content.val(''); $title.val(''); + var attachments = []; + $attachments.find('> span').each(function (i, el) { + var $el = $(el); + attachments.push({ + href: $el.attr('data-href'), + name: $el.attr('data-name') + }); + }); + send(ctx, id, 'TICKET', { + category: category, title: title, + attachments: attachments, message: content, }, dest); return true; }; - var makeForm = function (cb, title) { +Messages.support_cat_account = "User account"; // XXX +Messages.support_cat_data = "Loss of content"; // XXX +Messages.support_cat_bug = "Bug report"; // XXX +Messages.support_cat_other = "Other"; // XXX +Messages.support_cat_all = "All"; // XXX +Messages.support_category = "Category"; // XXX +Messages.support_attachments = "Attachments"; // XXX +Messages.support_addAttachment = "Add attachment"; // XXX + + var makeCategoryDropdown = function (ctx, container, onChange, all) { + var categories = ['account', 'data', 'bug', 'other']; + if (all) { categories.push('all'); } + categories = categories.map(function (key) { + return { + tag: 'a', + content: h('span', Messages['support_cat_'+key]), + action: function () { + onChange(key); + } + }; + }); + var dropdownCfg = { + text: Messages.support_category, + options: categories, + container: $(container), + isSelect: true + }; + return UIElements.createDropdown(dropdownCfg); + }; + + var makeForm = function (ctx, cb, title) { var button; if (typeof(cb) === "function") { @@ -93,8 +142,21 @@ define([ var cancel = title ? h('button.btn.btn-secondary', Messages.cancel) : undefined; + var category = h('input.cp-support-form-category', { + type: 'hidden', + value: '' + }); + var catContainer = h('div.cp-dropdown-container' + (title ? '.cp-hidden': '')); + makeCategoryDropdown(ctx, catContainer, function (key) { + $(category).val(key); + }); + + var attachments, addAttachment; + var content = [ h('hr'), + category, + catContainer, h('input.cp-support-form-title' + (title ? '.cp-hidden' : ''), { placeholder: Messages.support_formTitle, type: 'text', @@ -104,11 +166,53 @@ define([ h('textarea.cp-support-form-msg', { placeholder: Messages.support_formMessage }), + h('label', Messages.support_attachments), + attachments = h('div.cp-support-attachments'), + addAttachment = h('button', Messages.support_addAttachment), h('hr'), button, cancel ]; + $(addAttachment).click(function () { + var $input = $('', { + 'type': 'file', + 'style': 'display: none;', + 'multiple': 'multiple', + 'accept': 'image/*' + }).on('change', function (e) { + var files = Util.slice(e.target.files); + files.forEach(function (file) { + // XXX validate that the href is hosted on the same instance + // use relative URLs or compare it against a list or allowed domains? + var ev = {}; + ev.callback = function (data) { + var x, a; + var span = h('span', { + 'data-name': data.name, + 'data-href': data.url + }, [ + x = h('i.fa.fa-times'), + a = h('a', { + href: '#' + }, data.name) + ]); + $(x).click(function () { + $(span).remove(); + }); + $(a).click(function (e) { + e.preventDefault(); + ctx.common.openURL(data.url); + }); + + $(attachments).append(span); + }; + ctx.FM.handleFile(file, ev); + }); + }); + $input.click(); + }); + var form = h('div.cp-support-form-container', content); $(cancel).click(function () { @@ -125,6 +229,7 @@ define([ var privateData = metadataMgr.getPrivateData(); var ticketTitle = content.title + ' (#' + content.id + ')'; + var ticketCategory; var answer = h('button.btn.btn-primary.cp-support-answer', Messages.support_answer); var close = h('button.btn.btn-danger.cp-support-close', Messages.support_close); var hide = h('button.btn.btn-danger.cp-support-hide', Messages.support_remove); @@ -137,6 +242,7 @@ define([ var url; if (ctx.isAdmin) { + ticketCategory = Messages['support_cat_'+(content.category || 'other')] + ' - '; url = h('button.btn.btn-primary.fa.fa-clipboard'); $(url).click(function () { var link = privateData.origin + privateData.pathname + '#' + 'support-' + content.id; @@ -146,9 +252,10 @@ define([ } var $ticket = $(h('div.cp-support-list-ticket', { + 'data-cat': content.category, 'data-id': content.id }, [ - h('h2', [ticketTitle, url]), + h('h2', [ticketCategory, ticketTitle, url]), actions ])); @@ -179,7 +286,7 @@ define([ $(answer).click(function () { $ticket.find('.cp-support-form-container').remove(); $(actions).hide(); - var form = makeForm(function () { + var form = makeForm(ctx, function () { var sent = sendForm(ctx, content.id, form, content.sender); if (sent) { $(actions).show(); @@ -215,6 +322,21 @@ define([ ev.stopPropagation(); }); + var attachments = (content.attachments || []).map(function (obj) { + if (!obj || !obj.name || !obj.href) { return; } + var a = h('a', { + href: '#' + }, obj.name) + // XXX disallow remote URLs + $(a).click(function (e) { + e.preventDefault(); + ctx.common.openURL(obj.href); + }); + return h('span', [ + a + ]); + }); + var adminClass = (fromAdmin? '.cp-support-fromadmin': ''); var premiumClass = (fromPremium && !fromAdmin? '.cp-support-frompremium': ''); var name = Util.fixHTML(content.sender.name) || Messages.anonymous; @@ -226,6 +348,7 @@ define([ h('span.cp-support-message-time', content.time ? new Date(content.time).toLocaleString() : '') ]), h('pre.cp-support-message-content', content.message), + h('div.cp-support-attachments', attachments), isAdmin ? userData : undefined, ]); }; @@ -257,10 +380,25 @@ define([ adminKeys: Array.isArray(ApiConfig.adminKeys)? ApiConfig.adminKeys.slice(): [], }; + var fmConfig = { + body: $('body'), + onUploaded: function (ev, data) { + if (ev.callback) { + ev.callback(data); + } + } + }; + ctx.FM = common.createFileManager(fmConfig); + ui.sendForm = function (id, form, dest) { return sendForm(ctx, id, form, dest); }; - ui.makeForm = makeForm; + ui.makeForm = function (cb, title) { + return makeForm(ctx, cb, title); + }; + ui.makeCategoryDropdown = function (container, onChange, all) { + return makeCategoryDropdown(ctx, container, onChange, all); + }; ui.makeTicket = function ($div, content, onHide) { return makeTicket(ctx, $div, content, onHide); };