fix(page-tree/dnd): drop-раскрытие + guard от потери детей (#523) #532

Merged
vvzvlad merged 1 commits from fix/523-tree-dnd into fix/525-insert-unloaded 2026-07-12 05:08:44 +03:00
Collaborator

Summary

Три правки drag-and-drop дерева страниц (стоит стопкой на #525 — общий класс потери детей #159-#1):

  1. Задержка авто-раскрытия при hover 500→2000 мс (AUTO_EXPAND_MS) — меньше случайных раскрытий при проносе курсора.
  2. Не раскрывать цель при drop внутрь: убран onToggle(target,true) из make-child-drop; вместо навязчивого раскрытия — teal-пульс data-landed-child («уехало сюда») + инвалидация крошек перемещённой страницы.
  3. Guard от потери детей в handleMove: при make-child в незагруженную свёрнутую цель оптимистик = remove(source) + update(target, hasChildren:true) (gate взведён, children:[]), а не наивная материализация [source], которая скрыла бы серверных детей. Зеркалит фикс #525 на DnD-пути.

closes #523.

How verified

  • use-tree-mutation.move-guard.test.tsx (3) + update-cache-on-move.breadcrumbs.test.ts (1) — 2 файла, 4 passed.
  • Незагруженная цель НЕ материализует [source]; загруженная — позиционный insert среди существующих детей без потерь/дублей (["c1","X"]); genuinely-empty лист материализует source (["X"]).
  • Мутация: отключение guard'а (unloadedMakeChild=false) → тест «UNLOADED folder does NOT materialize [source]» краснеет.
  • Read-only дерево (doc-tree-row.tsx:137 if(!el||readOnly) return) drag/drop не регистрирует — shared-tree не затронут. Таймер teal-пульса чистится на unmount, уважает prefers-reduced-motion.

Примечание по стеку

База — fix/525-insert-unloaded (общий предикат isUnloadedBranch). После мержа #525 ретаргетить базу на develop.

Checklist

  • критерии приёмки из #523 выполнены
  • вне заявленного scope ничего не менялось
  • node_modules/миграций нет
## Summary Три правки drag-and-drop дерева страниц (стоит стопкой на #525 — общий класс потери детей #159-#1): 1. **Задержка авто-раскрытия при hover** 500→2000 мс (`AUTO_EXPAND_MS`) — меньше случайных раскрытий при проносе курсора. 2. **Не раскрывать цель при drop внутрь**: убран `onToggle(target,true)` из make-child-drop; вместо навязчивого раскрытия — teal-пульс `data-landed-child` («уехало сюда») + инвалидация крошек перемещённой страницы. 3. **Guard от потери детей в `handleMove`**: при make-child в незагруженную свёрнутую цель оптимистик = `remove(source)` + `update(target, hasChildren:true)` (gate взведён, `children:[]`), а не наивная материализация `[source]`, которая скрыла бы серверных детей. Зеркалит фикс #525 на DnD-пути. closes #523. ## How verified - `use-tree-mutation.move-guard.test.tsx` (3) + `update-cache-on-move.breadcrumbs.test.ts` (1) — **2 файла, 4 passed**. - Незагруженная цель НЕ материализует `[source]`; загруженная — позиционный insert среди существующих детей без потерь/дублей (`["c1","X"]`); genuinely-empty лист материализует source (`["X"]`). - Мутация: отключение guard'а (`unloadedMakeChild=false`) → тест «UNLOADED folder does NOT materialize [source]» краснеет. - Read-only дерево (`doc-tree-row.tsx:137 if(!el||readOnly) return`) drag/drop не регистрирует — shared-tree не затронут. Таймер teal-пульса чистится на unmount, уважает prefers-reduced-motion. ## Примечание по стеку База — `fix/525-insert-unloaded` (общий предикат `isUnloadedBranch`). После мержа #525 ретаргетить базу на `develop`. ## Checklist - [x] критерии приёмки из #523 выполнены - [x] вне заявленного scope ничего не менялось - [x] node_modules/миграций нет
agent_coder added 1 commit 2026-07-12 01:24:15 +03:00
Пять точечных клиентских правок (дизайн из адверсариального разбора #523):

1. Задержка hover-раскрытия при drag: `AUTO_EXPAND_MS` 500 → 2000 мс, чтобы
   проведение курсора через дерево не раскрывало ряды — только осознанное
   удержание.
2. Не раскрывать цель при drop внутрь (make-child): удалена строка
   `onToggle(target, true)` в `onDrop` (drop оставляет узел свёрнутым; раскрытие
   только по hover-hold). Восстановление раскрытости самого source не тронуто.
3. Guard от потери детей в `handleMove` (обязателен вместе с п.2): раньше эта же
   `onToggle` была ЕДИНСТВЕННЫМ триггером корректирующего lazy-load. `treeModel.move`
   материализует `target.children = [source]`; для НЕзагруженной цели (предикат
   gate `treeModel.isUnloadedBranch`) это скрыло бы остальных серверных детей папки
   (класс #159 #1). Теперь для незагруженной make-child-цели оптимистичное дерево
   строится БЕЗ материализации source: source удаляется из старого родителя, цели
   ставится `hasChildren:true`; gate остаётся взведён и раскрытие догружает полный
   набор. Для загруженной цели поведение прежнее (append сохраняет детей).
4. Инвалидация крошек: `updateCacheOnMovePage` инвалидирует
   `["breadcrumbs", pageId]` — guard делает `remove(source)`, и для текущей
   открытой страницы `findBreadcrumbPath` промахивается → крошки показывали бы
   старого родителя до refocus.
5. Сигнал «уехало сюда»: транзиентный teal-пульс на строке при make-child-drop в
   свёрнутую цель (узел НЕ раскрывается), отдельный `data-landed-child` +
   `DROP_LANDED_HIGHLIGHT_MS`, таймер чистится при анмаунте; уважает
   prefers-reduced-motion.

Тесты: `handleMove` — незагруженная make-child-цель не материализует `[source]`
(мутация guard'а краснит), загруженная цель append'ит и сохраняет детей,
genuinely-empty лист материализует; инвалидация `["breadcrumbs", pageId]`.

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

Хэндофф. Внутренний цикл: 1 проход, чисто — мутация (disable guard → тест «UNLOADED does NOT materialize» краснеет) подтвердила. База — ветка #525 (PR #531); после её мержа ретаргечу на develop. Guard зеркалит фикс #525 на DnD-пути; нормальные перемещения (reorder/reparent/to-root) без регрессий, read-only дерево не затронуто.

Хэндофф. Внутренний цикл: 1 проход, чисто — мутация (disable guard → тест «UNLOADED does NOT materialize» краснеет) подтвердила. База — ветка #525 (PR #531); после её мержа ретаргечу на develop. Guard зеркалит фикс #525 на DnD-пути; нормальные перемещения (reorder/reparent/to-root) без регрессий, read-only дерево не затронуто.
Collaborator

🤖 Авто-ревью · #532 (page-tree/dnd: drop-раскрытие + guard от потери детей, #523) — голова 27d51303. Вердикт: PASS

Это фикс ровно той вне-scope потери данных, что я флагнул на #531 (локальный DnD make-child в незагруженную ветку материализовал [source] и скрывал серверных детей). Закрыт корректно. Стоит стопкой на #531 (заапрувлен). Эскалаций нет.

Прогнал 9-аспектный веер (свёрнут в 3) + гейт (client vitest 94/94). Все 9 аспектов — LGTM. Сверил лично:

  • Guard переиспользует ОБЩИЙ isUnloadedBranch (use-tree-mutation.ts:83) — тот самый single-source-предикат из #531 (не локальная копия, коммент явно это отмечает), значит анти-дрейф держится.
  • Трихотомия корректна: UNLOADED (hasChildren:true, children:[]) → remove(source) + update(target,{hasChildren:true}), БЕЗ материализации [source] (gate взведён); LOADED → позиционный ["c1","X"] (без потери/дубля); genuinely-empty → материализует ["X"].
  • Reconcile «появляется РОВНО раз» сверен: un-expanded ветка не имеет react-query кэша → updateCacheOnMovePage (if(!old) return) ничего частичного не создаёт; на expand → isUnloadedBranch true → fetch полного серверного набора → appendChildren (dedup-by-id) → X один раз. Серверный move awaited, на failure — restore(before). Потери нет.
  • UX безопасен: teal-pulse таймер чистится на unmount (useEffect-cleanup), prefers-reduced-motion обработан, read-only/shared-дерево drag/drop не регистрирует (doc-tree-row:137).
  • Тесты не вакуумны — субагент подтвердил ПРОГОНОМ: мутация unloadedMakeChild=false → тест «UNLOADED does NOT materialize» краснеет.

Отличный фикс — закрывает мою вне-scope находку, переиспользуя общий предикат, reconcile корректен. Approve.

## 🤖 Авто-ревью · #532 (page-tree/dnd: drop-раскрытие + guard от потери детей, #523) — голова `27d51303`. Вердикт: **PASS** ✅ Это **фикс ровно той вне-scope потери данных, что я флагнул на #531** (локальный DnD make-child в незагруженную ветку материализовал `[source]` и скрывал серверных детей). Закрыт корректно. Стоит стопкой на #531 (заапрувлен). Эскалаций нет. Прогнал 9-аспектный веер (свёрнут в 3) + гейт (client vitest **94/94**). Все 9 аспектов — LGTM. Сверил лично: - **Guard переиспользует ОБЩИЙ `isUnloadedBranch`** (`use-tree-mutation.ts:83`) — тот самый single-source-предикат из #531 (не локальная копия, коммент явно это отмечает), значит анти-дрейф держится. - **Трихотомия корректна:** UNLOADED (`hasChildren:true, children:[]`) → `remove(source)` + `update(target,{hasChildren:true})`, БЕЗ материализации `[source]` (gate взведён); LOADED → позиционный `["c1","X"]` (без потери/дубля); genuinely-empty → материализует `["X"]`. - **Reconcile «появляется РОВНО раз» сверен:** un-expanded ветка не имеет react-query кэша → `updateCacheOnMovePage` (`if(!old) return`) ничего частичного не создаёт; на expand → `isUnloadedBranch` true → fetch полного серверного набора → `appendChildren` (dedup-by-id) → X один раз. Серверный move awaited, на failure — restore(before). Потери нет. - **UX безопасен:** teal-pulse таймер чистится на unmount (`useEffect`-cleanup), `prefers-reduced-motion` обработан, read-only/shared-дерево drag/drop не регистрирует (`doc-tree-row:137`). - **Тесты не вакуумны** — субагент подтвердил ПРОГОНОМ: мутация `unloadedMakeChild=false` → тест «UNLOADED does NOT materialize» краснеет. Отличный фикс — закрывает мою вне-scope находку, переиспользуя общий предикат, reconcile корректен. Approve.
agent_reviewer added review/approved and removed review/needs labels 2026-07-12 05:06:43 +03:00
vvzvlad merged commit 220142bef6 into fix/525-insert-unloaded 2026-07-12 05:08:44 +03:00
Sign in to join this conversation.