feat(access): Фаза 2c (часть) — массовые операции в матрице доступа

Клик по названию контента в матрице открывает/закрывает его сразу ВСЕМ классам;
клик по имени класса (заголовок столбца) — открывает/закрывает ВЕСЬ контент этому
классу. Массовое закрытие спрашивает подтверждение; перерисовывается только tbody.
Использует существующий accSetRule (без новых эндпоинтов).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-03 13:36:34 +03:00
parent 9b7585ac7b
commit d1f24736c3
+44 -2
View File
@@ -396,7 +396,10 @@
/* ════════ режим «Матрица» (класс × контент одним экраном) ════════ */
function matrixHeadCells(classes) {
return classes.map(c =>
`<th style="padding:6px 8px;font-size:11.5px;font-weight:600;color:var(--text-3);white-space:nowrap;border-bottom:1px solid var(--border)">${esc(c.name)}</th>`).join('');
`<th style="padding:6px 8px;border-bottom:1px solid var(--border)">
<button onclick="accMxColBulk(${c.id})" title="Открыть/закрыть весь контент классу «${esc(c.name)}»"
style="border:none;background:transparent;cursor:pointer;font-family:inherit;font-size:11.5px;font-weight:600;color:var(--text-3);white-space:nowrap;padding:0">${esc(c.name)}</button>
</th>`).join('');
}
function matrixBody() {
const classes = _matrix.classes || [];
@@ -410,7 +413,10 @@
return `<td style="text-align:center;border-bottom:1px solid var(--border-soft,#f0f0f0)">
<input type="checkbox" ${open ? 'checked' : ''} onchange="accMx('${type}','${esc(ref)}',${c.id},this.checked)" title="${esc(c.name)} · ${esc(it.title)}"></td>`;
}).join('');
return `<tr><th scope="row" style="text-align:left;padding:6px 10px;font-size:13px;font-weight:500;color:var(--text-1);white-space:nowrap;position:sticky;left:0;background:var(--card,#fff);border-bottom:1px solid var(--border-soft,#f0f0f0)">${esc(it.title)}</th>${cells}</tr>`;
return `<tr><th scope="row" style="text-align:left;padding:6px 10px;white-space:nowrap;position:sticky;left:0;background:var(--card,#fff);border-bottom:1px solid var(--border-soft,#f0f0f0)">
<button onclick="accMxRowBulk('${type}','${esc(ref)}')" title="Открыть/закрыть «${esc(it.title)}» всем классам"
style="border:none;background:transparent;cursor:pointer;font-family:inherit;font-size:13px;font-weight:500;color:var(--text-1);text-align:left;padding:0">${esc(it.title)}</button>
</th>${cells}</tr>`;
}).join('');
if (!rows) return '';
return `<tr><th colspan="${classes.length + 1}" style="text-align:left;padding:10px 10px 4px;font-size:12px;font-weight:700;color:var(--text-3)">${TYPE_LABEL[type] || type}</th></tr>${rows}`;
@@ -453,6 +459,40 @@
}
function mxSearch(v) { _mSearch = v; const b = document.getElementById('acc-mx-body'); if (b) b.innerHTML = matrixBody(); }
function mxRepaint() { const b = document.getElementById('acc-mx-body'); if (b) b.innerHTML = matrixBody(); }
function mxApply(o, type, ref, open) {
const arr = o[type] || (o[type] = []);
const i = arr.indexOf(ref);
if (open && i < 0) arr.push(ref);
if (!open && i >= 0) arr.splice(i, 1);
}
/* строка матрицы: открыть/закрыть один контент всем классам */
async function mxRowBulk(type, ref) {
const classes = _matrix.classes || [];
const allOpen = classes.length && classes.every(c => ((_matrix.open[c.id] || {})[type] || []).includes(ref));
const open = !allOpen;
if (!open && !confirm(`Закрыть «${contentTitle(type, ref)}» у всех классов?`)) return;
try {
await Promise.all(classes.map(c => LS.accessSetRule(type, ref, 'class', c.id, open ? 1 : null)));
classes.forEach(c => mxApply(_matrix.open[c.id] || (_matrix.open[c.id] = {}), type, ref, open));
mxRepaint();
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); _matrix = null; renderMatrix(); }
}
/* столбец матрицы: открыть/закрыть весь контент одному классу */
async function mxColBulk(classId) {
const items = CONTENT_TYPES.flatMap(t => itemsOf(t).map(it => [t, it[keyName(t)]]));
const o = _matrix.open[classId] || (_matrix.open[classId] = {});
const allOpen = items.length && items.every(([t, ref]) => (o[t] || []).includes(ref));
const open = !allOpen;
const cls = (_matrix.classes.find(c => c.id === classId) || {}).name || ('#' + classId);
if (!open && !confirm(`Закрыть весь контент у класса «${cls}»?`)) return;
try {
await Promise.all(items.map(([t, ref]) => LS.accessSetRule(t, ref, 'class', classId, open ? 1 : null)));
items.forEach(([t, ref]) => mxApply(o, t, ref, open));
mxRepaint();
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); _matrix = null; renderMatrix(); }
}
/* ── режим ── */
function setMode(m) {
if (m === _mode) return;
@@ -472,6 +512,8 @@
window.accClassBulk = classBulk;
window.accMx = mxToggle;
window.accMxSearch = mxSearch;
window.accMxRowBulk = mxRowBulk;
window.accMxColBulk = mxColBulk;
window.accLeftSearch = leftSearch;
window.AdminSections = window.AdminSections || {};