feat(preferences): configurable photo info attributes stored in user profile

- backend: users.preferences jsonb (migration 000004), Me returns preferences, PATCH /auth/me/preferences with validation (visible list + labels)
- frontend: gear in info section opens PhotoInfoSettingsDialog; base attribute set shown by default, remaining under ellipsis; per-attribute visibility and custom labels persisted to the profile; reset clears settings from the profile
- frontend: dialog has select-all/deselect-all icon buttons (no 'show all'); EXIF constants extracted to lib/exif.ts
- fix: render modal overlays via portal to document.body so they center in the viewport despite transformed panel/rail ancestors
- openspec: sync lightroom-workspace-ui artifacts (spec/design/proposal/tasks 9.8)
This commit is contained in:
iu
2026-10-10 21:04:08 +03:00
parent e1592549b2
commit 1b4906648d
23 changed files with 703 additions and 254 deletions
@@ -132,20 +132,23 @@ filmstrip, панель метаданных/комментариев, тулб
панель обходилась одним запросом. Переименование публикует `photo.updated`, страница альбома
обновляет запись в списке.
### 17. Настройки информации: оверрайды в профиле пользователя
Настройки раздела «Информация» хранятся в профиле как оверрайды, а не как полный список:
### 17. Настройки информации: явный список видимых в профиле пользователя
Настройки раздела «Информация» хранятся в профиле как явный список видимых атрибутов:
`users.preferences` (jsonb, дефолт `{}`) с секцией `photo_info` вида
`{ "hidden": ["Contrast", ...], "labels": { "Make": "Камера", ... } }`. Отсутствие ключа
означает, что атрибут видим и подпись дефолтная, поэтому новые и незнакомые теги EXIF не
«теряются» и не требуют миграций данных. `GET /auth/me` отдаёт `preferences` вместе с
пользователем; сохранение — `PATCH /auth/me/preferences` (полная замена объекта с серверной
валидацией: строки и массивы строк, ограничение длины подписи/ключа и числа скрытых ключей).
Настройки не входят в `PublicUser` (приватны). UI: `PanelSection` получает слот `actions` под
шестерёнку; модалка `PhotoInfoSettingsDialog` строится из объединения известного каталога
подписей и ключей текущего фото; список в панели фильтруется по оверрайдам, а скрытые
атрибуты сворачиваются под кнопку «…» (раскрытие — временное, для текущего фото).
Альтернативы: полный упорядоченный список в профиле (хрупок к новым тегам, нужны миграции) и
хранение на клиенте (не переносится между устройствами) — отклонены.
`{ "visible": ["Make", "Model", ...], "labels": { "Make": "Камера", ... } }`. Отсутствие
секции `photo_info` означает базовый набор по умолчанию (камера, объектив, выдержка,
диафрагма, ISO, фокусное расстояние) — не «все атрибуты». Список `visible` хранится явно,
поэтому различимо состояние «ничего не выбрано»; новые и незнакомые теги не «теряются» (их
можно включить в диалоге). `GET /auth/me` отдаёт `preferences` вместе с пользователем;
сохранение — `PATCH /auth/me/preferences` (полная замена объекта с серверной валидацией:
строки и массивы строк, ограничение длины подписи/ключа и числа записей). Пустой объект
(`{}`) удаляет настройки, возвращая базовый набор. Настройки не входят в `PublicUser`
(приватны). UI: `PanelSection` получает слот `actions` под шестерёнку; модалка
`PhotoInfoSettingsDialog` строится из объединения известного каталога подписей и ключей
текущего фото и даёт действия «отметить все»/«снять все» и «Сбросить»; список в панели
фильтруется по сохранённым видимым атрибутам, а остальные сворачиваются под кнопку «…»
(раскрытие — временное, для текущего фото). Альтернатива с оверрайдами `hidden` (пусто = все
видимы) отклонена: не даёт базовый набор по умолчанию.
### 18. Метаданные фото: устойчивый парсер и хранение в jsonb
EXIF извлекается при загрузке и хранится в `photos.exif` (jsonb, миграция `000003_photo_exif`)
@@ -42,9 +42,11 @@
- **Разделы и панели.** Правая панель — сворачиваемые разделы (состояние сохраняется), с
разделом «Информация» (EXIF/метаданные); левая и правая панели ресайзятся перетаскиванием.
- **Настройки информации.** Раздел «Информация» получает кнопку-шестерёнку, открывающую
модалку со списком EXIF-атрибутов: переключатели видимости и пользовательские подписи.
Скрытые атрибуты сворачиваются под элементом «…» (клик раскрывает все). Настройки хранятся
в профиле пользователя (server-side) и применяются ко всем фото.
модалку со списком EXIF-атрибутов: галочки видимости (с действиями «отметить все»/«снять
все»), пользовательские подписи и сброс. Скрытые атрибуты сворачиваются под элементом «…»
(клик раскрывает все). Настройки хранятся в профиле пользователя (server-side), применяются
ко всем фото, а при отсутствии настроек показывается базовый набор; «Сбросить» удаляет
настройки из профиля.
- **Счётчики.** В ответе альбома — число фото и участников.
- **Темы и хром.** Существующие 4 темы сохраняются; `dark` — графит Lightroom и дефолт,
`neutral` — тёплый графит; переключатель упорядочен тёмная → нейтральная → светлая →
@@ -219,21 +219,27 @@ SHALL отображаться вне каркаса, но в общей тем
### Requirement: Настройки списка атрибутов информации
Раздел «Информация» SHALL предоставлять доступ к настройкам (иконка-шестерёнка), открывающим
модальное окно, в котором пользователь SHALL выбирать отображаемые EXIF-атрибуты и задавать
их подписи. Настройки SHALL применяться ко всем фото и SHALL сохраняться в профиле
пользователя. При отсутствии настроек SHALL отображаться все доступные атрибуты.
модальное окно со списком EXIF-атрибутов, где пользователь SHALL отмечать отображаемые
атрибуты и задавать их подписи. В окне SHALL быть доступны действия «отметить все» и
«снять все». Настройки SHALL применяться ко всем фото и SHALL сохраняться в профиле
пользователя. При отсутствии сохранённых настроек SHALL отображаться базовый набор атрибутов
(камера, объектив, выдержка, диафрагма, ISO, фокусное расстояние).
#### Scenario: Открытие настроек
- **WHEN** пользователь нажимает иконку настроек в разделе «Информация»
- **THEN** открывается модальное окно со списком EXIF-атрибутов: переключатели видимости и поля подписей
- **THEN** открывается модальное окно со списком EXIF-атрибутов: галочки видимости и поля подписей, а также действия «отметить все»/«снять все»
#### Scenario: Значения по умолчанию
- **WHEN** у пользователя нет сохранённых настроек
- **THEN** в разделе «Информация» показан базовый набор атрибутов, а остальные доступны по элементу «…»
#### Scenario: Сохранение настроек
- **WHEN** пользователь скрывает атрибут, задаёт подпись и сохраняет
- **WHEN** пользователь отмечает атрибуты, задаёт подпись и сохраняет
- **THEN** раздел «Информация» учитывает изменения, а настройки сохраняются в профиле
#### Scenario: Сброс настроек
- **WHEN** пользователь сбрасывает настройки к значениям по умолчанию
- **THEN** показываются все атрибуты с исходными подписями
- **WHEN** пользователь сбрасывает настройки
- **THEN** сохранённые настройки удаляются из профиля, и отображается базовый набор с исходными подписями
### Requirement: Свёртка скрытых атрибутов
Если настройками часть атрибутов скрыта, в конце списка атрибутов SHALL отображаться элемент
@@ -59,10 +59,11 @@
## 9. Настройки информации пользователя
- [ ] 9.1 Миграция `000004_user_preferences` (up/down): `ADD COLUMN preferences jsonb NOT NULL DEFAULT '{}'`. Проверка: `make migrate` применяется, `\d users` показывает колонку, down её снимает.
- [ ] 9.2 `models.User` (+`Preferences`), `auth/store.go` (выборка `preferences` в `GetUserByID`, метод `UpdatePreferences`), `auth/handlers.go`/`service.go` (`Me` возвращает `preferences`; `PATCH /auth/me/preferences` с валидацией структуры, длины подписей/ключей и числа скрытых). Проверка: невалидный payload → 400, валидный сохраняется.
- [ ] 9.3 Маршрут `PATCH /auth/me/preferences` в `app.go` под middleware. Проверка: без токена 401, с токеном сохранение и чтение согласованы.
- [ ] 9.4 Frontend: `types.ts` (+`preferences`), auth-стор (+`preferences`, сохранение через API). Проверка: `cd frontend && npm run check`.
- [ ] 9.5 `PanelSection` — слот `actions` для шестерёнки; новый `PhotoInfoSettingsDialog` (список атрибутов, видимость, подписи, сброс к дефолту). Проверка: изменения применяются к разделу и сохраняются в профиле.
- [ ] 9.6 `AlbumPanel`: фильтрация атрибутов по оверрайдам, элемент «…» для показа скрытых (при отсутствии скрытых — не показывается). Проверка: скрытые сворачиваются, «…» раскрывает все, дефолт — все видимы.
- [ ] 9.7 Проверка: `cd frontend && npm run check`, `cd backend && go test ./...`, `make build`, `openspec validate "lightroom-workspace-ui"`.
- [x] 9.1 Миграция `000004_user_preferences` (up/down): `ADD COLUMN preferences jsonb NOT NULL DEFAULT '{}'`. Проверка: `make migrate` применяется, `\d users` показывает колонку, down её снимает.
- [x] 9.2 `models.User` (+`Preferences`), `auth/store.go` (выборка `preferences` в `GetUserByID`, метод `UpdatePreferences`), `auth/handlers.go`/`service.go` (`Me` возвращает `preferences`; `PATCH /auth/me/preferences` с валидацией структуры, длины подписей/ключей и числа скрытых). Проверка: невалидный payload → 400, валидный сохраняется.
- [x] 9.3 Маршрут `PATCH /auth/me/preferences` в `app.go` под middleware. Проверка: без токена 401, с токеном сохранение и чтение согласованы.
- [x] 9.4 Frontend: `types.ts` (+`preferences`), auth-стор (+`preferences`, сохранение через API). Проверка: `cd frontend && npm run check`.
- [x] 9.5 `PanelSection` — слот `actions` для шестерёнки; новый `PhotoInfoSettingsDialog` (список атрибутов, видимость, подписи, сброс к дефолту). Проверка: изменения применяются к разделу и сохраняются в профиле.
- [x] 9.6 `AlbumPanel`: фильтрация атрибутов по оверрайдам, элемент «…» для показа скрытых (при отсутствии скрытых — не показывается). Проверка: скрытые сворачиваются, «…» раскрывает все, дефолт — все видимы.
- [x] 9.7 Проверка: `cd frontend && npm run check`, `cd backend && go test ./...`, `make build`, `openspec validate "lightroom-workspace-ui"`.
- [x] 9.8 Базовый набор атрибутов по умолчанию при пустых настройках (модель `visible` вместо `hidden`); в диалоге — иконки «отметить все»/«снять все» вместо «Показать всё»; «Сбросить» удаляет настройки из профиля (`PATCH {}`). Проверка: Playwright — дефолт даёт базовый набор и «…», снятие/отметка всех, сброс очищает профиль (12/12).