Grade comments: show live usage count and allow deleting unused templates

This commit is contained in:
2026-09-29 15:56:00 +03:00
parent 4bb1e332e3
commit 2869142a79
9 changed files with 187 additions and 12 deletions

View File

@@ -312,8 +312,13 @@ code.code { padding: 0.125rem 0.5rem; border-radius: 0.25rem; font-family: monos
background: var(--c-surface); border: 1px solid var(--c-border);
border-radius: 0.4rem; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
}
.ac-item { padding: 0.4rem 0.6rem; cursor: pointer; font-size: 0.875rem; }
.ac-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0.6rem; cursor: pointer; font-size: 0.875rem; }
.ac-item:hover, .ac-item.active { background: var(--c-bg); }
.ac-text { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ac-count { flex: 0 0 auto; background: var(--c-border); color: var(--c-text-muted); }
.ac-del { flex: 0 0 auto; border: none; background: transparent; color: var(--c-text-muted); cursor: pointer; padding: 0 0.25rem; border-radius: 0.25rem; font-size: 0.75rem; line-height: 1.4; }
.ac-del:not(:disabled):hover { color: #dc2626; background: var(--c-bg); }
.ac-del:disabled { opacity: 0.3; cursor: not-allowed; }
.ac-empty { padding: 0.4rem 0.6rem; font-size: 0.875rem; color: var(--c-text-muted); }
/* Ячейка матрицы: вариант и кнопки действий под ним; кнопки в шапке ЛР */
.cell-stack { display: flex; flex-direction: column; align-items: center; gap: 0.3rem; }

View File

@@ -100,10 +100,42 @@ function attachCommentAutocomplete(input) {
}
list.innerHTML = '';
items.forEach(function (item) {
var count = parseInt(item.count || 0, 10) || 0;
var row = document.createElement('div');
row.className = 'ac-item';
row.textContent = item.text;
var text = document.createElement('span');
text.className = 'ac-text';
text.textContent = item.text;
row.appendChild(text);
var badge = document.createElement('span');
badge.className = 'badge badge-xs ac-count';
badge.textContent = count;
badge.title = 'Использований: ' + count;
row.appendChild(badge);
var del = document.createElement('button');
del.type = 'button';
del.className = 'ac-del';
del.textContent = '✕';
del.disabled = count > 0;
del.title = count > 0
? 'Нельзя удалить: комментарий используется ' + count + ' раз(а)'
: 'Удалить комментарий из подсказок';
del.addEventListener('mousedown', function (event) {
event.preventDefault();
event.stopPropagation();
});
del.addEventListener('click', function (event) {
event.stopPropagation();
if (count > 0) return;
removeCommentTemplate(item.text, load);
});
row.appendChild(del);
row.addEventListener('mousedown', function (event) {
if (event.target.closest('.ac-del')) return;
event.preventDefault();
input.value = item.text;
var tone = input.parentNode.querySelector('.cs-tone');
@@ -146,8 +178,54 @@ function attachCommentAutocomplete(input) {
});
}
/* ---------- Индивидуальное задание (защита) ---------- */
/* ---------- Удаление подсказки комментария и всплывающие уведомления ---------- */
function showToast(text, kind) {
var host = $id('toast-host');
if (!host) return;
var toast = document.createElement('div');
toast.className = 'toast-item ' + (kind === 'error' ? 'toast-error' : 'toast-success');
toast.setAttribute('data-toast', '');
var span = document.createElement('span');
span.className = 'toast-text';
span.textContent = text;
toast.appendChild(span);
var close = document.createElement('button');
close.type = 'button';
close.className = 'toast-close';
close.textContent = '✕';
close.addEventListener('click', function () { toast.remove(); });
toast.appendChild(close);
host.appendChild(toast);
setTimeout(function () { toast.remove(); }, 5000);
}
function removeCommentTemplate(text, done) {
var form = $id('grade-form');
var csrf = form ? form.querySelector('input[name="_csrf"]') : null;
var body = '_csrf=' + encodeURIComponent(csrf ? csrf.value : '') +
'&text=' + encodeURIComponent(text);
fetch('/admin/comment-templates/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body
})
.then(function (r) { return r.ok ? r.json() : { ok: false }; })
.then(function (res) {
if (res && res.ok) {
showToast('Комментарий удалён из подсказок.');
if (done) done();
} else {
showToast((res && res.error) || 'Не удалось удалить комментарий.', 'error');
}
})
.catch(function () { showToast('Не удалось удалить комментарий.', 'error'); });
}
/* ---------- Индивидуальное задание (защита) ---------- */
function defenseStatusText(status, code, seconds) {
if (status === 'waiting') {
return code