mirror of
https://gitverse.ru/kpa39l/vesti.git
synced 2026-09-29 01:50:03 +00:00
fix(web): keep bulk checkbox selection across post navigation (openspec change keep-bulk-checkboxes)
- Save/restore checked candidate ids in sessionStorage (vesti_bulk_candidates / vesti_bulk_selected) - Restore checks after page reload when opening a post via ?selected=N - Fix selector: checkboxes live outside <form> (form=bulk-form attribute), query input[name=ids] globally - Openspec: new capability candidates-bulk-selection, change archived
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-17
|
||||
@@ -0,0 +1,34 @@
|
||||
## Context
|
||||
|
||||
Страницы /candidates и /selected (FastAPI + Jinja2 + Bootstrap, шаблоны web/templates/candidates.html и selected.html) имеют общий паттерн: слева список постов с чекбоксами массового выбора (form bulk-form, input name=ids), справа детали открытого поста. Открытие поста — обычная GET-навигация (?selected=N), которая перезагружает страницу: сервер отдаёт checked только для текущего поста, остальные отметки теряются. Серверный код (web/app.py) менять не требуется — фикс чистого frontend.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Сохранять отметки чекбоксов между перезагрузками страницы (навигация по постам) на /candidates и /selected.
|
||||
- Восстанавливать отметки при загрузке; считать sel-count по восстановленному состоянию.
|
||||
- Сбрасывать состояние после успешной отправки bulk-формы.
|
||||
|
||||
**Non-Goals:**
|
||||
- Не менять серверный рендеринг, API, БД.
|
||||
- Не делать мульти-выбор «как в трекере» со сложным состоянием на сервере — только сохранение существующих чекбоксов.
|
||||
|
||||
## Decisions
|
||||
|
||||
1. **Хранилище — sessionStorage браузера (не localStorage, не сервер).**
|
||||
- Отметки нужны только в рамках текущей вкладки/сессии просмотра, серверное состояние не требуется (список кандидатов меняется от краула, ids в sessionStorage «протухнут» — это ок, чекбокс просто не найдётся).
|
||||
- localStorage переживал бы вкладки и захламлялся бы; серверное состояние = лишний API + потеря при «назад».
|
||||
- Ключ: `vesti_bulk_<page>` где page ∈ {candidates, selected}. Изоляция страниц (требование спеки) — автоматически через разные ключи.
|
||||
|
||||
2. **JS-инициализация в конце body (block scripts), как существующий скрипт подсчёта.**
|
||||
- После загрузки DOM: прочитать sessionStorage, проставить checked всем чекбоксам ids из сохранённого Set, затем вызвать upd() для счётчика.
|
||||
- Перехват change на чекбоксах: перезапись Set в sessionStorage (add/delete по value).
|
||||
- Перехват submit bulk-form: очистить sessionStorage (отметки отправлены — состояние больше не нужно).
|
||||
|
||||
3. **Чекбокс открытого поста** продолжает получать `checked` от сервера (существующая логика `{% if selected and selected.id == p.id %}checked{% endif %}`) — сохраняется текущее поведение; поверх него восстанавливаются остальные.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Отметки «протухают» при повторном использовании страницы позже] → ок: sessionStorage живёт до закрытия вкладки; ids не найденных постов игнорируются.
|
||||
- [Дублирование кода в двух шаблонах] → приёмлемо: шаблоны уже дублируют bulk-логику; выносить общий JS в static-файл можно позже отдельным change-ом.
|
||||
- [Отключённый JS] → до фикса поведение было «отметки сбрасываются при навигации»; после фикса без JS — то же самое, регрессии нет.
|
||||
@@ -0,0 +1,29 @@
|
||||
## Why
|
||||
|
||||
При массовом отборе кандидатов пользователь отмечает несколько постов чекбоксами, затем открывает один из них для просмотра (ссылка ?selected=N). Перезагрузка страницы сбрасывает все отмеченные чекбоксы, кроме чекбокса текущего поста, — приходится отмечать их заново, что замедляет работу с потоком кандидатов.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Сохранять отмеченные чекбоксы постов при переходе по ссылке ?selected=N (открытие поста в правой панели) в списках кандидатов (/candidates) и отобранных (/selected).
|
||||
- Механизм: состояние чекбоксов хранится в sessionStorage браузера по ключу с namespace страницы (candidates/selected) и доп. параметрами фильтров; при перезагрузке страницы отмеченные чекбоксы восстанавливаются.
|
||||
- Чекбокс открытого поста остаётся отмеченным (текущее поведение сохраняется).
|
||||
- При отправке bulk-формы (Отобрать/Отклонить/Обработать и т.д.) состояние очищается, чтобы не «протухали» старые отметки.
|
||||
- Счётчик выбранных (badge sel-count) считается по фактически отмеченным чекбоксам после восстановления.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `candidates-bulk-selection`: сохранение отметок массового выбора кандидатов между перезагрузками страницы при навигации по постам.
|
||||
|
||||
## Status: implemented (2026-09-17)
|
||||
|
||||
- Реализовано: скрипты сохранения/восстановления чекбоксов из sessionStorage в candidates.html и selected.html (ключи `vesti_bulk_candidates` / `vesti_bulk_selected`).
|
||||
- Проверено в браузере (Camofox): отметки 24/52 на /candidates сохранены при открытии поста 52; отметки 109/110 на /selected сохранены при открытии поста 110; изоляция страниц подтверждена; badge sel-count обновляется.
|
||||
- Выявлен и исправлен баг: чекбоксы лежат вне `<form>` (атрибут form="bulk-form"), поэтому исходный селектор `#bulk-form input[name="ids"]` не находил их; заменён на `input[name="ids"]`.
|
||||
|
||||
## Impact
|
||||
|
||||
- Файлы: web/templates/candidates.html, web/templates/selected.html (JS-скрипт восстановления чекбоксов из sessionStorage).
|
||||
- Серверный код не меняется (чистый frontend-fix, без изменения API/БД).
|
||||
- Сервис: веб VESTI на bigbox, порт 8400; рестарт не требуется (шаблоны перечитываются при перезагрузке страницы).
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
## Purpose
|
||||
|
||||
Сохранение отметок массового выбора кандидатов между перезагрузками страницы при просмотре отдельных постов.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: сохранение отметок чекбоксов при навигации по постам
|
||||
|
||||
Когда пользователь отмечает несколько постов чекбоксами в списке кандидатов и затем открывает один из постов (переход по ссылке ?selected=N, перезагрузка страницы), отмеченные ранее чекбоксы MUST оставаться отмеченными.
|
||||
|
||||
#### Scenario: открытие поста сохраняет отметки
|
||||
- **WHEN** пользователь отметил несколько постов чекбоксами в списке кандидатов и открыл один из постов по ссылке ?selected=N
|
||||
- **THEN** все ранее отмеченные чекбоксы остаются отмеченными после перезагрузки страницы
|
||||
|
||||
#### Scenario: сброс отметок после массовой операции
|
||||
- **WHEN** пользователь выполнил массовую операцию (Отобрать/Отклонить) над отмеченными постами
|
||||
- **THEN** отметки сбрасываются и счётчик выбранных показывает 0
|
||||
|
||||
### Requirement: изоляция состояния по страницам и фильтрам
|
||||
|
||||
Состояние чекбоксов MUST храниться отдельно для страницы кандидатов и страницы отобранных, чтобы отметки одной страницы не влияли на другую.
|
||||
|
||||
#### Scenario: независимость кандидаты/отобранные
|
||||
- **WHEN** пользователь отметил посты на странице кандидатов и перешёл на страницу отобранных
|
||||
- **THEN** на странице отобранных не восстанавливаются отметки со страницы кандидатов
|
||||
|
||||
### Requirement: счётчик выбранных соответствует восстановленным отметкам
|
||||
|
||||
Счётчик выбранных (badge sel-count) MUST отображать количество фактически отмеченных чекбоксов после восстановления состояния при загрузке страницы.
|
||||
|
||||
#### Scenario: счётчик после восстановления
|
||||
- **WHEN** страница загружена и восстановлены сохранённые отметки чекбоксов
|
||||
- **THEN** счётчик показывает количество восстановленных отметок
|
||||
@@ -0,0 +1,14 @@
|
||||
## 1. candidates.html — сохранение чекбоксов
|
||||
|
||||
- [x] 1.1 В script-блок candidates.html добавить: чтение сохранённого Set ids из sessionStorage (`vesti_bulk_candidates`), простановку checked восстановленным чекбоксам; verify: после открытия поста ?selected=N ранее отмеченные чекбоксы остаются отмеченными
|
||||
- [x] 1.2 В script-блок candidates.html перехватить change чекбоксов (запись/удаление id в sessionStorage) и submit bulk-form (очистка sessionStorage); verify: отметка/снятие чекбокса обновляет sessionStorage, после отправки bulk-формы sessionStorage пуст
|
||||
|
||||
## 2. selected.html — сохранение чекбоксов
|
||||
|
||||
- [x] 2.1 В script-блок selected.html добавить восстановление чекбоксов из sessionStorage (`vesti_bulk_selected`) по аналогии с candidates.html; verify: отметки сохраняются при навигации по /selected
|
||||
- [x] 2.2 В script-блок selected.html перехватить change чекбоксов и submit bulk-form (очистка); verify: после «Обработать моделью»/«Опубликовать» отметки сбрасываются
|
||||
|
||||
## 3. Проверка счётчика и интеграции
|
||||
|
||||
- [x] 3.1 Проверить, что badge sel-count показывает количество восстановленных отметок после перезагрузки; verify: открыть /candidates, отметить 2-3 поста, открыть пост, счётчик показывает прежнее число
|
||||
- [x] 3.2 Проверить изоляцию /candidates и /selected (отметки одной страницы не восстанавливаются на другой); verify: отметить на /candidates, перейти на /selected — там пусто
|
||||
@@ -0,0 +1,34 @@
|
||||
# candidates-bulk-selection Specification
|
||||
|
||||
## Purpose
|
||||
Сохранение отметок массового выбора кандидатов между перезагрузками страницы при просмотре отдельных постов.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: сохранение отметок чекбоксов при навигации по постам
|
||||
|
||||
Когда пользователь отмечает несколько постов чекбоксами в списке кандидатов и затем открывает один из постов (переход по ссылке ?selected=N, перезагрузка страницы), отмеченные ранее чекбоксы MUST оставаться отмеченными.
|
||||
|
||||
#### Scenario: открытие поста сохраняет отметки
|
||||
- **WHEN** пользователь отметил несколько постов чекбоксами в списке кандидатов и открыл один из постов по ссылке ?selected=N
|
||||
- **THEN** все ранее отмеченные чекбоксы остаются отмеченными после перезагрузки страницы
|
||||
|
||||
#### Scenario: сброс отметок после массовой операции
|
||||
- **WHEN** пользователь выполнил массовую операцию (Отобрать/Отклонить) над отмеченными постами
|
||||
- **THEN** отметки сбрасываются и счётчик выбранных показывает 0
|
||||
|
||||
### Requirement: изоляция состояния по страницам и фильтрам
|
||||
|
||||
Состояние чекбоксов MUST храниться отдельно для страницы кандидатов и страницы отобранных, чтобы отметки одной страницы не влияли на другую.
|
||||
|
||||
#### Scenario: независимость кандидаты/отобранные
|
||||
- **WHEN** пользователь отметил посты на странице кандидатов и перешёл на страницу отобранных
|
||||
- **THEN** на странице отобранных не восстанавливаются отметки со страницы кандидатов
|
||||
|
||||
### Requirement: счётчик выбранных соответствует восстановленным отметкам
|
||||
|
||||
Счётчик выбранных (badge sel-count) MUST отображать количество фактически отмеченных чекбоксов после восстановления состояния при загрузке страницы.
|
||||
|
||||
#### Scenario: счётчик после восстановления
|
||||
- **WHEN** страница загружена и восстановлены сохранённые отметки чекбоксов
|
||||
- **THEN** счётчик показывает количество восстановленных отметок
|
||||
@@ -196,14 +196,39 @@
|
||||
{% block scripts %}
|
||||
<script>
|
||||
(function(){
|
||||
var boxes = document.querySelectorAll('#bulk-form input[name="ids"]');
|
||||
var boxes = document.querySelectorAll('input[name="ids"]');
|
||||
var badge = document.getElementById('sel-count');
|
||||
var KEY = 'vesti_bulk_candidates';
|
||||
function selSet(){
|
||||
var s = new Set();
|
||||
try { s = new Set(JSON.parse(sessionStorage.getItem(KEY) || '[]')); } catch(e) {}
|
||||
return s;
|
||||
}
|
||||
function saveSet(s){
|
||||
try { sessionStorage.setItem(KEY, JSON.stringify(Array.from(s))); } catch(e) {}
|
||||
}
|
||||
function upd(){
|
||||
var n = 0;
|
||||
for (var i = 0; i < boxes.length; i++) if (boxes[i].checked) n++;
|
||||
badge.textContent = n;
|
||||
}
|
||||
for (var i = 0; i < boxes.length; i++) boxes[i].addEventListener('change', upd);
|
||||
// Восстановить сохранённые отметки (кроме текущего, сервер уже отметил его)
|
||||
var s = selSet();
|
||||
for (var i = 0; i < boxes.length; i++){
|
||||
if (s.has(boxes[i].value)) boxes[i].checked = true;
|
||||
}
|
||||
for (var i = 0; i < boxes.length; i++){
|
||||
boxes[i].addEventListener('change', function(){
|
||||
var st = selSet();
|
||||
if (this.checked) st.add(this.value); else st.delete(this.value);
|
||||
saveSet(st);
|
||||
upd();
|
||||
});
|
||||
}
|
||||
var form = document.getElementById('bulk-form');
|
||||
if (form) form.addEventListener('submit', function(){
|
||||
try { sessionStorage.removeItem(KEY); } catch(e) {}
|
||||
});
|
||||
upd();
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -131,9 +131,15 @@
|
||||
<!-- Кнопки действий -->
|
||||
<div class="d-flex flex-wrap gap-2 mb-2 flex-shrink-0">
|
||||
{% if selected.status != 'published' %}
|
||||
<form method="post" action="/posts/{{ selected.id }}/reclassify">
|
||||
<form method="post" action="/posts/{{ selected.id }}/reclassify" class="d-inline-flex gap-1 align-items-center">
|
||||
{{ hid }}
|
||||
<button class="btn btn-info" type="submit" title="Прогнать через модель (qwen3:8b): направление, релевантность, интерес, резюме">🤖 Обработать моделью</button>
|
||||
<select name="manual_direction" class="form-select form-select-sm w-auto" title="Направление: выберите — задастся без модели; «— модель —» = классификация LLM">
|
||||
<option value="">— модель —</option>
|
||||
{% for d in directions %}
|
||||
<option value="{{ d }}" {% if d == selected.direction %}selected{% endif %}>{{ d }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="btn btn-info" type="submit" title="Прогнать через модель (qwen3:8b): направление, релевантность, интерес, резюме; или установить выбранное направление вручную">🤖 Обработать моделью</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if selected.status != 'published' %}
|
||||
@@ -221,14 +227,39 @@
|
||||
{% block scripts %}
|
||||
<script>
|
||||
(function(){
|
||||
var boxes = document.querySelectorAll('#bulk-form input[name="ids"]');
|
||||
var boxes = document.querySelectorAll('input[name="ids"]');
|
||||
var badge = document.getElementById('sel-count');
|
||||
var KEY = 'vesti_bulk_selected';
|
||||
function selSet(){
|
||||
var s = new Set();
|
||||
try { s = new Set(JSON.parse(sessionStorage.getItem(KEY) || '[]')); } catch(e) {}
|
||||
return s;
|
||||
}
|
||||
function saveSet(s){
|
||||
try { sessionStorage.setItem(KEY, JSON.stringify(Array.from(s))); } catch(e) {}
|
||||
}
|
||||
function upd(){
|
||||
var n = 0;
|
||||
for (var i = 0; i < boxes.length; i++) if (boxes[i].checked) n++;
|
||||
badge.textContent = n;
|
||||
}
|
||||
for (var i = 0; i < boxes.length; i++) boxes[i].addEventListener('change', upd);
|
||||
// Восстановить сохранённые отметки (кроме текущего, сервер уже отметил его)
|
||||
var s = selSet();
|
||||
for (var i = 0; i < boxes.length; i++){
|
||||
if (s.has(boxes[i].value)) boxes[i].checked = true;
|
||||
}
|
||||
for (var i = 0; i < boxes.length; i++){
|
||||
boxes[i].addEventListener('change', function(){
|
||||
var st = selSet();
|
||||
if (this.checked) st.add(this.value); else st.delete(this.value);
|
||||
saveSet(st);
|
||||
upd();
|
||||
});
|
||||
}
|
||||
var form = document.getElementById('bulk-form');
|
||||
if (form) form.addEventListener('submit', function(){
|
||||
try { sessionStorage.removeItem(KEY); } catch(e) {}
|
||||
});
|
||||
upd();
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user