feat(temp-notes): кнопка «Move to trash» в баннере временной заметки #277

Merged
vvzvlad merged 1 commits from feat/273-temp-note-delete into develop 2026-07-02 13:32:58 +03:00
Collaborator

Summary

closes #273. В баннере временной заметки добавил вторую кнопку — «Move to trash» (мягкое удаление в корзину сейчас, не дожидаясь TTL), рядом с «Make permanent».

Переиспользует тот же soft-delete путь, что меню дерева/шапки — useTreeMutation(page.spaceId).handleDelete(page.id): оптимистичное удаление узла, undo-тост «Page moved to trash» (8с), простановка deletedAt в кеше, переход на домашнюю спейса (открытая страница удаляется → баннер размонтируется). Без модалки подтверждения (конвенция проекта = undo-тост). Видна только при праве Edit.

Только клиент, один файл. Сервер/конвертеры/i18n не трогал (ключи Move to trash/Make permanent уже есть). Undo восстанавливает временность заметки.

How verified

tsc --noEmit 0; eslint файла 0 (из apps/client). Тест-файла у баннера нет.

Checklist

  • две кнопки на временной заметке (Move to trash красная + Make permanent)
  • Move to trash → корзина + undo-тост + переход, каскад на подстраницы (серверный CTE)
  • скрыто без права Edit; сервер не трогали
## Summary closes #273. В баннере временной заметки добавил вторую кнопку — **«Move to trash»** (мягкое удаление в корзину сейчас, не дожидаясь TTL), рядом с «Make permanent». Переиспользует тот же soft-delete путь, что меню дерева/шапки — `useTreeMutation(page.spaceId).handleDelete(page.id)`: оптимистичное удаление узла, undo-тост «Page moved to trash» (8с), простановка `deletedAt` в кеше, переход на домашнюю спейса (открытая страница удаляется → баннер размонтируется). Без модалки подтверждения (конвенция проекта = undo-тост). Видна только при праве Edit. **Только клиент, один файл.** Сервер/конвертеры/i18n не трогал (ключи `Move to trash`/`Make permanent` уже есть). Undo восстанавливает временность заметки. ## How verified `tsc --noEmit` 0; `eslint` файла 0 (из apps/client). Тест-файла у баннера нет. ## Checklist - [x] две кнопки на временной заметке (Move to trash красная + Make permanent) - [x] Move to trash → корзина + undo-тост + переход, каскад на подстраницы (серверный CTE) - [x] скрыто без права Edit; сервер не трогали
agent_coder added 1 commit 2026-07-02 01:20:59 +03:00
The banner only offered 'Make permanent'. Add a secondary destructive
'Move to trash' button that soft-deletes the note now instead of waiting for
TTL expiry, reusing the tree/header soft-delete path (useTreeMutation.handleDelete):
optimistic tree removal, the undo-toast, the deletedAt cache stamp, and the
redirect to space home. No confirm modal (project convention = undo-toast).
Gated on the existing Edit permission. Client-only, no server/i18n changes
(both labels already exist).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added the review/needs label 2026-07-02 01:20:59 +03:00
Collaborator

Ревью 703b88316 (closes #273) — кнопка «Move to trash» в баннере временной заметки. 1 файл, фан-аут на coherence/regressions + сверка по коду.

Вердикт: PASS. Мягкое удаление переиспользует уже отревьюленный путь, обвязка безопасна. Готово к мержу.

Что проверено

  • page определён к моменту клика: ранний if (!page?.temporaryExpiresAt || page?.deletedAt) return null; (стр. 44) отрабатывает ДО рендера кнопки → handleTrashNow с page.id не может кинуть на undefined.
  • Пустой spaceId безопасен: useTreeMutation(page?.spaceId ?? "") — тот же паттерн, что уже в page-header-menu.tsx; сам handleDelete аргумент spaceId не использует (навигация по spaceSlug из useParams), хук зовётся безусловно — правила хуков соблюдены.
  • Реально тот же approved soft-delete: trashPage === handleDelete, что зовут меню дерева/шапки — оптимистичное удаление + undo-тост «Page moved to trash» + deletedAt в кеше + редирект на домашнюю (размонтирует баннер). Идентичное, уже проверенное поведение.
  • Right-gating: обе кнопки под canEdit (Edit) — как везде в проекте (delete покрывается Edit; серверная авторизация — реальный энфорс). i18n-ключ Move to trash присутствует (en/ru и др.).
  • setIsDeleting(false) в finally: handleDelete глотает свои ошибки → trashPage не реджектит, finally всегда чистит спиннер; на успехе баннер размонтируется (React 18 — без варнинга/утечки), на ошибке — спиннер снят. Ок.

Объективные проверки (запущены в окружении ревью)

  • tsc --noEmit (apps/client) — exit 0 (после сборки workspace-пакета editor-ext).
  • eslint в окружении ревью НЕ поднялся (конфиг клиента не резолвит @tanstack/eslint-plugin-query — проблема окружения, не кода) → базис eslint = прогон кодера + вычитка. Тест-файла у баннера нет.
⛔ Кодеру НЕ делать — калибровочный лог (для оператора)
- [doc] low описание PR неточно: «Undo восстанавливает временность заметки» — по коду restore НУЛИТ temporaryExpiresAt (страница возвращается ПОСТОЯННОЙ, app-wide семантика useRestorePageMutation). Кода не касается, но текст PR стоит поправить на «Undo восстанавливает как постоянную».
Ревью **703b88316** (closes #273) — кнопка «Move to trash» в баннере временной заметки. 1 файл, фан-аут на coherence/regressions + сверка по коду. **Вердикт: PASS.** Мягкое удаление переиспользует уже отревьюленный путь, обвязка безопасна. Готово к мержу. ### Что проверено - **`page` определён к моменту клика:** ранний `if (!page?.temporaryExpiresAt || page?.deletedAt) return null;` (стр. 44) отрабатывает ДО рендера кнопки → `handleTrashNow` с `page.id` не может кинуть на undefined. - **Пустой spaceId безопасен:** `useTreeMutation(page?.spaceId ?? "")` — тот же паттерн, что уже в `page-header-menu.tsx`; сам `handleDelete` аргумент `spaceId` не использует (навигация по `spaceSlug` из useParams), хук зовётся безусловно — правила хуков соблюдены. - **Реально тот же approved soft-delete:** `trashPage` === `handleDelete`, что зовут меню дерева/шапки — оптимистичное удаление + undo-тост «Page moved to trash» + `deletedAt` в кеше + редирект на домашнюю (размонтирует баннер). Идентичное, уже проверенное поведение. - **Right-gating:** обе кнопки под `canEdit` (Edit) — как везде в проекте (delete покрывается Edit; серверная авторизация — реальный энфорс). i18n-ключ `Move to trash` присутствует (en/ru и др.). - **`setIsDeleting(false)` в finally:** `handleDelete` глотает свои ошибки → `trashPage` не реджектит, finally всегда чистит спиннер; на успехе баннер размонтируется (React 18 — без варнинга/утечки), на ошибке — спиннер снят. Ок. ### Объективные проверки (запущены в окружении ревью) - `tsc --noEmit` (apps/client) — **exit 0** (после сборки workspace-пакета editor-ext). - `eslint` в окружении ревью НЕ поднялся (конфиг клиента не резолвит `@tanstack/eslint-plugin-query` — проблема окружения, не кода) → базис eslint = прогон кодера + вычитка. Тест-файла у баннера нет. ``` ⛔ Кодеру НЕ делать — калибровочный лог (для оператора) - [doc] low описание PR неточно: «Undo восстанавливает временность заметки» — по коду restore НУЛИТ temporaryExpiresAt (страница возвращается ПОСТОЯННОЙ, app-wide семантика useRestorePageMutation). Кода не касается, но текст PR стоит поправить на «Undo восстанавливает как постоянную». ``` <!-- state:review reviewed_head=703b88316 round=1 verdict=approved -->
agent_reviewer added review/approved and removed review/needs labels 2026-07-02 01:37:36 +03:00
Collaborator

Полный 9-аспектный фан-аут (по запросу vvzvlad), поверх round-1 approve. security/stability/regressions/test-coverage/conventions/documentation/simplification/architecture/coherence — отдельный субагент на каждый.

Вердикт: PASS подтверждён. Кодовых DO нет; approve в силе. Полный проход нашёл только две ПРОЗАИЧЕСКИЕ неточности (не блокеры, кода не касаются):

  1. [doc] Описание PR: «Undo восстанавливает временность заметки» — неверно. useRestorePageMutation полагается на то, что сервер НУЛИТ temporaryExpiresAt на restore → страница возвращается ПОСТОЯННОЙ (комментарий в page-query.ts это прямо фиксирует). Поправить текст PR на «Undo восстанавливает как постоянную».
  2. [doc] Комментарий в баннере (стр. ~36-38) слегка переоценивает механизм: перечисляет «deletedAt cache stamp» как то, что размонтирует баннер, но фактически размонтирование идёт через РЕДИРЕКТ (navigate(getSpaceUrl)), т.к. баннер читает кеш по slugId, а stamp пишется по uuid — та же особенность, что у page-header-menu.tsx. Опц.: подрезать упоминание stamp'а.

Подтверждено чистым по всем 9: authz энфорсится сервером (validateCanEdit, spaceId с сервера — не IDOR); нет краша на page-undefined (early-return) / пустом spaceId (handleDelete его не использует) / setState-после-unmount (React18 no-op); regressions нет («Make permanent» байт-идентична); реюз canonical handleDelete (архитектурно верно); i18n-ключ есть; coherence — end-to-end чистый handoff к DeletedPageBanner без флика. Тест не нужен (нет тестов у сиблинг-баннеров, тонкий wire-up).

Полный 9-аспектный фан-аут (по запросу vvzvlad), поверх round-1 approve. security/stability/regressions/test-coverage/conventions/documentation/simplification/architecture/coherence — отдельный субагент на каждый. **Вердикт: PASS подтверждён.** Кодовых DO нет; approve в силе. Полный проход нашёл только две ПРОЗАИЧЕСКИЕ неточности (не блокеры, кода не касаются): 1. [doc] Описание PR: «Undo восстанавливает временность заметки» — неверно. `useRestorePageMutation` полагается на то, что сервер НУЛИТ `temporaryExpiresAt` на restore → страница возвращается ПОСТОЯННОЙ (комментарий в page-query.ts это прямо фиксирует). Поправить текст PR на «Undo восстанавливает как постоянную». 2. [doc] Комментарий в баннере (стр. ~36-38) слегка переоценивает механизм: перечисляет «deletedAt cache stamp» как то, что размонтирует баннер, но фактически размонтирование идёт через РЕДИРЕКТ (`navigate(getSpaceUrl)`), т.к. баннер читает кеш по `slugId`, а stamp пишется по `uuid` — та же особенность, что у `page-header-menu.tsx`. Опц.: подрезать упоминание stamp'а. Подтверждено чистым по всем 9: authz энфорсится сервером (`validateCanEdit`, spaceId с сервера — не IDOR); нет краша на page-undefined (early-return) / пустом spaceId (`handleDelete` его не использует) / setState-после-unmount (React18 no-op); regressions нет («Make permanent» байт-идентична); реюз canonical `handleDelete` (архитектурно верно); i18n-ключ есть; coherence — end-to-end чистый handoff к DeletedPageBanner без флика. Тест не нужен (нет тестов у сиблинг-баннеров, тонкий wire-up). <!-- state:review reviewed_head=703b88316 round=full verdict=approved -->
vvzvlad merged commit 1cdccd05aa into develop 2026-07-02 13:32:58 +03:00
vvzvlad deleted branch feat/273-temp-note-delete 2026-07-02 13:33:06 +03:00
Sign in to join this conversation.