fix(temporary-notes): tree clock marker updates without reload + mobile-friendly full-width create buttons #225
Reference in New Issue
Block a user
Delete Branch "fix/temporary-notes-ui"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Что
Два UI-бага фичи временных заметок (#201/#215, уже влита в develop):
1. Маркер-часики в дереве не обновлялся до релоада
Баннер на странице обновлялся сразу, а оранжевый 🕐 на узле сайдбара — только после reload. Три причины, все исправлены (маркер обновляется без перезагрузки):
syncTemporaryExpiresInCache()патчил только кэш страницы, неtreeDataAtom. ДобавленgetDefaultStore()+treeModel.update(...).invalidateOnCreatePage()строилnewPageбезtemporaryExpiresAt, и перестройка дерева (buildTree→mergeRootTrees) затирала оптимистичный/сокетный маркер наundefined. Добавлено поле.temporaryExpiresAtпроброшен через серверный broadcastaddTreeNode(TreeNodeSnapshot,PAGE_CREATEDвpage.repo,broadcastPageCreated); idempotency-guardhandleCreateдополняет дедлайн, если broadcast выиграл гонку.2. Кнопки «New note» / «New temporary note» не влезали на мобиле
В
new-note-button.tsxиspace-create-note-buttons.tsx: side-by-sideGroup grow→ вертикальныйStack, обеfullWidth, временная кнопкаcolor="orange", обычнаяgray(чуть разные цвета). Текст «New temporary note» больше не обрезается.Проверка (Playwright, живой стенд)
rgb(73,80,87)vs orangergb(253,126,20)).tscexit 0;tree-socket-reducers16/16,ws-tree.service+page-ws.listener25/25.Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
🤖 Generated with Claude Code
Code review — multi-aspect (8 аспектов параллельно)
Вердикт: Approve with comments (одобрить с замечаниями).
Изменение узкое, корректное и хорошо прокомментированное. 6 из 8 аспектов — чисто (security, stability, conventions, documentation, regressions, simplification). Логику гонки независимо подтвердили рецензенты по stability и regressions: патч только добавляет дедлайн и никогда не затирает существующий. Блокирующих находок нет. Единственное замечание — ядро багфикса (ветка race-guard в
handleCreate) уезжает без юнит-теста, хотя ручная Playwright-проверка и тест socket-пути в PR есть.Область ревью:
git diff --merge-base develop(базаdevelop@e9409e24, головаfix/temporary-notes-ui@12ff76fb), 10 файлов, +92/−14. Лок-файлов/генерёжки нет.Must fix before merge
[warning][test coverage] Покрыть тестом ветку race-guard в
handleCreate—apps/client/src/features/page/tree/hooks/use-tree-mutation.ts:176-191Это ядро багфикса, но оно не покрыто ни одним тестом. У ветки два различимых поведения, которые ничто не проверяет: (а) когда серверный
addTreeNode-броадкаст выиграл гонку и вставил узел безtemporaryExpiresAt— дописать дедлайн из ответа на создание (newNode.temporaryExpiresAt && !existing.temporaryExpiresAt→treeModel.update); (б) явный guard «НЕ затирать», когда у существующего узла дедлайн уже есть. Существующий блок «handleCreate optimistic-insert idempotency» вtree-model.test.tsвоспроизводит лишь старую логику find-then-skip и новую ветку не включает; единственный добавленный тест вtree-socket-reducers.test.tsпокрывает другой путь (applyAddTreeNode). Условие «не затирать» — ровно тот guard, который будущий рефакторинг может молча сломать без срабатывания тестов.Fix: добавить тест (по образцу блока idempotency в
tree-model.test.ts), прогоняющий обе под-ветки: (1) узел уже есть безtemporaryExpiresAt+ ответ несёт дедлайн → у узла появляетсяtemporaryExpiresAt; (2) узел уже есть сtemporaryExpiresAt→ значение не перезаписывается,prevвозвращается по ссылке.[suggestion][test coverage] Расширить тест
broadcastPageCreated, проверив переносtemporaryExpiresAt(и нормализацию?? null) —apps/server/src/ws/ws-tree.service.spec.ts:69Новая строка
temporaryExpiresAt: page.temporaryExpiresAt ?? nullвws-tree.service.ts:41-44— серверная половина фикса, но существующий тест проверяетdataчерезexpect.objectContaining, а фикстураsnapshotполя не содержит. Именно поэтому сьют «проходит 25/25», а новое поле и нормализация absent→null остаются непроверенными.Fix: добавить в фикстуру вариант с
temporaryExpiresAtи проверить, что броадкаст его несёт; добавить второй кейс без поля и проверитьdata.temporaryExpiresAt === null, зафиксировав?? null.[suggestion][test coverage] Покрыть патч
treeDataAtomвsyncTemporaryExpiresInCache—apps/client/src/features/page-embed/queries/page-embed-query.ts:37-49Новый патч содержит ветвление: чтение
treeDataAtomиз дефолтного стора jotai,treeModel.update, запись только при изменении ссылки (if (nextTree !== prevTree) store.set(...)). Оба пути (страница есть/нет в дереве) — новое поведение, движущее «маркер без релоада», теста у функции нет. Легче предыдущего, т.к. самtreeModel.updateхорошо покрыт.Fix: vitest на оба случая — узел в дереве получает
temporaryExpiresAt; id отсутствует в дереве → значение атома остаётся той же ссылкой.Test coverage
Новой логики совсем без тестов по существу две единицы: ветка race-guard в
handleCreate(warning) и патчtreeDataAtomвsyncTemporaryExpiresInCache(suggestion). Поле-«плимбинг» (invalidateOnCreatePage, снапшот вpage.repo.ts, поле вTreeNodeSnapshot) — однострочный перенос без ветвлений, тестов не требует. UI-изменения (Group→Stack,fullWidth,color) презентационные, ниже планки тестирования проекта. Добавленный тест наapplyAddTreeNode— корректный и соразмерный для покрытого им socket-пути.Architecture & design
1. Клиентский tree-sync: пять мест ручной сборки
IPage → SpaceTreeNode.Один и тот же набор полей вручную копируется в 5 местах:
buildTree(utils.ts — канонический маппер, уже несётtemporaryExpiresAt), оптимистичный узел вhandleCreate,newPageвinvalidateOnCreatePage(этот PR добавил поле),nodeDataвuseRestorePageMutation(не несёт) иtreeNodeDataвhandleDuplicatePage(не несёт). Это структурная причина класса багов «забыли пронести поле X — маркер пропадает до релоада». Пути restore/duplicate не сломаны сегодня только по совпадению: restore явно сбрасываетtemporaryExpiresAt: nullна сервере, а duplicate дедлайн не копирует, так что обе делают заметки постоянными.единый
pageToTreeNode(page)маппер*, через который ходятbuildTree,handleCreate, restore и duplicate (effort: small). Pros: новое поле добавляется в одном месте, убирает 4 почти-дубликата, закрывает корень класса багов. Cons: у площадок есть мелкие отличия (name: title || "Untitled",canEdit: true) → «map + overrides», аinvalidateOnCreatePageстроит запись IPage-кэша, неSpaceTreeNode.2. Серверный снапшот броадкаста: два литерала против
TreeNodeSnapshot.Узел
addTreeNodeсобирается двумя ручными литералами — вpage.repo.tsиws-tree.service.ts, оба обязаны соответствовать интерфейсуTreeNodeSnapshot. Мягче клиентской: литерала всего два и они уже делят общий тип (TS ловит пропуск обязательного поля; слепое пятно — только опциональные).хелпер
toTreeNodeSnapshot(page)*, вызываемый из обоих мест (effort: small). Cons: площадки чуть различаются (hasChildren: false,children: []) → нужны overrides.