баги
This commit is contained in:
@@ -425,6 +425,9 @@ Access JWT содержит claim'ы `tenant`, `userId`, `username`, `role`, `de
|
||||
frontend восстанавливает access JWT и профиль в памяти через refresh-cookie; Web Storage
|
||||
для данных авторизации не используется. Backend возвращает `401`, если токен отсутствует
|
||||
или некорректен, и `403`, если роли недостаточно.
|
||||
Подписанный токен с отсутствующей ролью или пустым логином, а также с неположительным
|
||||
ID пользователя отклоняется как некорректный. Отсутствие обязательных данных профиля
|
||||
не должно приводить к внутренней ошибке сервера.
|
||||
|
||||
`AuthContext` существует только в границах одного servlet-запроса. Интерцептор очищает
|
||||
`ThreadLocal` до любых ранних выходов, устанавливает пользователя только после успешной
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| **Фреймворк** | Переходный режим: React 19 для страницы входа, кабинетов студента и преподавателя, страницы настроек и общего слоя `react/shared`, Vanilla JavaScript для остальных разделов |
|
||||
| **Фреймворк** | React 19 для всех пользовательских страниц и вкладок; общие сервисы и чистые вспомогательные функции остаются ES-модулями |
|
||||
| **Модульная система** | ES6 Modules (`import`/`export`) |
|
||||
| **Стили** | CSS (модульный подход) |
|
||||
| **Шрифт** | Системный стек без внешних font-CDN |
|
||||
| **Веб-сервер** | Apache httpd на Alpine со строгим CSP |
|
||||
|
||||
Пошаговый план и актуальная точка продолжения находятся в
|
||||
[`REACT_MIGRATION.md`](REACT_MIGRATION.md). Миграция выполняется по целым страницам и
|
||||
разделам без изменения действующих URL и REST-контрактов.
|
||||
[`REACT_MIGRATION.md`](REACT_MIGRATION.md). Все страницы и вкладки переведены на React
|
||||
с сохранением действующих URL и REST-контрактов.
|
||||
|
||||
---
|
||||
|
||||
@@ -102,14 +102,15 @@ frontend/
|
||||
│ │ │ └── TeacherAbsencesTab.jsx # Отсутствия: реестр, мастер замены, пожелания и заявки на изменение
|
||||
│ │ ├── main.jsx # React entrypoint для `/admin/`
|
||||
│ │ └── testing/ # render-admin.jsx для DOM-проверок
|
||||
│ └── shared/ # Общий React-слой для следующих переносимых страниц
|
||||
│ └── shared/ # Общий React-слой всех кабинетов
|
||||
│ ├── api-client.js # requestJson/apiClient поверх auth-транспорта
|
||||
│ ├── auth-client.js # Восстановление сессии и logout с redirect
|
||||
│ ├── use-sidebar.js # Десктопное сворачивание и мобильное меню
|
||||
│ ├── dates.js # Дата-математика (недели, ISO, форматирование)
|
||||
│ ├── session-policy.js # Фазы loading/ready/unauthorized/error и русские тексты
|
||||
│ ├── use-authorized-session.js # Хук проверки роли при монтировании страницы
|
||||
│ ├── runtime-entry.jsx # Единая точка экспорта общего runtime-chunk
|
||||
│ ├── ui/ # AppErrorBoundary, AsyncState, FormAlert, AsyncCombobox, ScheduleOverview, ConfirmDialog, Pagination
|
||||
│ ├── ui/ # CustomSelect, SidebarToggle, AppErrorBoundary, AsyncState, FormAlert, AsyncCombobox, ScheduleOverview, ConfirmDialog, Pagination
|
||||
│ └── testing/ # render-shared.jsx для DOM-проверок через renderToStaticMarkup
|
||||
├── tests/
|
||||
│ ├── academic-calendar-grid.test.mjs # Неполная первая неделя календарного графика
|
||||
@@ -132,6 +133,7 @@ frontend/
|
||||
│ ├── schedule-view-semesters.test.mjs # Выбор семестра и расчёт двухнедельного диапазона просмотра
|
||||
│ ├── teacher-absences.test.mjs # Payload мастера замены и доступность вкладки по ролям
|
||||
│ ├── teacher-preferences.test.mjs # Календарь пожеланий, заявки и подсказки конструктора
|
||||
│ ├── browser/ # Playwright: наведение, селекты и мобильное меню
|
||||
│ └── security-policy.test.mjs # Web Storage, XSS, пароли, язык, CSP и Dockerfile
|
||||
├── index.html # 🔐 Страница авторизации (общая)
|
||||
├── style.css # Стили страницы авторизации
|
||||
@@ -330,9 +332,15 @@ settings SPA не содержат неработающей кнопки бур
|
||||
`PageResponse`; размер страницы — 25, 50 или 100. `view-state.js` унифицирует
|
||||
загрузку со spinner, пустой результат и ошибку с кнопкой повторного запроса.
|
||||
- `AsyncCombobox` загружает варианты с сервера, сохраняет выбранный ID, поддерживает
|
||||
клавиатуру и роли `combobox/listbox`. Он используется для групп, преподавателей,
|
||||
клавиатуру и роли `combobox/listbox`. При новом вводе сразу исключаются устаревшие
|
||||
результаты, включая ответы во время задержки поиска. Escape закрывает меню с возвратом
|
||||
фокуса без нового запроса; уход фокуса за пределы компонента отменяет отложенный поиск.
|
||||
Возврат стрелкой вверх сохраняет введённый текст. Он используется для групп, преподавателей,
|
||||
аудиторий и совместимых календарных графиков; предварительная загрузка вариантов не
|
||||
раскрывает меню. Студент больше не загружает полный список групп.
|
||||
- Реестры пользователей, групп, дисциплин и заявок сохраняют выбранные параметры до
|
||||
завершения запроса, чтобы быстрые изменения фильтров и сортировки не отменяли друг друга.
|
||||
Сброс фильтров групп также отменяет отложенный поиск.
|
||||
- `dialog.js` заменяет нативные `confirm`, `prompt` и `alert`: возвращает фокус, удерживает
|
||||
его внутри окна, закрывается по Escape и может требовать причину. Нативных диалогов в
|
||||
admin/settings-модулях нет.
|
||||
@@ -414,11 +422,28 @@ SHA-256 style-хэшей с CSP. Там же закреплены безопас
|
||||
npm run build # собрать dist/vendor/otel.js и dist/react/login.js
|
||||
npm test # frontend unit/static tests
|
||||
npm run check # React-сборка, синтаксис auth/UI-модулей + все frontend-тесты
|
||||
npm run check:browser # Проверка UI, асинхронного поиска и валидации в локальном Chromium
|
||||
```
|
||||
|
||||
React-сборка выполняется существующим esbuild без отдельного dev-сервера. Для страницы
|
||||
входа установлен лимит 75 000 байт gzip; превышение завершает сборку ошибкой. При появлении
|
||||
нескольких React entrypoint общий runtime будет вынесен в отдельный кэшируемый chunk.
|
||||
React-сборка выполняется esbuild без отдельного dev-сервера. Общие зависимости вынесены
|
||||
в кэшируемые chunk'и, вкладки админки загружаются через `React.lazy`. Холодный бюджет
|
||||
входа — 85 000 байт gzip, остальных кабинетов — 90 000 байт. Для каждого entrypoint
|
||||
считаются только его статические зависимости: lazy-вкладки и модули других страниц
|
||||
не включаются в холодную загрузку. Превышение лимита завершает сборку ошибкой.
|
||||
|
||||
Браузерные проверки требуют запущенной локальной версии в Docker и Chromium
|
||||
(`/usr/bin/chromium`, другой путь задаётся через `CHROMIUM_PATH`). По умолчанию
|
||||
используются `https://localhost` и демо-аккаунт `admin`; настройки можно передать через
|
||||
`FRONTEND_TEST_BASE_URL`, `FRONTEND_TEST_LOGIN`, `FRONTEND_TEST_PASSWORD`.
|
||||
Проверка пожеланий использует демо-аккаунт преподавателя; для другого аккаунта
|
||||
задаются `FRONTEND_TEST_TEACHER_LOGIN` и `FRONTEND_TEST_TEACHER_PASSWORD`.
|
||||
Проверки не сохраняют изменения данных: проверяются обе темы, доступные вкладки,
|
||||
края кнопок, смена роли в форме, сброс фильтра, мобильное меню админки/настроек
|
||||
и стабильность ячеек пожеланий преподавателя. Отдельные сценарии задерживают ответы
|
||||
API для проверки совместного изменения роли и сортировки и отмены поиска после сброса.
|
||||
`shared-controls.test.mjs` собирает текущие компоненты в изолированную браузерную форму:
|
||||
проверяет устаревшие ответы и ошибки поиска, клавиатурное управление, уход фокуса
|
||||
и валидацию обязательного селекта без записи данных в API.
|
||||
|
||||
---
|
||||
|
||||
@@ -549,6 +574,32 @@ ID преподавателя берётся из восстановленног
|
||||
4. **`modals.css`** — Модальные окна
|
||||
5. **`departments-data.css`** — Стили создания кафедры/специальности
|
||||
|
||||
### Общие элементы управления после миграции
|
||||
|
||||
Кнопки сохраняют область наведения: hover и active меняют цвет, границу и тень,
|
||||
не смещая сам элемент через `translateY`. Это исключает мерцание у нижней кромки.
|
||||
Пункты бокового меню и ячейки пожеланий преподавателя также не смещаются при наведении.
|
||||
Карточки и боковая панель админки/настроек используют непрозрачные фоновые токены
|
||||
без `backdrop-filter`; у карточек отключена анимация с сохраняемым `transform`.
|
||||
Это убирает фоновые фильтруемые слои, на которых наблюдались цветные полосы при
|
||||
перерисовке после наведения на меню. В развёрнутой панели не создаются избыточные
|
||||
нативные подсказки `title`; подсказки остаются у скрытых подписей свёрнутого меню.
|
||||
Волна нажатия страницы входа ограничена `.btn-submit`; админка больше не добавляет
|
||||
ripple-узлы в React-разметку старым глобальным обработчиком.
|
||||
|
||||
Одинарные селекты вкладок и пагинации используют React-компонент `CustomSelect`.
|
||||
Подписи, пункты и `disabled` синхронизируются с нативным контролом после рендера;
|
||||
пустой пункт доступен для сброса фильтров. Автоматическое оборачивание селектов
|
||||
через `admin/js/dropdown.js` отключено. При отправке формы пустой обязательный
|
||||
селект показывает «Выберите значение», получает фокус на видимой кнопке и помечается
|
||||
`aria-invalid`; выбор значения снимает ошибку. Пример — в `docs/UI_COMPONENTS.md`.
|
||||
|
||||
Оболочки админки и настроек используют `useSidebar` и `SidebarToggle`:
|
||||
сворачивание десктопной панели хранится в `localStorage`, открытие мобильного
|
||||
меню — отдельное временное состояние. Мобильная панель закрывается при выборе
|
||||
раздела, клике по затемнению, Escape и смене размера экрана. Навигация развёрнутой
|
||||
панели прокручивается, а её заголовок и нижние действия остаются доступными.
|
||||
|
||||
### Темизация
|
||||
|
||||
Общий `ui-foundation.css` подключается последним во всех пяти оболочках.
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
| Компонент | Технология |
|
||||
|-----------|-----------|
|
||||
| **Backend** | Java 17, Spring Boot 3.5.16 |
|
||||
| **Frontend** | React 19 + переходные Vanilla JavaScript ES-модули + HTML/CSS |
|
||||
| **Frontend** | React 19 для всех пользовательских страниц + общие ES-модули + HTML/CSS |
|
||||
| **База данных** | PostgreSQL (через Flyway миграции) |
|
||||
| **Контейнеризация** | Docker, Docker Compose |
|
||||
| **Продакшн** | Kubernetes, Caddy (реверс-прокси) |
|
||||
|
||||
@@ -4,45 +4,53 @@
|
||||
|
||||
---
|
||||
|
||||
## 1. Стандартные одинарные списки (Custom Select Wrapper)
|
||||
## 1. Одинарные списки на React
|
||||
|
||||
Этот компонент автоматически "оборачивает" любые стандартные теги `<select>` на всём сайте, превращая их в красивые выпадающие меню. Вам **не нужно** писать сложный HTML, всё работает автоматически!
|
||||
В админке используется `frontend/react/shared/ui/CustomSelect.jsx`. Он сохраняет
|
||||
стили `.custom-select-wrapper`, нативный `select` для формы и событие `change`,
|
||||
но разметкой меню, выбранной подписью и состоянием `disabled` управляет React.
|
||||
Глобальный `MutationObserver` из `admin/js/dropdown.js` больше не запускается.
|
||||
Его прежнее применение к React-дереву оставляло устаревшие подписи и лишние DOM-узлы.
|
||||
|
||||
### Как добавить новый одинарный список:
|
||||
```jsx
|
||||
import { CustomSelect } from '../../shared/ui/CustomSelect.jsx';
|
||||
|
||||
Просто добавьте обычный тег `<select>` в HTML:
|
||||
|
||||
```html
|
||||
<div class="form-group">
|
||||
<label for="my-new-select">Выберите опцию</label>
|
||||
<select id="my-new-select">
|
||||
<option value="">Выберите...</option>
|
||||
<option value="1">Опция 1</option>
|
||||
<option value="2">Опция 2</option>
|
||||
</select>
|
||||
<div className="form-group">
|
||||
<label htmlFor="my-new-select">Выберите опцию</label>
|
||||
<CustomSelect
|
||||
id="my-new-select"
|
||||
value={selectedId}
|
||||
disabled={loading}
|
||||
onChange={event => setSelectedId(event.target.value)}
|
||||
>
|
||||
<option value="">Все значения</option>
|
||||
{options.map(option => (
|
||||
<option key={option.id} value={option.id}>{option.name}</option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Как это работает:
|
||||
1. В файле `frontend/admin/js/dropdown.js` инициализируется глобальный **`MutationObserver`**.
|
||||
2. Как только любой скрипт или загрузка страницы добавляет `<select>` в DOM, скрипт автоматически:
|
||||
- Скрывает оригинальный `<select>` (но оставляет его доступным из JS!).
|
||||
- Рисует поверх него красивый `div.custom-select-wrapper` с нужным текстом, иконкой-шевроном и эффектом размытия фона.
|
||||
- Синхронизирует состояния (если вы выберете элемент в кастомном UI, он автоматически изменит `select.value` и кинет событие `change`).
|
||||
|
||||
### Динамическое обновление списка (через JS):
|
||||
Если вы подгружаете список с API, просто обновите `innerHTML` **нативного селекта**, как обычно:
|
||||
|
||||
```javascript
|
||||
const select = document.getElementById('my-new-select');
|
||||
select.innerHTML = '<option value="99">Новое значение с API</option>';
|
||||
```
|
||||
**Магия!** Экземпляр `CustomSelect` использует свой собственный внутренний `MutationObserver` для отслеживания изменений `<option>`, поэтому он **автоматически перестроит красивый кастомный выпадающий список**. Никаких дополнительных вызовов для перерисовки не требуется.
|
||||
Список обновляется через `options` и `value`, без `innerHTML`. Пустое значение
|
||||
можно выбрать для сброса фильтра; недоступные пункты помечаются `disabled`.
|
||||
При количестве вариантов больше шести появляется поиск. Поддерживаются стрелки,
|
||||
Home/End, Escape, закрытие снаружи и снятие обработчиков при размонтировании.
|
||||
Для обязательного поля передайте `required`: пустое значение блокирует отправку,
|
||||
показывает «Выберите значение» и переводит фокус на видимую кнопку селекта.
|
||||
Ошибка связана с кнопкой через `aria-describedby` и снимается после выбора.
|
||||
Для поиска по API используется отдельный `AsyncCombobox`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Множественный выбор (Multi-Select с чекбоксами)
|
||||
|
||||
На React-страницах меню и закрытие снаружи управляются состоянием компонентов
|
||||
(`EquipmentMultiSelect` в `ClassroomsTab.jsx`, `WorkloadMultiSelect` в
|
||||
`AuditoriumWorkloadTab.jsx` и аналогичные компоненты остальных вкладок).
|
||||
Глобальные `initMultiSelect` и `closeAllDropdownsOnOutsideClick` к React-дереву
|
||||
не применяются. HTML-примеры ниже описывают прежний контракт CSS-классов;
|
||||
новые элементы следует создавать в JSX и управлять ими через React-состояние.
|
||||
|
||||
Этот UI-компонент позволяет выбирать сразу несколько элементов из выпадающего списка. Он включает в себя кастомные красивые галочки (checkmarks) с неоновой подсветкой и кастомный скроллбар.
|
||||
|
||||
Этот компонент требует написания определённой HTML-структуры, так как нативного тега `select multiple` с похожей функциональностью не существует.
|
||||
|
||||
Reference in New Issue
Block a user