Variant distribution: treat redirect as success, refresh modal in place, toasts above modal (inside dialog); asset v32

This commit is contained in:
2026-10-08 10:28:34 +03:00
parent 78130dbcd7
commit 6c2e41d558
10 changed files with 49 additions and 30 deletions

View File

@@ -468,7 +468,7 @@ code.code { padding: 0.125rem 0.5rem; border-radius: var(--radius-xs); font-fami
.cell-btn.st.dim::before { background: var(--c-border-strong); } .cell-btn.st.dim::before { background: var(--c-border-strong); }
.mini-count { font-size: 0.72rem; margin-left: 0.15rem; color: var(--c-text-muted); } .mini-count { font-size: 0.72rem; margin-left: 0.15rem; color: var(--c-text-muted); }
.defense-cell-timer { font-variant-numeric: tabular-nums; } .defense-cell-timer { font-variant-numeric: tabular-nums; }
.lab-head-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem; } .lab-head-actions { display: flex; gap: 0.2rem; }
.btn btn-secondary btn-sm p-2 { .btn btn-secondary btn-sm p-2 {
display: inline-flex; align-items: center; justify-content: center; display: inline-flex; align-items: center; justify-content: center;
min-width: 1.5rem; height: 1.5rem; padding: 0 0.3rem; cursor: pointer; min-width: 1.5rem; height: 1.5rem; padding: 0 0.3rem; cursor: pointer;

View File

@@ -209,7 +209,20 @@ function attachCommentAutocomplete(input) {
/* ---------- Удаление подсказки комментария и всплывающие уведомления ---------- */ /* ---------- Удаление подсказки комментария и всплывающие уведомления ---------- */
function showToast(text, kind) { function showToast(text, kind) {
var host = $id('toast-host'); // уведомления должны быть видны поверх открытой модалки: кладём их в сам <dialog>
// (элемент верхнего слоя), иначе они оказываются под ним
var dlg = document.querySelector('dialog[open]');
var host = null;
if (dlg) {
host = dlg.querySelector('.toast-host');
if (!host) {
host = document.createElement('div');
host.className = 'toast-host';
dlg.appendChild(host);
}
} else {
host = $id('toast-host');
}
if (!host) return; if (!host) return;
var toast = document.createElement('div'); var toast = document.createElement('div');
toast.className = 'toast-item ' + (kind === 'error' ? 'toast-error' : 'toast-success'); toast.className = 'toast-item ' + (kind === 'error' ? 'toast-error' : 'toast-success');
@@ -663,8 +676,15 @@ document.addEventListener('submit', function (ev) {
var btn = form.querySelector('button[type="submit"]'); var btn = form.querySelector('button[type="submit"]');
if (btn) btn.disabled = true; if (btn) btn.disabled = true;
fetch(form.action, { method: 'POST', body: new FormData(form), headers: { 'X-Requested-With': 'fetch' } }) fetch(form.action, { method: 'POST', body: new FormData(form), headers: { 'X-Requested-With': 'fetch' } })
.then(function (r) { if (r.redirected) throw new Error('сессия истекла — обновите страницу'); return r.text(); }) .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
.then(function () { return refreshAssignModal(form.getAttribute('data-refresh')); }) .then(function (html) {
// эндпоинты отвечают редиректом на страницу лабы — это норма, не ошибка;
// flash-сообщение забираем из отданного HTML
var err = /<div[^>]*class="[^"]*toast-item toast-error[^"]*"[^>]*>\s*<span class="toast-text">([^<]*)</.exec(html);
var ok = /<span class="toast-text">([^<]*)<\/span>/.exec(html);
showToast(err ? err[1].trim() : (ok ? ok[1].trim() : 'Сохранено'), err ? 'error' : 'success');
return refreshAssignModal(form.getAttribute('data-refresh'));
})
.catch(function (e) { showToast('Не удалось сохранить: ' + e.message, 'error'); }) .catch(function (e) { showToast('Не удалось сохранить: ' + e.message, 'error'); })
.finally(function () { if (btn) btn.disabled = false; }); .finally(function () { if (btn) btn.disabled = false; });
}); });

View File

@@ -44,10 +44,10 @@
<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>
<span class="lab-col-title" th:text="${lab.title}">—</span> <span class="lab-col-title" th:text="${lab.title}">—</span>
<div class="lab-head-actions"> <div class="lab-head-actions">
<button class="btn btn-primary btn-sm" type="button" <button class="btn btn-secondary btn-sm p-2" type="button"
th:attr="data-url=@{/admin/groups/{id}/assign-modal(id=${group.id}, labId=${lab.id})},data-lab-name=|ЛР ${lab.number}|" data-action="assign" th:attr="data-url=@{/admin/groups/{id}/assign-modal(id=${group.id}, labId=${lab.id})},data-lab-name=|ЛР ${lab.number}|" data-action="assign"
title="Раздать варианты студентам: уровни, варианты, автокраспределение по группе"> title="Выдача вариантов: уровни, варианты, автокраспределение по группе">
<i class="bi bi-people" aria-hidden="true"></i> Варианты <i class="cell-ico ico bi bi-list" aria-hidden="true"></i>
</button> </button>
<form class="inline-form" th:with="pub=${labPublication[lab.id]}, pc=${pub != null ? pub.publishedCount : 0}, all=${pub != null and pub.allPublished}" <form class="inline-form" th:with="pub=${labPublication[lab.id]}, pc=${pub != null ? pub.publishedCount : 0}, all=${pub != null and pub.allPublished}"
th:action="@{/admin/groups/{id}/publish(id=${group.id})}" method="post"> th:action="@{/admin/groups/{id}/publish(id=${group.id})}" method="post">
@@ -64,10 +64,9 @@
<span class="mini-count js-pub-count" th:classappend="(${all} or ${pc == 0}) ? 'is-hidden' : ''" th:text="${pc}"></span> <span class="mini-count js-pub-count" th:classappend="(${all} or ${pc == 0}) ? 'is-hidden' : ''" th:text="${pc}"></span>
</button> </button>
</form> </form>
<a class="btn btn-secondary btn-sm" <a class="btn btn-secondary btn-sm p-2"
th:href="'/admin/groups/' + ${group.id} + '/submissions.zip?labId=' + ${lab.id}" th:href="'/admin/groups/' + ${group.id} + '/submissions.zip?labId=' + ${lab.id}"
th:title="|Скачать решения по ЛР ${lab.number}|"> th:title="|Скачать решения по ЛР ${lab.number}|">⤓<span class="mini-count" th:text="${labSubmissionCounts[lab.id] ?: 0}">0</span></a>
<i class="bi bi-download" aria-hidden="true"></i><span class="mini-count" th:text="${labSubmissionCounts[lab.id] ?: 0}">0</span></a>
</div> </div>
</div> </div>
</th> </th>
@@ -343,7 +342,7 @@
<div class="dlg-body" id="assign-modal-body"></div> <div class="dlg-body" id="assign-modal-body"></div>
</dialog> </dialog>
<script th:src="@{/js/admin-group.js(v='30')}" defer></script> <script th:src="@{/js/admin-group.js(v='32')}" defer></script>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -196,7 +196,7 @@
</dialog> </dialog>
</th:block> </th:block>
</div> </div>
<script th:src="@{/js/admin-lab.js(v='30')}" defer></script> <script th:src="@{/js/admin-lab.js(v='32')}" defer></script>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -13,22 +13,22 @@
<noscript> <noscript>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
</noscript> </noscript>
<link rel="icon" type="image/svg+xml" th:href="@{/favicon.svg(v='30')}"> <link rel="icon" type="image/svg+xml" th:href="@{/favicon.svg(v='32')}">
<!-- v=4: сброс кэша стилей после редизайна; при изменении css поднимать версию --> <!-- v=4: сброс кэша стилей после редизайна; при изменении css поднимать версию -->
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
<link rel="stylesheet" th:href="@{/css/bootstrap-icons.min.css}"> <link rel="stylesheet" th:href="@{/css/bootstrap-icons.min.css}">
<link rel="stylesheet" th:href="@{/css/app.css(v='30')}"> <link rel="stylesheet" th:href="@{/css/app.css(v='32')}">
<link id="hljs-light" rel="stylesheet" th:href="@{/css/hljs-github.css(v='30')}"> <link id="hljs-light" rel="stylesheet" th:href="@{/css/hljs-github.css(v='32')}">
<link id="hljs-dark" rel="stylesheet" th:href="@{/css/hljs-github-dark.css(v='30')}" disabled> <link id="hljs-dark" rel="stylesheet" th:href="@{/css/hljs-github-dark.css(v='32')}" disabled>
<script th:src="@{/js/highlight.min.js(v='30')}" defer></script> <script th:src="@{/js/highlight.min.js(v='32')}" defer></script>
<script th:src="@{/js/table-scroll.js(v='30')}" defer></script> <script th:src="@{/js/table-scroll.js(v='32')}" defer></script>
<script th:src="@{/js/tablekit.js(v='30')}" defer></script> <script th:src="@{/js/tablekit.js(v='32')}" defer></script>
<script th:src="@{/js/bootstrap.bundle.min.js}" defer></script> <script th:src="@{/js/bootstrap.bundle.min.js}" defer></script>
<script th:src="@{/js/theme-init.js(v='30')}"></script> <script th:src="@{/js/theme-init.js(v='32')}"></script>
<script th:src="@{/js/app.js(v='30')}" defer></script> <script th:src="@{/js/app.js(v='32')}" defer></script>
<script th:src="@{/js/ui-state.js(v='30')}" defer></script> <script th:src="@{/js/ui-state.js(v='32')}" defer></script>
<script th:src="@{/js/md-content.js(v='30')}" defer></script> <script th:src="@{/js/md-content.js(v='32')}" defer></script>
<script th:src="@{/js/submit-guard.js(v='30')}" defer></script> <script th:src="@{/js/submit-guard.js(v='32')}" defer></script>
</head> </head>
<body> <body>
<div class="page d-flex flex-column min-vh-100"> <div class="page d-flex flex-column min-vh-100">

View File

@@ -138,7 +138,7 @@
</div> </div>
<script th:src="@{/js/mdeditor.js(v='30')}" defer></script> <script th:src="@{/js/mdeditor.js(v='32')}" defer></script>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -183,7 +183,7 @@
</div> </div>
</dialog> </dialog>
<script th:src="@{/js/content-sort.js(v='30')}" defer></script> <script th:src="@{/js/content-sort.js(v='32')}" defer></script>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -236,8 +236,8 @@
</div> </div>
</dialog> </dialog>
<script th:src="@{/js/mdeditor.js(v='30')}" defer></script> <script th:src="@{/js/mdeditor.js(v='32')}" defer></script>
<script th:src="@{/js/content-sort.js(v='30')}" defer></script> <script th:src="@{/js/content-sort.js(v='32')}" defer></script>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -143,7 +143,7 @@
</div> </div>
</dialog> </dialog>
<script th:src="@{/js/content-sort.js(v='30')}" defer></script> <script th:src="@{/js/content-sort.js(v='32')}" defer></script>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -107,7 +107,7 @@
<p class="text-sm text-muted">Ожидайте преподавателя с проверкой.</p> <p class="text-sm text-muted">Ожидайте преподавателя с проверкой.</p>
</div> </div>
</div> </div>
<script th:src="@{/js/defense-timer.js(v='30')}" defer></script> <script th:src="@{/js/defense-timer.js(v='32')}" defer></script>
</div> </div>
</body> </body>
</html> </html>