New support: add search and fix UI issues

This commit is contained in:
yflory 2024-03-06 15:29:28 +01:00
parent b943930948
commit a4bdc64520
7 changed files with 232 additions and 377 deletions

View File

@ -133,6 +133,26 @@
margin: 0;
}
}
.cp-sidebar-input-block {
display: inline-flex;
width: @sidebar_button-width;
max-width: 100%;
input {
flex: 1;
//border-radius: 0.25em 0 0 0.25em;
border: 1px solid @cryptpad_color_grey_500;
border-right: 0px;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
}
button {
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
border-left: 0px;
height: 40px;
margin: 0 !important;
}
}
}
.cp-sidebarlayout-description {

View File

@ -99,9 +99,6 @@ define([
],
'support': [ // Msg.admin_cat_support
'cp-admin-support-new',
'cp-admin-support-list',
'cp-admin-support-init',
'cp-admin-support-priv',
],
'broadcast': [ // Msg.admin_cat_broadcast
'cp-admin-maintenance',
@ -2623,301 +2620,6 @@ Example
return $div;
};
var supportKey = ApiConfig.supportMailbox;
var checkAdminKey = function (priv) {
if (!supportKey) { return; }
return Hash.checkBoxKeyPair(priv, supportKey);
};
create['support-list'] = function () {
if (!supportKey || !APP.privateKey || !checkAdminKey(APP.privateKey)) { return; }
// XXX NO NEED ANYMORE
return;
var $container = makeBlock('support-list'); // Msg.admin_supportListHint, .admin_supportListTitle
var $div = $(h('div.cp-support-container')).appendTo($container);
var catContainer = h('div.cp-dropdown-container');
var col1 = h('div.cp-support-column', h('h1', [
h('span', Messages.admin_support_premium),
h('span.cp-support-count'),
h('button.btn.cp-support-column-button', Messages.admin_support_collapse)
]));
var col2 = h('div.cp-support-column', h('h1', [
h('span', Messages.admin_support_normal),
h('span.cp-support-count'),
h('button.btn.cp-support-column-button', Messages.admin_support_collapse)
]));
var col3 = h('div.cp-support-column', h('h1', [
h('span', Messages.admin_support_answered),
h('span.cp-support-count'),
h('button.btn.cp-support-column-button', Messages.admin_support_collapse)
]));
var col4 = h('div.cp-support-column', h('h1', [
h('span', Messages.admin_support_closed),
h('span.cp-support-count'),
h('button.btn.cp-support-column-button', Messages.admin_support_collapse)
]));
var $col1 = $(col1), $col2 = $(col2), $col3 = $(col3), $col4 = $(col4);
$div.append([
//catContainer
col1,
col2,
col3,
col4
]);
$div.find('.cp-support-column-button').click(function () {
var $col = $(this).closest('.cp-support-column');
$col.toggleClass('cp-support-column-collapsed');
if ($col.hasClass('cp-support-column-collapsed')) {
$(this).text(Messages.admin_support_open);
$(this).toggleClass('btn-primary');
} else {
$(this).text(Messages.admin_support_collapse);
$(this).toggleClass('btn-primary');
}
});
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 || '';
var linkedId = cat.indexOf('-') !== -1 && cat.slice(8);
var hashesById = {};
var getTicketData = function (id) {
var t = hashesById[id];
if (!Array.isArray(t) || !t.length) { return; }
var ed = Util.find(t[0], ['content', 'msg', 'content', 'sender', 'edPublic']);
// If one of their ticket was sent as a premium user, mark them as premium
var premium = t.some(function (msg) {
var _ed = Util.find(msg, ['content', 'msg', 'content', 'sender', 'edPublic']);
if (ed !== _ed) { return; }
return Util.find(msg, ['content', 'msg', 'content', 'sender', 'plan']) ||
Util.find(msg, ['content', 'msg', 'content', 'sender', 'quota', 'plan']);
});
var lastMsg = t[t.length - 1];
var lastMsgEd = Util.find(lastMsg, ['content', 'msg', 'content', 'sender', 'edPublic']);
return {
lastMsg: lastMsg,
time: Util.find(lastMsg, ['content', 'msg', 'content', 'time']),
lastMsgEd: lastMsgEd,
lastAdmin: lastMsgEd !== ed && ApiConfig.adminKeys.indexOf(lastMsgEd) !== -1,
premium: premium,
authorEd: ed,
closed: Util.find(lastMsg, ['content', 'msg', 'type']) === 'CLOSE'
};
};
var addClickHandler = function ($ticket) {
$ticket.on('click', function () {
$ticket.toggleClass('cp-support-open', true);
$ticket.off('click');
});
};
var makeOpenButton = function ($ticket) {
var button = h('button.btn.btn-primary.cp-support-expand', Messages.admin_support_open);
var collapse = h('button.btn.cp-support-collapse', Messages.admin_support_collapse);
$(button).click(function () {
$ticket.toggleClass('cp-support-open', true);
});
addClickHandler($ticket);
$(collapse).click(function (e) {
$ticket.toggleClass('cp-support-open', false);
e.stopPropagation();
setTimeout(function () {
addClickHandler($ticket);
});
});
$ticket.find('.cp-support-title-buttons').prepend([button, collapse]);
$ticket.append(h('div.cp-support-collapsed'));
};
var updateTicketDetails = function ($ticket, isPremium) {
var $first = $ticket.find('.cp-support-message-from').first();
var user = $first.find('span').first().html();
var time = $first.find('.cp-support-message-time').text();
var last = $ticket.find('.cp-support-message-from').last().find('.cp-support-message-time').text();
var $c = $ticket.find('.cp-support-collapsed');
var txtClass = isPremium ? ".cp-support-ispremium" : "";
$c.html('').append([
UI.setHTML(h('span'+ txtClass), user),
h('span', [
h('b', Messages.admin_support_first),
h('span', time)
]),
h('span', [
h('b', Messages.admin_support_last),
h('span', last)
])
]);
};
var sort = function (id1, id2) {
var t1 = getTicketData(id1);
var t2 = getTicketData(id2);
if (!t1) { return 1; }
if (!t2) { return -1; }
/*
// If one is answered and not the other, put the unanswered first
if (t1.lastAdmin && !t2.lastAdmin) { return 1; }
if (!t1.lastAdmin && t2.lastAdmin) { return -1; }
*/
// Otherwise, sort them by time
return t1.time - t2.time;
};
var _reorder = function () {
var orderAnswered = [];
var orderPremium = [];
var orderNormal = [];
var orderClosed = [];
Object.keys(hashesById).forEach(function (id) {
var d = getTicketData(id);
if (!d) { return; }
if (d.closed) {
return void orderClosed.push(id);
}
if (d.lastAdmin /* && !d.closed */) {
return void orderAnswered.push(id);
}
if (d.premium /* && !d.lastAdmin && !d.closed */) {
return void orderPremium.push(id);
}
orderNormal.push(id);
//if (!d.premium && !d.lastAdmin && !d.closed) { return void orderNormal.push(id); }
});
var cols = [$col1, $col2, $col3, $col4];
[orderPremium, orderNormal, orderAnswered, orderClosed].forEach(function (list, j) {
list.sort(sort);
list.forEach(function (id, i) {
var $t = $div.find('[data-id="'+id+'"]');
var d = getTicketData(id);
$t.css('order', i).appendTo(cols[j]);
updateTicketDetails($t, d.premium);
});
var len;
try {
len = cols[j].find('div.cp-support-list-ticket').length;
} catch (err) {
UI.warn(Messages.error);
return void console.error(err);
}
if (!len) {
cols[j].hide();
} else {
cols[j].show();
cols[j].find('.cp-support-count').text(len);
}
});
};
var reorder = Util.throttle(_reorder, 150);
var to = Util.throttle(function () {
var $ticket = $div.find('.cp-support-list-ticket[data-id="'+linkedId+'"]');
$ticket.addClass('cp-support-open');
$ticket[0].scrollIntoView();
linkedId = undefined;
}, 200);
// Register to the "support" mailbox
common.mailbox.subscribe(['supportadmin'], {
onMessage: function (data) {
/*
Get ID of the ticket
If we already have a div for this ID
Push the message to the end of the ticket
If it's a new ticket ID
Make a new div for this ID
*/
var msg = data.content.msg;
var hash = data.content.hash;
var content = msg.content;
var id = content.id;
var $ticket = $div.find('.cp-support-list-ticket[data-id="'+id+'"]');
hashesById[id] = hashesById[id] || [];
if (hashesById[id].indexOf(hash) === -1) {
hashesById[id].push(data);
}
if (msg.type === 'CLOSE') {
// A ticket has been closed by the admins...
if (!$ticket.length) { return; }
$ticket.addClass('cp-support-list-closed');
$ticket.append(APP.support.makeCloseMessage(content, hash));
reorder();
return;
}
if (msg.type !== 'TICKET') { return; }
$ticket.removeClass('cp-support-list-closed');
if (!$ticket.length) {
$ticket = APP.support.makeTicket($div, content, function (hideButton) {
// the ticket will still be displayed until the worker confirms its deletion
// so make it unclickable in the meantime
hideButton.setAttribute('disabled', true);
var error = false;
nThen(function (w) {
hashesById[id].forEach(function (d) {
common.mailbox.dismiss(d, w(function (err) {
if (err) {
error = true;
console.error(err);
}
}));
});
}).nThen(function () {
if (!error) {
$ticket.remove();
delete hashesById[id];
reorder();
return;
}
// if deletion failed then reactivate the button and warn
hideButton.removeAttribute('disabled');
// and show a generic error message
UI.alert(Messages.error);
});
});
makeOpenButton($ticket);
if (category !== 'all' && $ticket.attr('data-cat') !== category) {
$ticket.hide();
}
}
$ticket.append(APP.support.makeMessage(content, hash));
reorder();
if (linkedId) { to(); }
}
});
return $container;
};
create['support-priv'] = function () {
if (!supportKey || !APP.privateKey || !checkAdminKey(APP.privateKey)) { return; }
var $div = makeBlock('support-priv', true); // Msg.admin_supportPrivHint, .admin_supportPrivTitle, .admin_supportPrivButton
var $button = $div.find('button').click(function () {
$button.remove();
var $selectable = $(UI.dialog.selectable(APP.privateKey)).css({ 'max-width': '28em' });
$div.append($selectable);
});
return $div;
};
// XXX
Messages.admin_supportNewHint = "Create or update the current support keys.";
Messages.admin_supportNewTitle = "Initialize support";
@ -3183,48 +2885,6 @@ Example
refresh();
return $block;
};
create['support-init'] = function () {
var $div = makeBlock('support-init'); // Msg.admin_supportInitHint, .admin_supportInitTitle
if (!supportKey) {
(function () {
return; // XXX old support can't be created anymore
})();
return $div;
}
if (!APP.privateKey || !checkAdminKey(APP.privateKey)) {
$div.append(h('p', Messages.admin_supportInitPrivate));
var error = h('div.cp-admin-support-error');
var input = h('input.cp-admin-add-private-key');
var button = h('button.btn.btn-primary', Messages.admin_supportAddKey);
if (APP.privateKey && !checkAdminKey(APP.privateKey)) {
$(error).text(Messages.admin_supportAddError);
}
$div.append(h('div', [
error,
input,
button
]));
$(button).click(function () {
var key = $(input).val();
if (!checkAdminKey(key)) {
$(input).val('');
return void $(error).text(Messages.admin_supportAddError);
}
sFrameChan.query("Q_ADMIN_MAILBOX", key, function () {
APP.privateKey = key;
$('.cp-admin-support-init').hide();
APP.$rightside.append(create['support-list']());
APP.$rightside.append(create['support-priv']());
});
});
return $div;
}
return;
};
var getApi = function (cb) {
return function () {

View File

@ -34,13 +34,13 @@ define([
const items = {};
let blocks = sidebar.blocks = {};
blocks.labelledInput = (label, input) => {
blocks.labelledInput = (label, input, inputBlock) => {
let uid = Util.uid();
let id = `cp-${app}-item-${uid}`;
input.setAttribute('id', id);
return [
h('label', { for: id }, label),
input,
inputBlock || input,
];
};
blocks.button = (type, icon, text) => {
@ -63,6 +63,16 @@ define([
blocks.input = (attr) => {
return h('input', attr);
};
blocks.inputButton = (input, button, opts) => {
if (opts.onEnterDelegate) {
$(input).on('keypress', e => {
if (e.which === 13) {
$(button).click();
}
});
}
return h('div.cp-sidebar-input-block', [input, button]);
};
blocks.text = (value) => {
return h('span', value);
};

View File

@ -647,6 +647,45 @@ define([
});
};
var searchAdmin = (ctx, data, cId, cb) => {
if (!ctx.adminRdyEvt) { return void cb({ error: 'EFORBIDDEN' }); }
let tags = data.tags || [];
let text = (data.text || '').toLowerCase();
console.error(tags, text);
ctx.adminRdyEvt.reg(() => {
let doc = ctx.adminDoc.proxy;
let t = doc.tickets;
let all = {};
let add = (id, data, category) => {
let clone = Util.clone(data);
clone.category = category;
all[id] = clone;
};
['active', 'pending', 'closed'].some(cat => {
let tickets = t[cat];
return Object.keys(tickets).some(id => {
let ticket = tickets[id];
// Check if this ticket uses at least one selected tag
let hasTag = !tags.length || (ticket.tags || []).some(tag => {
return tags.includes(tag);
});
if (!hasTag) { return; }
// Filter by searched text within these tags
let id64 = Util.hexToBase64(id).slice(0,10);
if (text === id64) { // If ticket ID found, stop search and return result
all = {};
add(id, ticket, cat);
return true;
}
let hasText = !text || ticket.title.toLowerCase().includes(text);
if (!hasText) { return; }
add(id, ticket, cat);
});
});
cb({ tickets: all });
});
};
var setTagsAdmin = (ctx, data, cId, cb) => {
if (!ctx.adminRdyEvt) { return void cb({ error: 'EFORBIDDEN' }); }
ctx.adminRdyEvt.reg(() => {
@ -1284,6 +1323,9 @@ define([
if (cmd === 'USE_RECORDED') {
return void useRecorded(ctx, data, clientId, cb);
}
if (cmd === 'SEARCH_ADMIN') {
return void searchAdmin(ctx, data, clientId, cb);
}
if (cmd === 'FILTER_TAGS_ADMIN') {
return void filterTagsAdmin(ctx, data, clientId, cb);
}

View File

@ -78,5 +78,10 @@
}
}
}
.cp-support-search-container {
display: flex;
flex-flow: column;
}
}

View File

@ -55,6 +55,9 @@ define([
// XXX
Messages.support_pending = "Pending tickets:";
Messages.support_pending_tag = "Pending";
Messages.support_active_tag = "Active";
Messages.support_closed_tag = "Closed";
/*
Messages.support_activeListTitle = "Active tickets";
Messages.support_pendingListTitle = "Pending tickets";
@ -89,6 +92,8 @@ define([
Messages.support_legacyDump = "Export all";
Messages.support_legacyClear = "Delete from my account";
Messages.support_searchLabel = "Search (title or ticketId)";
var andThen = function (common, $container, linkedTicket) {
const sidebar = Sidebar.create(common, 'support', $container);
const blocks = sidebar.blocks;
@ -96,6 +101,42 @@ define([
APP.allTags = [];
APP.openTicketCategory = Util.mkEvent();
var sortTicket = tickets => (c1, c2) => {
return tickets[c2].time - tickets[c1].time;
};
const onShowTicket = function (ticket, channel, data, done) {
APP.module.execCommand('LOAD_TICKET_ADMIN', {
channel: channel,
curvePublic: data.authorKey,
supportKey: data.supportKey
}, function (obj) {
if (!Array.isArray(obj)) {
console.error(obj && obj.error);
done(false);
return void UI.warn(Messages.error);
}
var $ticket = $(ticket);
obj.forEach(function (msg) {
// Only add notifications channel if this is coming from the other user
if (!data.notifications && msg.sender.drive) {
data.notifications = Util.find(msg, ['sender', 'notifications']);
}
if (msg.close) {
$ticket.addClass('cp-support-list-closed');
return $ticket.append(APP.support.makeCloseMessage(msg));
}
if (msg.legacy && msg.messages) {
msg.messages.forEach(c => {
$ticket.append(APP.support.makeMessage(c));
});
return;
}
$ticket.append(APP.support.makeMessage(msg));
});
done(true);
});
};
// Support panel functions
let open = [];
let refreshAll = function () {};
@ -154,40 +195,12 @@ define([
col1 = col2 = col3 = col5;
}
$container.append([col1, col2, col3]);
var sortTicket = function (c1, c2) {
return tickets[c2].time - tickets[c1].time;
};
const onShow = function (ticket, channel, data, done) {
APP.module.execCommand('LOAD_TICKET_ADMIN', {
channel: channel,
curvePublic: data.authorKey,
supportKey: data.supportKey
}, function (obj) {
if (!Array.isArray(obj)) {
console.error(obj && obj.error);
done();
return void UI.warn(Messages.error);
onShowTicket(ticket, channel, data, (success) => {
if (success) {
if (!open.includes(channel)) { open.push(channel); }
}
var $ticket = $(ticket);
obj.forEach(function (msg) {
// Only add notifications channel if this is coming from the other user
if (!data.notifications && msg.sender.drive) {
data.notifications = Util.find(msg, ['sender', 'notifications']);
}
if (msg.close) {
$ticket.addClass('cp-support-list-closed');
return $ticket.append(APP.support.makeCloseMessage(msg));
}
if (msg.legacy && msg.messages) {
msg.messages.forEach(c => {
$ticket.append(APP.support.makeMessage(c));
});
return;
}
$ticket.append(APP.support.makeMessage(msg));
});
if (!open.includes(channel)) { open.push(channel); }
done();
});
};
@ -272,7 +285,7 @@ define([
// Show tickets, reload the previously open ones and cal back
// once everything is loaded
let n = nThen;
Object.keys(tickets).sort(sortTicket).forEach(function (channel) {
Object.keys(tickets).sort(sortTicket(tickets)).forEach(function (channel) {
// Update allTags
var d = tickets[channel];
(d.tags || []).forEach(tag => {
@ -386,12 +399,30 @@ define([
'closed-list'
]
},
'search': {
icon: undefined,
content: [
'filter',
'search'
],
onOpen: () => {
APP.searchAutoRefresh = true;
setTimeout(() => {
$('.cp-support-search-input').focus();
});
}
},
'settings': {
icon: undefined,
content: [
'notifications',
'recorded'
]
],
onOpen: () => {
setTimeout(() => {
$('.cp-support-recorded-id').focus();
});
}
},
'ticket': {
icon: undefined,
@ -400,6 +431,9 @@ define([
],
onOpen: () => {
APP.openTicketCategory.fire();
setTimeout(() => {
$('.cp-support-newticket-paste').focus();
});
}
},
'legacy': {
@ -450,6 +484,81 @@ define([
}
});
sidebar.addItem('search', cb => {
let inputSearch = blocks.input({type:'text', class: 'cp-support-search-input'});
let button = blocks.button('primary', 'fa-search');
let inputBlock = blocks.inputButton(inputSearch, button, { onEnterDelegate: true });
let searchBlock = blocks.labelledInput(Messages.support_searchLabel,
inputSearch, inputBlock);
let list = blocks.box([], 'cp-support-container');
let container = blocks.box([searchBlock, list], 'cp-support-search-container');
let $list = $(list);
let searchText = '';
APP.searchAutoRefresh = false;
let redraw = (_cb) => {
let cb = _cb || function () {};
$list.empty();
let tags = APP.filterTags || [];
let text = searchText;
if (!text.length && !tags.length) { return void cb(); }
APP.module.execCommand('SEARCH_ADMIN', { text, tags }, function (obj) {
cb();
if (obj && obj.error) {
console.error(obj && obj.error);
return void UI.warn(Messages.error);
}
$list.empty();
let tickets = obj.tickets || {};
const onShow = onShowTicket;
const onHide = function (ticket, channel, data, done) {
$(ticket).find('.cp-support-list-message').remove();
done();
};
const onTag = () => {};
onTag.readOnly = true;
onTag.getAllTags = () => [];
Object.keys(tickets).sort(sortTicket(tickets)).forEach(id => {
let content = tickets[id];
content.tags = content.tags || [];
let catTag = Messages[`support_${content.category}_tag`];
if (catTag) {
// Msg.support_active_tag.support_pending_tag.support_closed_tag
content.tags.unshift(catTag.toUpperCase());
}
var ticket = APP.support.makeTicket({
id,
content,
onTag, onShow, onHide
});
$list.append(ticket);
});
});
};
let $input = $(inputSearch);
let $button = $(button);
Util.onClickEnter($button, function () {
$button.prop('disabled', 'disabled');
searchText = $input.val().trim();
redraw(() => {
APP.searchAutoRefresh = true;
$button.prop('disabled', false);
});
});
events.REFRESH_FILTER.reg(() => {
if (!APP.searchAutoRefresh) { return; }
redraw();
});
cb(container);
}, { noTitle: true, noHint: true });
sidebar.addItem('filter', cb => {
let container = blocks.box([], 'cp-support-filter-container');
let $container = $(container);
@ -532,7 +641,8 @@ define([
Messages.kanban_delete
], []);
let labelledList = blocks.labelledInput(Messages.support_recordedList, list);
let inputId = blocks.input({type:'text', maxlength: 20 });
let inputId = blocks.input({type:'text', class: 'cp-support-recorded-id',
maxlength: 20 });
let inputContent = blocks.textArea();
let labelId = blocks.labelledInput(Messages.support_recordedId, inputId);
let labelContent = blocks.labelledInput(Messages.support_recordedContent, inputContent);
@ -637,6 +747,7 @@ define([
let reset = blocks.button('danger-alt', 'fa-times', Messages.form_reset);
let paste = blocks.textArea({
class: 'cp-support-newticket-paste',
placeholder: Messages.support_pasteUserData
});
let inputs = h('div.cp-moderation-userdata-inputs', [ labelName, labelChan, labelKey ]);
@ -817,6 +928,7 @@ Attachments:${JSON.stringify(msg.attachments, 0, 2)}`;
});
};
if (!$ticket.length) {
content.category = 'closed'; // Consider ticket closed
$ticket = APP.support.makeTicket({id, content, onMove});
$div.append($ticket);
}

View File

@ -421,6 +421,7 @@ define([
if (!err) { UI.log(Messages.shareSuccess); }
});
});
if (content.category === 'closed') { url = undefined; }
// Load & open ticket
@ -448,6 +449,7 @@ define([
});
};
Util.onClickEnter($show, adminOpen);
if (!onShow) { show = undefined; }
// Move active/pending
let move;
@ -465,6 +467,7 @@ define([
tag = h('button.btn.btn-secondary.fa.fa-tags', {
title: Messages.fm_tagsName
});
if (onTag.readOnly) { tag = undefined; }
tagsContainer = h('div');
tagsList = h('div.cp-tags-list');
let $list = $(tagsList);
@ -475,6 +478,7 @@ define([
});
};
redrawTags(content.tags);
console.error(content.tags);
let $tags = $(tagsContainer).hide();
let input = UI.dialog.textInput({id: `cp-${Util.uid()}`});
@ -538,6 +542,7 @@ define([
$(close).remove();
onClose(ticket, id, content);
});
if (!onClose) { $(close).remove(); }
UI.confirmButton(remove, {
classes: 'btn-danger'
@ -564,7 +569,8 @@ define([
$ticket.append(newForm);
};
if (form) { addForm(); }
$(answer).click(addForm);
Util.onClickEnter($(answer), addForm);
if (!onReply) { $(answer).remove(); }
return ticket;
};