Table columns: dropdown/text filters per column
- tablekit builds a filter row under the headers for every th with data-filter; distinct cell values become a select (label from data-filter-label), or a text substring filter with data-filter="text" - filters combine with the global table search; placeholder rows (colspan) are not filtered away; filter row follows the lab-focus column hiding - enabled on: matrix lab columns (by variant), defense table (ЛР, status with readable values), students (Основа, Пароль), lab assignment lists (Уровень, Источник, Сдача), labs list (Статус)
This commit is contained in:
@@ -338,3 +338,10 @@ th.th-sortable.is-desc::after { content: " ▼"; color: var(--c-text-muted); fon
|
|||||||
.row-hidden { display: none !important; }
|
.row-hidden { display: none !important; }
|
||||||
.table-search { width: 14rem; }
|
.table-search { width: 14rem; }
|
||||||
@media (max-width: 640px) { .table-search { width: 100%; } }
|
@media (max-width: 640px) { .table-search { width: 100%; } }
|
||||||
|
.filter-row th { padding: 0.25rem 0.35rem; background: var(--c-bg); }
|
||||||
|
.filter-control {
|
||||||
|
width: 100%; min-width: 3.5rem; font-size: 0.72rem; line-height: 1.4;
|
||||||
|
padding: 0.1rem 0.25rem; border: 1px solid var(--c-border); border-radius: 0.35rem;
|
||||||
|
background: var(--c-surface); color: var(--c-text); font-family: inherit;
|
||||||
|
}
|
||||||
|
th.th-filterable { padding-bottom: 0.35rem; }
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ function focusLab() {
|
|||||||
var s = $id('lab-focus');
|
var s = $id('lab-focus');
|
||||||
if (!s) return;
|
if (!s) return;
|
||||||
var labId = s.value;
|
var labId = s.value;
|
||||||
document.querySelectorAll('th.lab-col[data-lab], td.cell-lab[data-lab]').forEach(function (el) {
|
document.querySelectorAll('th.lab-col[data-lab], td.cell-lab[data-lab], .filter-row th[data-lab]').forEach(function (el) {
|
||||||
el.style.display = (!labId || el.getAttribute('data-lab') === labId) ? '' : 'none';
|
el.style.display = (!labId || el.getAttribute('data-lab') === labId) ? '' : 'none';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,48 @@
|
|||||||
/* tablekit: сортировка по клику на заголовок + живой поиск по строке.
|
/* tablekit: сортировка по клику на заголовок, поиск по таблице и фильтры по колонкам.
|
||||||
Таблицу размечают: <table class="tablekit" id="...">, поле поиска
|
Разметка:
|
||||||
<input class="table-search" data-target="id-таблицы">.
|
<table class="tablekit" id="t"> — таблица;
|
||||||
Начальная сортировка (ФИО) выполняется на сервере; клик по заголовку меняет её. */
|
<input class="table-search" data-target="t"> — поиск по строке;
|
||||||
|
<th data-filter> — включить фильтр по колонке (выпадающий список значений);
|
||||||
|
<th data-filter="text"> — текстовый фильтр по колонке;
|
||||||
|
<th data-nosort> — колонка не сортируется;
|
||||||
|
data-sort / data-filter-value / data-filter-label на <td> — значения и подписи.
|
||||||
|
Начальная сортировка выполняется на сервере; клик по заголовку меняет её. */
|
||||||
(function () {
|
(function () {
|
||||||
function normalize(value) {
|
function normalize(value) {
|
||||||
return (value || '').trim().toLowerCase().replace(/\s+/g, ' ');
|
return (value == null ? '' : String(value)).trim().toLowerCase().replace(/\s+/g, ' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
function cellValue(cell) {
|
function source(cell) {
|
||||||
if (!cell) return '';
|
if (!cell) return '';
|
||||||
if (cell.dataset && cell.dataset.sort !== undefined) return normalize(cell.dataset.sort);
|
if (cell.dataset.filterValue !== undefined) return normalize(cell.dataset.filterValue);
|
||||||
|
if (cell.dataset.sort !== undefined) return normalize(cell.dataset.sort);
|
||||||
return normalize(cell.textContent);
|
return normalize(cell.textContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function label(cell) {
|
||||||
|
if (!cell) return '';
|
||||||
|
if (cell.dataset.filterLabel !== undefined) return cell.dataset.filterLabel.trim();
|
||||||
|
var text = (cell.textContent || '').trim().replace(/\s+/g, ' ');
|
||||||
|
return text || source(cell);
|
||||||
|
}
|
||||||
|
|
||||||
function compared(a, b) {
|
function compared(a, b) {
|
||||||
var na = a.match(/-?\d+([.,]\d+)?/g);
|
var ma = (a.match(/-?\d+([.,]\d+)?/) || [''])[0].replace(',', '.');
|
||||||
var nb = b.match(/-?\d+([.,]\d+)?/g);
|
var mb = (b.match(/-?\d+([.,]\d+)?/) || [''])[0].replace(',', '.');
|
||||||
var fa = na ? parseFloat(na[0].replace(',', '.')) : NaN;
|
var fa = parseFloat(ma);
|
||||||
var fb = nb ? parseFloat(nb[0].replace(',', '.')) : NaN;
|
var fb = parseFloat(mb);
|
||||||
if (!isNaN(fa) && !isNaN(fb) && fa !== fb) return fa - fb;
|
if (!isNaN(fa) && !isNaN(fb) && fa !== fb) return fa - fb;
|
||||||
return a.localeCompare(b, 'ru', { numeric: true, sensitivity: 'base' });
|
return a.localeCompare(b, 'ru', { numeric: true, sensitivity: 'base' });
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearMarkers(table, exceptColumn) {
|
function headerRow(table) {
|
||||||
var cells = table.tHead.rows[table.tHead.rows.length - 1].cells;
|
return table.__headerRow || table.tHead.rows[table.tHead.rows.length - 1];
|
||||||
for (var i = 0; i < cells.length; i++) {
|
}
|
||||||
if (i === exceptColumn) continue;
|
|
||||||
cells[i].classList.remove('is-asc', 'is-desc');
|
function clearMarkers(table, keep) {
|
||||||
}
|
Array.prototype.forEach.call(headerRow(table).cells, function (th, i) {
|
||||||
|
if (i !== keep) th.classList.remove('is-asc', 'is-desc');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortColumn(table, index, direction) {
|
function sortColumn(table, index, direction) {
|
||||||
@@ -35,22 +50,114 @@
|
|||||||
if (!body) return;
|
if (!body) return;
|
||||||
var rows = Array.prototype.slice.call(body.rows);
|
var rows = Array.prototype.slice.call(body.rows);
|
||||||
rows.sort(function (a, b) {
|
rows.sort(function (a, b) {
|
||||||
return compared(cellValue(a.cells[index]), cellValue(b.cells[index]));
|
return compared(source(a.cells[index]), source(b.cells[index]));
|
||||||
});
|
});
|
||||||
if (direction < 0) rows.reverse();
|
if (direction < 0) rows.reverse();
|
||||||
rows.forEach(function (row) { body.appendChild(row); });
|
rows.forEach(function (row) { body.appendChild(row); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyFilters(table) {
|
||||||
|
var body = table.tBodies[0];
|
||||||
|
if (!body) return;
|
||||||
|
var query = table.__query || '';
|
||||||
|
var filters = table.__filters || {};
|
||||||
|
var columnCount = headerRow(table).cells.length;
|
||||||
|
Array.prototype.forEach.call(body.rows, function (row) {
|
||||||
|
var placeholder = row.cells.length < columnCount;
|
||||||
|
var show = !query || normalize(row.textContent).indexOf(query) >= 0;
|
||||||
|
if (show && !placeholder) {
|
||||||
|
for (var key in filters) {
|
||||||
|
var filter = filters[key];
|
||||||
|
if (!filter || !filter.value) continue;
|
||||||
|
var value = source(row.cells[key]);
|
||||||
|
var match = filter.kind === 'text'
|
||||||
|
? value.indexOf(filter.value) >= 0
|
||||||
|
: value === filter.value;
|
||||||
|
if (!match) { show = false; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row.classList.toggle('row-hidden', !show);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFilters(table) {
|
||||||
|
var head = table.tHead;
|
||||||
|
var body = table.tBodies[0];
|
||||||
|
if (!head || !body) return;
|
||||||
|
var row = headerRow(table);
|
||||||
|
var columns = [];
|
||||||
|
Array.prototype.forEach.call(row.cells, function (th, index) {
|
||||||
|
if (th.hasAttribute('data-filter')) {
|
||||||
|
th.classList.add('th-filterable');
|
||||||
|
columns.push(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!columns.length) return;
|
||||||
|
|
||||||
|
var filterRow = document.createElement('tr');
|
||||||
|
filterRow.className = 'filter-row';
|
||||||
|
Array.prototype.forEach.call(row.cells, function (th, index) {
|
||||||
|
var cell = document.createElement('th');
|
||||||
|
if (th.dataset.lab) cell.setAttribute('data-lab', th.dataset.lab);
|
||||||
|
if (columns.indexOf(index) >= 0) {
|
||||||
|
var kind = th.getAttribute('data-filter') || 'values';
|
||||||
|
var control;
|
||||||
|
if (kind === 'text') {
|
||||||
|
control = document.createElement('input');
|
||||||
|
control.type = 'text';
|
||||||
|
control.placeholder = 'фильтр…';
|
||||||
|
} else {
|
||||||
|
control = document.createElement('select');
|
||||||
|
var any = document.createElement('option');
|
||||||
|
any.value = '';
|
||||||
|
any.textContent = 'все';
|
||||||
|
control.appendChild(any);
|
||||||
|
var seen = {};
|
||||||
|
var options = [];
|
||||||
|
Array.prototype.forEach.call(body.rows, function (tr) {
|
||||||
|
var td = tr.cells[index];
|
||||||
|
if (!td) return;
|
||||||
|
var value = source(td);
|
||||||
|
if (!value || seen[value]) return;
|
||||||
|
seen[value] = 1;
|
||||||
|
options.push({ value: value, label: label(td) });
|
||||||
|
});
|
||||||
|
options.sort(function (a, b) { return compared(a.label, b.label); });
|
||||||
|
options.forEach(function (option) {
|
||||||
|
var item = document.createElement('option');
|
||||||
|
item.value = option.value;
|
||||||
|
item.textContent = option.label;
|
||||||
|
control.appendChild(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
control.className = 'filter-control';
|
||||||
|
control.setAttribute('aria-label', (th.textContent || '').trim() + ' — фильтр');
|
||||||
|
control.addEventListener(kind === 'text' ? 'input' : 'change', function () {
|
||||||
|
table.__filters = table.__filters || {};
|
||||||
|
table.__filters[index] = {
|
||||||
|
kind: kind,
|
||||||
|
value: kind === 'text' ? normalize(control.value) : control.value,
|
||||||
|
};
|
||||||
|
applyFilters(table);
|
||||||
|
});
|
||||||
|
cell.appendChild(control);
|
||||||
|
}
|
||||||
|
filterRow.appendChild(cell);
|
||||||
|
});
|
||||||
|
head.appendChild(filterRow);
|
||||||
|
}
|
||||||
|
|
||||||
function bind(table) {
|
function bind(table) {
|
||||||
if (table.dataset.tablekitBound) return;
|
if (table.dataset.tablekitBound) return;
|
||||||
table.dataset.tablekitBound = '1';
|
table.dataset.tablekitBound = '1';
|
||||||
var head = table.tHead;
|
var head = table.tHead;
|
||||||
if (!head) return;
|
if (!head) return;
|
||||||
var cells = head.rows[head.rows.length - 1].cells;
|
var row = head.rows[head.rows.length - 1];
|
||||||
Array.prototype.forEach.call(cells, function (th, index) {
|
table.__headerRow = row;
|
||||||
|
Array.prototype.forEach.call(row.cells, function (th, index) {
|
||||||
if (th.hasAttribute('data-nosort')) return;
|
if (th.hasAttribute('data-nosort')) return;
|
||||||
th.classList.add('th-sortable');
|
th.classList.add('th-sortable');
|
||||||
th.title = th.title || 'Сортировать по этому столбцу';
|
if (!th.title) th.title = 'Сортировать по этому столбцу';
|
||||||
th.addEventListener('click', function (event) {
|
th.addEventListener('click', function (event) {
|
||||||
if (event.target.closest('form, button, a, select, input')) return;
|
if (event.target.closest('form, button, a, select, input')) return;
|
||||||
var asc = !th.classList.contains('is-asc');
|
var asc = !th.classList.contains('is-asc');
|
||||||
@@ -60,16 +167,15 @@
|
|||||||
th.title = (th.textContent.trim() || 'Столбец') + ' — ' + (asc ? 'по возрастанию' : 'по убыванию');
|
th.title = (th.textContent.trim() || 'Столбец') + ' — ' + (asc ? 'по возрастанию' : 'по убыванию');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
buildFilters(table);
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindSearch(input) {
|
function bindSearch(input) {
|
||||||
var table = document.getElementById(input.dataset.target);
|
var table = document.getElementById(input.dataset.target);
|
||||||
if (!table || !table.tBodies[0]) return;
|
if (!table || !table.tBodies[0]) return;
|
||||||
input.addEventListener('input', function () {
|
input.addEventListener('input', function () {
|
||||||
var query = normalize(input.value);
|
table.__query = normalize(input.value);
|
||||||
Array.prototype.forEach.call(table.tBodies[0].rows, function (row) {
|
applyFilters(table);
|
||||||
row.classList.toggle('row-hidden', !!query && normalize(row.textContent).indexOf(query) < 0);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="matrix-sticky th-sortable is-asc" title="ФИО — по возрастанию">Студент</th>
|
<th class="matrix-sticky th-sortable is-asc" title="ФИО — по возрастанию">Студент</th>
|
||||||
<th th:each="lab : ${labs}" class="text-center lab-col" th:attr="data-lab=${lab.id}">
|
<th th:each="lab : ${labs}" class="text-center lab-col" data-filter th:attr="data-lab=${lab.id}">
|
||||||
<div class="lab-col-head">
|
<div class="lab-col-head">
|
||||||
<div class="lab-head-row">
|
<div class="lab-head-row">
|
||||||
<a th:href="@{/admin/labs/{id}(id=${lab.id})}" th:text="|ЛР ${lab.number}|">ЛР</a>
|
<a th:href="@{/admin/labs/{id}(id=${lab.id})}" th:text="|ЛР ${lab.number}|">ЛР</a>
|
||||||
@@ -75,7 +75,8 @@
|
|||||||
<div class="font-medium" th:text="${row.fullName}">—</div>
|
<div class="font-medium" th:text="${row.fullName}">—</div>
|
||||||
<div class="text-sm text-muted font-mono" th:text="${row.studentNo}">—</div>
|
<div class="text-sm text-muted font-mono" th:text="${row.studentNo}">—</div>
|
||||||
</td>
|
</td>
|
||||||
<td th:each="cell, iterStat : ${row.cells}" class="text-center cell-lab" th:attr="data-lab=${cell.labId},data-sort=${cell.variantNumber}">
|
<td th:each="cell, iterStat : ${row.cells}" class="text-center cell-lab"
|
||||||
|
th:attr="data-lab=${cell.labId},data-sort=${cell.variantNumber},data-filter-label=${cell.variantNumber != null ? 'вариант ' + cell.variantNumber : 'без варианта'}">
|
||||||
<div class="cell-chips">
|
<div class="cell-chips">
|
||||||
<span class="cell-chip"
|
<span class="cell-chip"
|
||||||
th:title="(${cell.variantNumber} ? 'Вариант ' + ${cell.variantNumber} : 'Вариант') + ' — ' + (${cell.variantManual} ? 'закреплён вручную (раздача его не тронет)' : 'выдан автоматически')">
|
th:title="(${cell.variantNumber} ? 'Вариант ' + ${cell.variantNumber} : 'Вариант') + ' — ' + (${cell.variantManual} ? 'закреплён вручную (раздача его не тронет)' : 'выдан автоматически')">
|
||||||
@@ -121,7 +122,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card card-flush mb-8">
|
<div class="card card-flush mb-8">
|
||||||
<table id="defense-table" class="tablekit">
|
<table id="defense-table" class="tablekit">
|
||||||
<thead><tr><th class="th-sortable is-asc" title="ФИО — по возрастанию">Студент</th><th>ЛР</th><th>Задание</th><th>Статус</th><th class="text-right" data-nosort>Действия</th></tr></thead>
|
<thead><tr><th class="th-sortable is-asc" title="ФИО — по возрастанию">Студент</th><th data-filter>ЛР</th><th>Задание</th><th data-filter>Статус</th><th class="text-right" data-nosort>Действия</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr th:each="d : ${defenseRows}" th:attr="data-lab=${d.labId}">
|
<tr th:each="d : ${defenseRows}" th:attr="data-lab=${d.labId}">
|
||||||
<td>
|
<td>
|
||||||
@@ -130,7 +131,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="font-mono" th:text="|ЛР ${d.labNumber}|">—</td>
|
<td class="font-mono" th:text="|ЛР ${d.labNumber}|">—</td>
|
||||||
<td class="text-sm text-muted" th:text="${#strings.abbreviate(d.assignmentMd, 60)}">—</td>
|
<td class="text-sm text-muted" th:text="${#strings.abbreviate(d.assignmentMd, 60)}">—</td>
|
||||||
<td>
|
<td th:attr="data-filter-value=${d.status == 'waiting' ? (d.code != null ? 'код выдан' : 'ждёт кода') : (d.status == 'started' ? 'пишет' : (d.status == 'ready' ? 'готов' : d.status))}">
|
||||||
<span class="badge badge-warn" th:if="${d.status == 'waiting' and d.code == null}">ждёт кода</span>
|
<span class="badge badge-warn" th:if="${d.status == 'waiting' and d.code == null}">ждёт кода</span>
|
||||||
<span class="badge badge-warn" th:if="${d.status == 'waiting' and d.code != null}">код выдан</span>
|
<span class="badge badge-warn" th:if="${d.status == 'waiting' and d.code != null}">код выдан</span>
|
||||||
<span class="badge badge-warn" th:if="${d.status == 'started'}">пишет (<span th:text="${d.secondsLeft}"></span> c)</span>
|
<span class="badge badge-warn" th:if="${d.status == 'started'}">пишет (<span th:text="${d.secondsLeft}"></span> c)</span>
|
||||||
@@ -173,7 +174,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card card-flush">
|
<div class="card card-flush">
|
||||||
<table id="students-table" class="tablekit">
|
<table id="students-table" class="tablekit">
|
||||||
<thead><tr><th data-nosort>№</th><th class="th-sortable is-asc" title="ФИО — по возрастанию">ФИО</th><th>Логин</th><th>Основа</th><th>Варианты</th><th>Пароль</th><th class="text-right" data-nosort>Действия</th></tr></thead>
|
<thead><tr><th data-nosort>№</th><th class="th-sortable is-asc" title="ФИО — по возрастанию">ФИО</th><th>Логин</th><th data-filter>Основа</th><th>Варианты</th><th data-filter>Пароль</th><th class="text-right" data-nosort>Действия</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr th:each="row, iter : ${studentGrades}">
|
<tr th:each="row, iter : ${studentGrades}">
|
||||||
<td class="text-muted" th:text="${iter.count}">1</td>
|
<td class="text-muted" th:text="${iter.count}">1</td>
|
||||||
|
|||||||
@@ -81,8 +81,8 @@
|
|||||||
<div class="scroll-x">
|
<div class="scroll-x">
|
||||||
<table class="tablekit" th:attr="id='assign-table-' + ${gStat.index}">
|
<table class="tablekit" th:attr="id='assign-table-' + ${gStat.index}">
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
<th class="th-w-sm" data-nosort>Выбор</th><th class="th-sortable is-asc" title="ФИО — по возрастанию">Студент</th><th class="th-w-md">Уровень</th><th class="th-w-md">Вариант</th>
|
<th class="th-w-sm" data-nosort>Выбор</th><th class="th-sortable is-asc" title="ФИО — по возрастанию">Студент</th><th class="th-w-md" data-filter>Уровень</th><th class="th-w-md">Вариант</th>
|
||||||
<th class="th-w-sm">Источник</th><th>Сдача</th>
|
<th class="th-w-sm" data-filter>Источник</th><th data-filter>Сдача</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr th:each="s : ${g.students}">
|
<tr th:each="s : ${g.students}">
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
<span class="badge" th:classappend="${s.source == 'вручную'} ? 'badge-ok' : 'badge-warn'"
|
<span class="badge" th:classappend="${s.source == 'вручную'} ? 'badge-ok' : 'badge-warn'"
|
||||||
th:text="${s.source}">авто</span>
|
th:text="${s.source}">авто</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td th:attr="data-filter-value=${s.submissionId != null ? 'сдано' : 'нет'}">
|
||||||
<a th:if="${s.submissionId}" class="cell-link"
|
<a th:if="${s.submissionId}" class="cell-link"
|
||||||
th:href="'/admin/submissions/' + ${s.submissionId} + '/download'">
|
th:href="'/admin/submissions/' + ${s.submissionId} + '/download'">
|
||||||
попытка <span th:text="${s.submissionSeq}">1</span> ↓</a>
|
попытка <span th:text="${s.submissionSeq}">1</span> ↓</a>
|
||||||
|
|||||||
@@ -19,13 +19,13 @@
|
|||||||
|
|
||||||
<div class="card card-flush">
|
<div class="card card-flush">
|
||||||
<table id="labs-table" class="tablekit">
|
<table id="labs-table" class="tablekit">
|
||||||
<thead><tr><th class="th-sortable is-asc" title="Номер — по возрастанию">№</th><th>Название</th><th>Вариантов</th><th>Статус</th><th data-nosort></th></tr></thead>
|
<thead><tr><th class="th-sortable is-asc" title="Номер — по возрастанию">№</th><th>Название</th><th>Вариантов</th><th data-filter>Статус</th><th data-nosort></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr th:each="lab : ${labs}">
|
<tr th:each="lab : ${labs}">
|
||||||
<td th:text="${lab.number}">1</td>
|
<td th:text="${lab.number}">1</td>
|
||||||
<td><a th:href="@{/admin/labs/{id}(id=${lab.id})}" th:text="${lab.title}">—</a></td>
|
<td><a th:href="@{/admin/labs/{id}(id=${lab.id})}" th:text="${lab.title}">—</a></td>
|
||||||
<td th:text="${lab.variantCount}">0</td>
|
<td th:text="${lab.variantCount}">0</td>
|
||||||
<td>
|
<td th:attr="data-filter-value=${lab.status}">
|
||||||
<form class="inline-form" th:action="@{/admin/labs/{id}/toggle-status(id=${lab.id})}" method="post">
|
<form class="inline-form" th:action="@{/admin/labs/{id}/toggle-status(id=${lab.id})}" method="post">
|
||||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||||
<button class="badge badge-btn"
|
<button class="badge badge-btn"
|
||||||
|
|||||||
Reference in New Issue
Block a user