E-mail media
src/features/email_media_library/ · (UI-екран) — Family-agnostic core для перегляду, завантаження, відбору, модерації та видалення медіафайлів TU; у production він підключений для Family golden.
Суть
E-mail media зберігає бібліотеку фото, відео й аудіо для кожної TU та розділяє файли на три статуси: Approved, Rejected і On moderation. Ліва колонка визначає активну TU, центральна працює зі схваленими або відхиленими файлами, права показує чергу модерації.
Media-логіка, стани, дії та API-flow винесені в один core, який може працювати через Family-specific adapter дерева operator/team lead/TU. Поточний adapter і routes реалізовані лише для Family golden; підтримку інших Family не слід вважати готовою, доки для них не додані й не підключені власні loaders.
Межа Family-specific логіки
Family-specific частина обмежена endpoint-параметрами, маршрутом/menu-entry та адаптацією відповіді до спільної ієрархії team lead → operator → TU. Точка розширення — src/features/email_media_library/utils/getEmailMediaPageUsers.ts: після нормалізації дерева й приєднання media counts усі наступні сценарії виконує shared EmailMedia.
Нову Family підключають новим loader/adapter, а не копіюванням media-компонентів. Решта екрана не повинна знати початкову форму Family-відповіді.
Use cases
Operator, team lead і top manager використовують екран для роботи з бібліотекою медіа доступних їм TU. Точні відмінності дій за ролями зафіксовані в таблиці нижче.
Доступ і маршрути
| Роль | Family | Маршрут | Точка підключення |
|---|---|---|---|
| Operator | Golden | /operator/email_media | src/components/operator/golden_operator/Index.tsx |
| Team lead | Golden | /teamlead/golden/email_media | src/components/team_lead/Index.js |
| Top manager | Golden | /top_manager/golden/email_media | src/components/top_manager/Index.js |
Для Udate, Chathouse та Prime production routes і adapters у поточній реалізації відсутні.
Доступи
| Дія | Operator | Team lead | Top manager |
|---|---|---|---|
| Перегляд TU та медіа | ✓ | ✓ | ✓ |
| Завантаження нового медіа | ✓ | ✓ | ✓ |
| Вибір файлів у центральній колонці | — | ✓ | ✓ |
| Видалення вибраних файлів | — | ✓ | ✓ |
| Approve / Reject | — | ✓ | ✓ |
До вибору TU центральна і права колонки неактивні: перемикачі статусу, фільтри й Add new недоступні.
Структура екрана
TU Profiles
Ліва колонка показує доступні TU в ієрархії ролі:
- operator бачить плоский список власних TU;
- team lead спочатку розкриває operator, потім вибирає TU;
- top manager спочатку розкриває team lead, потім operator і TU.
Групи та доступні TU можна розкривати або вибирати з клавіатури; поточний стан розкриття або вибору передається допоміжним технологіям.
Рядок TU містить avatar, ім’я, вік та зовнішній ID. Активна TU позначається check-mark.
Approved / Rejected
Центральна колонка має дві взаємовиключні вкладки:
Approved— стартова вкладка після вибору TU;Rejected— відхилені під час модерації файли.
У цій колонці доступні компактні картки, fullscreen preview, незалежний media-фільтр, upload і, для team lead/top manager, мультивибір із видаленням.
On moderation
Права колонка показує файли, які очікують рішення. Заголовок On moderation (N) відображає кількість файлів, уже завантажених у поточний frontend-список, а не гарантований загальний count на бекенді.
Team lead і top manager бачать Approve та Reject. Operator бачить вміст без кнопок модерації.
Вибір TU та початкове завантаження
При виборі іншої TU екран атомарно переходить у початковий стан:
- активною стає вибрана TU;
- центральна вкладка повертається на
Approved; - обидва фільтри повертаються до
Photo + Video + Audio; - попередній вибір файлів очищається;
- попередні списки всіх трьох статусів скидаються;
ApprovedіOn moderationзавантажуються паралельно;Rejectedзавантажується ліниво лише при першому відкритті вкладки.
Повторний клік по вже активній TU не створює нового запиту.
Індикатори TU та груп
Колір рядків дозволяє знайти TU, які потребують уваги, ще до відкриття медіа.
| Стан | Де застосовується | Вигляд і код кольору | Умова для TU |
|---|---|---|---|
| No access | TU | Приглушений рядок без окремого background: opacity: 0.5 | Для TU не повернуто media counts. Рядок не можна вибрати. |
| Warning | TU і групи | Світло-жовтий: #fafa9b8a | onModerationCount > 0. Warning має пріоритет над empty-станом. |
| Danger | TU і групи | Світло-червоний: #f5c6cb | Доступ є, але allCount === 0. |
| Default | TU і групи | Без status-background: transparent / успадкований фон | Доступ є, файли присутні й черга модерації порожня. |
Для operator/team lead груп діють агреговані правила:
- warning, якщо хоча б одна доступна TU має файл на модерації;
- danger, якщо в групі є хоча б одна доступна TU і всі доступні TU мають
allCount === 0; - TU без media counts не беруть участі в перевірці групи на danger.
Групи використовують ті самі .warning і .danger backgrounds, що й TU. active не є окремим status-кольором: вибраний рядок отримує font-weight: 600, а hover без status-індикатора — нейтральний #e0e6ed.
Локальні counts змінюються одразу після успішного upload, moderation або delete, без очікування повного перезавантаження списку TU.
Фільтри
Центральна колонка й On moderation мають незалежні фільтри з трьома типами:
Photo;Video;Audio.
Кожне натискання відразу застосовує набір і закриває popup. Якщо користувач вимкнув останній тип, frontend нормалізує порожній набір назад до всіх трьох типів. Отже стан «не показувати нічого» через фільтр не підтримується.
Зміна фільтра:
- запускає новий запит першої сторінки для відповідної колонки;
- показує global loader як реакцію на явну дію користувача;
- у центральній колонці очищає вибрані файли;
- не змінює фільтр іншої колонки.
Якщо користувач переходить між Approved і Rejected, уже ініціалізований список без змін фільтра використовується повторно. Новий запит потрібен лише для ще не відкритого статусу або коли його appliedFilters відрізняються від поточного набору.
Пагінація та нескінченний скрол
Медіа завантажуються cursor-пагінацією по 20 файлів. Наступна сторінка запитується, коли sentinel наприкінці фактичної scroll-області входить у viewport цієї колонки.
Load more дозволений лише коли одночасно виконуються умови:
- TU вибрана;
- список уже має хоча б один елемент;
- бекенд повернув наступний cursor;
- для статусу немає активного initial/reload/load-more запиту.
Коли cursor дорівнює null або бекенд повторює cursor щойно завантаженої сторінки, список вважається завершеним. Це не дає нескінченному скролу повторювати той самий запит без прогресу.
Нові сторінки додаються в кінець у серверному порядку, дублікати id відкидаються. Initial і reload дедуплікують та впорядковують першу сторінку:
On moderation— від старіших до новіших за датою додавання;ApprovedіRejected— від новіших до старіших за датою модерації.
Віртуалізація
Віртуалізація обмежує кількість media DOM-вузлів і декодованих зображень, але не змінює склад списку чи пагінацію.
Центральна колонка
| Значення | Величина |
|---|---|
| Картка | 80 × 95 |
| Gap між картками | 6 |
| Висота віртуального рядка | 101 |
| Overscan | 6 рядків |
Кількість колонок розраховується за реальною корисною шириною. Ширина центральної секції прив’язується до цілої кількості карток і окремо резервує фактичний scrollbar gutter, тому поява вертикального скролу не залишає місце, якого недостатньо для ще однієї картки.
On moderation
| Значення | Величина |
|---|---|
| Початкова оцінка рядка | 375 |
| Overscan | 2 картки |
| Відступ після картки | 6 |
Фактична висота moderation-картки вимірюється після рендера. Це важливо для різних типів медіа й адаптивної ширини колонки.
Media URLs і якість
Кожен media record може мати три окремі джерела:
| Поле | Призначення в цьому розділі |
|---|---|
previewUrl | Preview фото або відео для compact-картки Approved / Rejected. |
mediumUrl | Preview фото або відео для великої inline-картки On moderation. |
url | Оригінальний файл для fullscreen, фактичного відтворення відео та інших дій, де потрібне повне медіа. |
Frontend використовує ці окремі рівні, але не визначає й не перевіряє їхні фактичні pixel dimensions. Значення розмірів не слід документувати без підтвердженого backend-контракту.
Frontend навмисно не підставляє original image замість відсутнього previewUrl або mediumUrl: це могло б змусити grid декодувати багато повнорозмірних файлів і повернути фрізи скролу. Якщо потрібного preview немає або воно не завантажилось, відповідний slot показує placeholder правильного розміру.
Для відео preview є окремим зображенням із бекенда. Frontend не генерує кадр із відеофайлу. В inline-картці placeholder/preview прибирається після фактичного завантаження video data, бо з цього моменту браузеру вже є що показувати.
Fullscreen завжди отримує original url.
Детальна поведінка image/video/audio, poster, playback controls, fullscreen і fallback-станів належить окремій документації shared media renderer. Цей документ фіксує лише контракт E-mail media з ним.
Завантаження файлів
Add new приймає множинний вибір image/*, video/* та audio/*. Додаткова frontend-перевірка пропускає лише MIME-type з префіксом image/, video/ або audio/.
Окремого client-side ліміту розміру файла в feature не визначено. Фактичні backend-обмеження потрібно перевіряти через API documentation.
Файли обробляються послідовно. Для кожного файла progress report фіксує success або failure, а загальний відсоток рахується як кількість завершених спроб від загальної кількості вибраних файлів.
Після кожного успішного upload:
- локальний
allCountTU збільшується на 1; - локальний
onModerationCountзбільшується на 1; - інші файли з пачки продовжують завантажуватись навіть після окремої помилки.
Після завершення пачки, якщо успішний хоча б один файл, On moderation тихо синхронізується з бекендом без очищення вже відрендереного списку. Політика loader-а для цього й інших сценаріїв зведена в секції Loading, empty та error states.
Progress report залишається відкритим до натискання Confirm. Поки upload активний, повторне відкриття file picker заблоковане.
Модерація
Після успішної відповіді файл:
- видаляється з
On moderation; - додається на початок відповідного локального
ApprovedабоRejectedсписку; - зменшує
onModerationCountTU на 1, але не нижче 0; - при approve збільшує
approvedCountна 1; - не змінює
allCount, оскільки файл не створюється і не видаляється.
Кнопки модерації відсутні для operator, але сама moderation-колонка доступна для перегляду.
Вибір і видалення
Team lead і top manager можуть вибрати кілька compact-карток у поточній центральній вкладці. Перехід Approved ↔ Rejected, зміна центрального фільтра або вибір іншої TU очищає selection.
Перед видаленням показується confirmation. Кожен вибраний файл видаляється окремою операцією; запити виконуються з обмеженням до чотирьох одночасних операцій, тому пачка може завершитися частковим успіхом:
- успішно видалені файли відразу прибираються з усіх локальних status-списків і selection;
- counts відповідної TU коригуються за фактичним статусом файла;
- результат усієї пачки показується однією notification: success, failure або combined summary для часткового успіху;
- global loader залишається до завершення всієї пачки.
Видалення з fullscreen використовує ту саму операцію й ті самі правила доступу.
Preview та інформація про файл
Клік по медіа відкриває shared fullscreen preview:
- у центральній колонці preview отримує весь видимий після фільтрації список і стартує з натиснутого файла;
- у moderation відкривається один поточний файл;
- delete у fullscreen передається лише для team lead/top manager.
Info-icon compact-картки показує, хто й коли додав файл, а також хто й коли його модерував. Відсутній moderation record не показується.
Loading, empty та error states
| Сценарій | Поведінка |
|---|---|
| Завантаження списку TU | Global loader. |
| Перше завантаження вибраної TU | Global loader; Approved і On moderation запитуються паралельно. |
| Зміна фільтра | Global loader, бо користувач очікує повністю новий результат. |
| Перше відкриття ще не завантаженої вкладки | Global loader. |
| Upload | Локальний progress/report без глобального overlay. |
| Фонова синхронізація після upload | Без global loader і без очищення карток. |
| Infinite scroll | Без global loader і без додаткового текстового рядка. |
| Moderation / delete | Global loader до завершення mutation. |
| Порожній завершений список | No media files. |
| Initial loading у порожній колонці | Loading.... |
| Помилка завантаження | Global notification і error-state відповідної колонки з дією Retry; backend message зберігається, за його відсутності використовується frontend fallback. |
Якщо кілька операцій із global loader перекриваються, overlay залишається видимим до завершення останньої з них.
Reload з помилкою зберігає попередній список і показує compact error-state з Retry. Помилка initial-запиту завершує loading-стан і показує повний error-state з Retry; те саме правило застосовується до завантаження дерева TU.
Поки error-state активний, infinite scroll не запускає автоматичних повторних запитів. Завантаження продовжується лише після явної дії Retry.
Конкурентні запити та плавність
Для одного status одночасно актуальним може бути лише останній initial/reload. Новий базовий запит скасовує попередній запит цього status і незавершений load more. Скасування не показує error notification.
Request id та list epoch залишаються додатковим захистом: навіть якщо транспортне скасування запізнилось, застаріла відповідь не може перезаписати новішу TU, фільтр, cursor або список.
Після успішного delete або moderation активні media-запити відповідних status інвалідовуються й скасовуються перед локальним commit, якщо TU mutation усе ще вибрана. Інвалідація іншої TU не зачіпає запити поточної, а запізнілий GET не може повернути змінений файл у список.
Завантаження дерева TU має такий самий захист за актуальністю ролі, Family та авторизованого користувача. Зміна контексту скасовує попереднє завантаження, а його запізнілі дані або помилка не застосовуються до нового екрана.
Під час reload незмінені media records повторно використовують попередні object references. Це дозволяє memoized-карткам не рендеритися повторно лише через те, що бекенд повернув еквівалентний record новим JSON-об’єктом.
Lazy image loading, async decoding, preview-рівні та віртуалізація працюють разом. Прибирати один із цих рівнів без performance-перевірки не варто: головне навантаження цієї сторінки створюють мережеве завантаження й декодування медіа, а не кількість простих placeholder-блоків.
API та realtime
Media operations використовують constants із src/utils/emailMedia.js:
EMAIL_MEDIA_GET_BY_OPTIONS—/email-media/getMedia;EMAIL_MEDIA_GET_COUNT_INFO_FOR_LADIES—/lady-emails/emailMediaInfo;EMAIL_MEDIA_CREATE_FILE—/email-media/createMedia;EMAIL_MEDIA_UPDATE_FILE_STATUS—/email-media/updateMediaStatus;EMAIL_MEDIA_DELETE_FILES—/email-media/deleteMedia.
Golden tree adapters використовують:
G_GET_LADIES_FOR_OPER—/golden/operator/getLadiesізsrc/utils/operatorData.js;GOLDEN_LADIES_GROUPED_BY_OPERATOR—/golden/lady/getLadiesGroupedByOperatorsізsrc/utils/ladiesData.js;GOLDEN_LADIES_GROUPED_BY_OPERATORS_AND_SUPERVISORS—/golden/lady/getLadiesGroupedByOperatorsAndSupervisorsізsrc/utils/ladiesData.js.
Socket events або listeners у src/features/email_media_library/ відсутні.
Зв’язки
- Frontend entry:
src/features/email_media_library/Index.tsx. - Family/role adapters:
src/features/email_media_library/utils/getEmailMediaPageUsers.ts. - Shared media integration: media-renderer — renderer, playback, fullscreen, placeholders і performance-контракт.
- Frontend endpoint constants:
src/utils/emailMedia.js,src/utils/operatorData.js,src/utils/ladiesData.js. - swagger — правила й реєстр Swagger UI цього frontend-сервісу.
- TODO: додати перевірені operation deeplink-и для list/count, upload, moderation, delete та Golden tree endpoints.