public/src/client/flags/list.js
import {
Chart,
LineController,
CategoryScale,
LinearScale,
LineElement,
PointElement,
Tooltip,
Filler,
} from 'chart.js';
import * as categoryFilter from '../../modules/categoryFilter';
import * as userFilter from '../../modules/userFilter';
import * as autocomplete from '../../modules/autocomplete';
import * as api from '../../modules/api';
import * as alerts from '../../modules/alerts';
import * as components from '../../modules/components';
Chart.register(LineController, CategoryScale, LinearScale, LineElement, PointElement, Tooltip, Filler);
const selected = new Map([
['cids', []],
['assignee', []],
['targetUid', []],
['reporterId', []],
]);
export function init() {
enableFilterForm();
enableCheckboxes();
handleBulkActions();
if (ajaxify.data.filters.hasOwnProperty('cid')) {
selected.set('cids', Array.isArray(ajaxify.data.filters.cid) ?
ajaxify.data.filters.cid : [ajaxify.data.filters.cid]);
}
categoryFilter.init($('[component="category/dropdown"]'), {
privilege: 'moderate',
selectedCids: selected.get('cids'),
updateButton: function ({ selectedCids: cids }) {
selected.set('cids', cids);
applyFilters();
},
});
['assignee', 'targetUid', 'reporterId'].forEach((filter) => {
if (ajaxify.data.filters.hasOwnProperty('filter')) {
selected.set(filter, ajaxify.data.selected[filter]);
}
const filterEl = $(`[component="flags/filter/${filter}"]`);
userFilter.init(filterEl, {
selectedUsers: selected.get(filter),
template: 'partials/flags/filters',
selectedBlock: `selected.${filter}`,
onSelect: function (_selectedUsers) {
selected.set(filter, _selectedUsers);
},
onHidden: function () {
applyFilters();
},
});
});
components.get('flags/list')
.on('click', '[data-flag-id]', function (e) {
if (['BUTTON', 'A'].includes(e.target.nodeName)) {
return;
}
const flagId = this.getAttribute('data-flag-id');
ajaxify.go('flags/' + flagId);
});
$('#flags-daily-wrapper').one('shown.bs.collapse', function () {
handleGraphs();
});
autocomplete.user($('#filter-assignee, #filter-targetUid, #filter-reporterId'), (ev, ui) => {
setTimeout(() => { ev.target.value = ui.item.user.uid; });
});
}
export function enableFilterForm() {
const $filtersEl = components.get('flags/filters');
if ($filtersEl && $filtersEl.get(0).nodeName !== 'FORM') {
// Harmony; update hidden form and submit on change
const filtersEl = $filtersEl.get(0);
const formEl = filtersEl.querySelector('form');
filtersEl.addEventListener('click', (e) => {
const subselector = e.target.closest('[data-value]');
if (!subselector) {
return;
}
const name = subselector.getAttribute('data-name');
const value = subselector.getAttribute('data-value');
formEl[name].value = value;
applyFilters();
});
} else {
// Persona; parse ajaxify data to set form values to reflect current filters
for (const filter in ajaxify.data.filters) {
if (ajaxify.data.filters.hasOwnProperty(filter)) {
$filtersEl.find('[name="' + filter + '"]').val(ajaxify.data.filters[filter]);
}
}
$filtersEl.find('[name="sort"]').val(ajaxify.data.sort);
document.getElementById('apply-filters').addEventListener('click', function () {
applyFilters();
});
$filtersEl.find('button[data-target="#more-filters"]').click((ev) => {
const textVariant = ev.target.getAttribute('data-text-variant');
if (!textVariant) {
return;
}
ev.target.setAttribute('data-text-variant', ev.target.textContent);
ev.target.firstChild.textContent = textVariant;
});
}
}
function applyFilters() {
let formEl = components.get('flags/filters').get(0);
if (!formEl) {
return;
}
if (formEl.nodeName !== 'FORM') {
formEl = formEl.querySelector('form');
}
const payload = new FormData(formEl);
// cid is special comes from categoryFilter module
selected.get('cids').forEach(function (cid) {
payload.append('cid', cid);
});
// these three fields are special; comes from userFilter module
['assignee', 'targetUid', 'reporterId'].forEach((filter) => {
selected.get(filter).forEach(({ uid }) => {
payload.append(filter, uid);
});
});
const length = Array.from(payload.values()).filter(Boolean);
const qs = new URLSearchParams(payload).toString();
ajaxify.go('flags?' + (length ? qs : 'reset=1'));
}
export function enableCheckboxes() {
const flagsList = document.querySelector('[component="flags/list"]');
const checkboxes = flagsList.querySelectorAll('[data-flag-id] input[type="checkbox"]');
const bulkEl = document.querySelector('[component="flags/bulk-actions"] button');
let lastClicked;
document.querySelector('[data-action="toggle-all"]').addEventListener('click', function () {
const state = this.checked;
checkboxes.forEach(function (el) {
el.checked = state;
});
bulkEl.disabled = !state;
});
flagsList.addEventListener('click', function (e) {
const subselector = e.target.closest('input[type="checkbox"]');
if (subselector) {
// Stop checkbox clicks from going into the flag details
e.stopImmediatePropagation();
if (lastClicked && e.shiftKey && lastClicked !== subselector) {
// Select all the checkboxes in between
const state = subselector.checked;
let started = false;
checkboxes.forEach(function (el) {
if ([subselector, lastClicked].some(function (ref) {
return ref === el;
})) {
started = !started;
}
if (started) {
el.checked = state;
}
});
}
// (De)activate bulk actions button based on checkboxes' state
bulkEl.disabled = !Array.prototype.some.call(checkboxes, function (el) {
return el.checked;
});
lastClicked = subselector;
}
// If you miss the checkbox, don't descend into the flag details, either
if (e.target.querySelector('input[type="checkbox"]')) {
e.stopImmediatePropagation();
}
});
}
export function handleBulkActions() {
document.querySelector('[component="flags/bulk-actions"]').addEventListener('click', function (e) {
const subselector = e.target.closest('[data-action]');
if (subselector) {
const action = subselector.getAttribute('data-action');
const flagIds = getSelected();
const promises = flagIds.map((flagId) => {
const data = {};
if (action === 'bulk-assign') {
data.assignee = app.user.uid;
} else if (action === 'bulk-mark-resolved') {
data.state = 'resolved';
}
return api.put(`/flags/${flagId}`, data);
});
Promise.allSettled(promises).then(function (results) {
const fulfilled = results.filter(function (res) {
return res.status === 'fulfilled';
}).length;
const errors = results.filter(function (res) {
return res.status === 'rejected';
});
if (fulfilled) {
alerts.success('[[flags:bulk-success, ' + fulfilled + ']]');
ajaxify.refresh();
}
errors.forEach(function (res) {
alerts.error(res.reason);
});
});
}
});
}
export function getSelected() {
const checkboxes = document.querySelectorAll('[component="flags/list"] [data-flag-id] input[type="checkbox"]');
const payload = [];
checkboxes.forEach(function (el) {
if (el.checked) {
payload.push(el.closest('[data-flag-id]').getAttribute('data-flag-id'));
}
});
return payload;
}
export function handleGraphs() {
const dailyCanvas = document.getElementById('flags:daily');
const dailyLabels = utils.getDaysArray().map(function (text, idx) {
return idx % 3 ? '' : text;
});
if (utils.isMobile()) {
Chart.defaults.plugins.tooltip.enabled = false;
}
const data = {
'flags:daily': {
labels: dailyLabels,
datasets: [
{
label: '',
backgroundColor: 'rgba(151,187,205,0.2)',
borderColor: 'rgba(151,187,205,1)',
pointBackgroundColor: 'rgba(151,187,205,1)',
pointHoverBackgroundColor: '#fff',
pointBorderColor: '#fff',
pointHoverBorderColor: 'rgba(151,187,205,1)',
data: ajaxify.data.analytics,
},
],
},
};
dailyCanvas.width = $(dailyCanvas).parent().width();
new Chart(dailyCanvas.getContext('2d'), {
type: 'line',
data: data['flags:daily'],
options: {
responsive: true,
animation: false,
scales: {
y: {
beginAtZero: true,
},
},
},
});
}