feat: allow re-ordering of auto-categorization rules

This commit is contained in:
Julian Lam
2026-02-11 12:53:59 -05:00
parent 78d7130c7a
commit fd43368a92
5 changed files with 67 additions and 21 deletions

View File

@@ -7,6 +7,8 @@ define('admin/settings/activitypub', [
'api',
'alerts',
'translator',
'jquery-ui/widgets/sortable',
], function (Benchpress, bootbox, categorySelector, api, alerts, translator) {
const ActivityPub = {};
@@ -106,30 +108,55 @@ define('admin/settings/activitypub', [
function setupRules() {
const rulesEl = document.getElementById('rules');
if (rulesEl) {
rulesEl.addEventListener('click', (e) => {
const subselector = e.target.closest('[data-action]');
if (subselector) {
const action = subselector.getAttribute('data-action');
switch (action) {
case 'rules.add': {
ActivityPub.throwRulesModal();
break;
}
if (!rulesEl) {
return;
}
case 'rules.delete': {
const rid = subselector.closest('tr').getAttribute('data-rid');
api.del(`/admin/activitypub/rules/${rid}`, {}).then(async (data) => {
const html = await Benchpress.render('admin/settings/activitypub', { rules: data }, 'rules');
const tbodyEl = document.querySelector('#rules tbody');
if (tbodyEl) {
tbodyEl.innerHTML = html;
}
}).catch(alerts.error);
}
rulesEl.addEventListener('click', (e) => {
const subselector = e.target.closest('[data-action]');
if (subselector) {
const action = subselector.getAttribute('data-action');
switch (action) {
case 'rules.add': {
ActivityPub.throwRulesModal();
break;
}
case 'rules.delete': {
const rid = subselector.closest('tr').getAttribute('data-rid');
api.del(`/admin/activitypub/rules/${rid}`, {}).then(async (data) => {
const html = await Benchpress.render('admin/settings/activitypub', { rules: data }, 'rules');
const tbodyEl = document.querySelector('#rules tbody');
if (tbodyEl) {
tbodyEl.innerHTML = html;
}
}).catch(alerts.error);
}
}
}
});
const tbodyEl = $(rulesEl).find('tbody');
tbodyEl.sortable({
handle: '.drag-handle',
helper: fixWidthHelper,
placeholder: 'ui-state-highlight',
axis: 'y',
update: function () {
var rids = [];
tbodyEl.find('tr').each(function () {
rids.push($(this).data('rid'));
});
api.put('/admin/activitypub/rules/order', { rids }).catch(alerts.error);
},
});
function fixWidthHelper(e, ui) {
ui.children().each(function () {
$(this).width($(this).width());
});
return ui;
}
}

View File

@@ -41,4 +41,12 @@ Rules.delete = async (rid) => {
db.sortedSetRemove('categorization:rid', rid),
db.delete(`rid:${rid}`),
]);
};
Rules.reorder = async (rids) => {
const exists = await db.isSortedSetMembers('categorization:rid', rids);
rids = rids.filter((_, idx) => exists[idx]);
const scores = Array.from({ length: rids.length }, (_, idx) => idx);
await db.sortedSetAdd('categorization:rid', scores, rids);
};

View File

@@ -104,6 +104,12 @@ Admin.activitypub.deleteRule = async (req, res) => {
helpers.formatApiResponse(200, res, await activitypub.rules.list());
};
Admin.activitypub.reorderRules = async (req, res) => {
const { rids } = req.body;
await activitypub.rules.reorder(rids);
helpers.formatApiResponse(200, res, await activitypub.rules.list());
};
Admin.activitypub.addRelay = async (req, res) => {
const { url } = req.body;

View File

@@ -27,6 +27,7 @@ module.exports = function () {
setupApiRoute(router, 'post', '/activitypub/rules', [...middlewares, middleware.checkRequired.bind(null, ['cid', 'value', 'type'])], controllers.write.admin.activitypub.addRule);
setupApiRoute(router, 'delete', '/activitypub/rules/:rid', [...middlewares], controllers.write.admin.activitypub.deleteRule);
setupApiRoute(router, 'put', '/activitypub/rules/order', [...middlewares, middleware.checkRequired.bind(null, ['rids'])], controllers.write.admin.activitypub.reorderRules);
setupApiRoute(router, 'post', '/activitypub/relays', [...middlewares, middleware.checkRequired.bind(null, ['url'])], controllers.write.admin.activitypub.addRelay);
setupApiRoute(router, 'delete', '/activitypub/relays/:url', [...middlewares], controllers.write.admin.activitypub.removeRelay);

View File

@@ -51,14 +51,18 @@
<p>[[admin/settings/activitypub:rules-intro]]</p>
<table class="table table-striped" id="rules">
<thead>
<th></th>
<th>[[admin/settings/activitypub:rules.type]]</th>
<th>[[admin/settings/activitypub:rules.value]]</th>
<th>[[admin/settings/activitypub:rules.cid]]</th>
<th></th>
</thead>
<tbody>
<tbody class="sortable-container">
{{{ each rules }}}
<tr data-rid="{./rid}">
<td class="align-items-center" style="cursor: move;">
<i class="fa fa-grip-lines text-muted drag-handle"></i>
</td>
<td>{./type}</td>
<td>{./value}</td>
<td>{./cid}</td>