fix(editor): реактивное чтение editor.isEditable в DictationGroup — иконка диктовки больше не залипает серой (#311) #316

Merged
vvzvlad merged 1 commits from fix/311-reactive-editable into develop 2026-07-03 21:25:50 +03:00
Collaborator

Summary

На редактируемой странице микрофон диктовки в байлайне залипал серым/disabled после досинка коллаборации — до тех пор, пока что-то постороннее (переключение view↔edit, навигация) случайно не перерисует байлайн. DictationGroup читал НЕреактивное поле editor.isEditable прямо в рендере; editor приходит из pageEditorAtom (стабильная ссылка на объект), поэтому editor.setEditable(true) после синка (гейт #218) меняет внутреннее состояние TipTap, не меняя ссылку атома → байлайн не перерендеривается, disabled=true застревает.

Фикс — читать editable РЕАКТИВНО через useEditorState (тот же приём, что уже использует тело редактора): микрофон оживает, когда тело становится editable, и гаснет обратно при потере editable. Исходное намерение #218 (нет диктовки до досинка) сохранено — просто стало реактивным.

closes #311

How verified

  • vitest run dictation-group.test.tsx1 passed. Тест флипает isEditable false→true и ассертит переход микрофона disabled→enabled; против дореформенного кода (raw-field read без useEditorState) — падает (микрофон залипает).
  • tsc --noEmit — чисто по dictation-group; eslint — чисто (компонент + тест).

Checklist

  • иконка автоматически активируется при editable и деактивируется при потере — реактивно
  • намерение #218 сохранено; вне scope не менялось
## Summary На редактируемой странице микрофон диктовки в байлайне залипал серым/`disabled` после досинка коллаборации — до тех пор, пока что-то постороннее (переключение view↔edit, навигация) случайно не перерисует байлайн. `DictationGroup` читал НЕреактивное поле `editor.isEditable` прямо в рендере; `editor` приходит из `pageEditorAtom` (стабильная ссылка на объект), поэтому `editor.setEditable(true)` после синка (гейт #218) меняет внутреннее состояние TipTap, не меняя ссылку атома → байлайн не перерендеривается, `disabled=true` застревает. Фикс — читать editable РЕАКТИВНО через `useEditorState` (тот же приём, что уже использует тело редактора): микрофон оживает, когда тело становится editable, и гаснет обратно при потере editable. Исходное намерение #218 (нет диктовки до досинка) сохранено — просто стало реактивным. closes #311 ## How verified - `vitest run dictation-group.test.tsx` → **1 passed**. Тест флипает `isEditable` false→true и ассертит переход микрофона disabled→enabled; против дореформенного кода (raw-field read без `useEditorState`) — падает (микрофон залипает). - `tsc --noEmit` — чисто по `dictation-group`; eslint — чисто (компонент + тест). ## Checklist - [x] иконка автоматически активируется при editable и деактивируется при потере — реактивно - [x] намерение #218 сохранено; вне scope не менялось
agent_coder added 1 commit 2026-07-03 18:20:31 +03:00
On an editable page the dictation mic in the byline stayed grey/disabled after
collab finished syncing, until an unrelated re-render (view↔edit toggle,
navigation) happened. DictationGroup read the NON-reactive `editor.isEditable`
field directly in render; `editor` comes from pageEditorAtom (a stable object
reference), so `editor.setEditable(true)` after sync (#218 gate) mutates TipTap
state without changing the atom reference — the byline never re-renders and
disabled=true sticks.

Read editable via `useEditorState` (the same reactive read the editor body
already uses), so the mic re-enables when the body flips editable and disables
again when it loses editable. The #218 pre-sync intent is preserved — just made
reactive. Test flips isEditable false→true and asserts the mic goes
disabled→enabled (fails against the pre-fix raw-field read).

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

Ревью — #316 (editor: реактивное чтение editor.isEditable в DictationGroup, #311), round 1, head 0210faab, base develop

Scope: реальная дельта — ТОЛЬКО 0210faab поверх смердженного #305 (родитель 36b35395); 2 файла, 87 строк (dictation-group.tsx +12, НОВЫЙ .test +75).

Вердикт: PASS — точечный фикс реактивности верен, объективка зелёная, регрессий нет. Готово к мержу.

Веер (stability, regressions, test-coverage, coherence, conventions). Объективка запущена мной (детач 0210faab): client tsc --noEmit0; vitest dictation-group1 passed.

Подтверждено по коду + прогоны

  • Фикс верен. DictationGroup читал НЕреактивное мутабельное editor.isEditable напрямую (disabled={!editor.isEditable}) → мик застревал disabled на editable-странице, пока не случался несвязанный ре-рендер. Стало: useEditorState({ editor, selector: (ctx)=>ctx.editor?.isEditable ?? false }) — подписка на editor-события → ре-рендер при флипе после collab-sync. Зеркалит реактивное чтение самого body СИМВОЛ-В-СИМВОЛ (page-editor.tsx:362-367, тот же selector + ?? false-guard). Инлайн-selector — без stale-closure/лишних ре-рендеров (tiptap сравнивает ВЫХОД selector'а; boolean стабилен); ?? false null/destroyed-safe.
  • Регрессий нет. Steady editable → мик активен (как прежде); #218-намерение (pre-sync read-only → мик disabled) сохранено. Import type {Editor}{Editor, useEditorState} — Editor всё ещё как тип, паттерн уже есть в соседях, линтов нет. Единственный вызыватель (full-editor.tsx:255) под editor && …-гвардом.
  • Полнота. dictation-group — ЕДИНСТВЕННАЯ группа fixed-toolbar с прямым .isEditable-чтением; остальные ~25 прямых чтений — ProseMirror NodeViews (другая render-модель через transaction-lifecycle, не тот баг). Фикс полон в своём scope, вне-scope хвостов нет.
  • Тест не-вакуозен. Мок useEditorState faithful (ре-ран ТОЛЬКО через editor.on("update")-подписку, не безусловно); против pre-fix (!editor.isEditable, нет подписки) emit не ре-рендерит → финальный toBe(false) падает. Fake-editor surface адекватен (следует sibling-harness code-block-view.test). Reverse/null-пути ниже DROP-floor для one-liner-фикса.
## Ревью — #316 (editor: реактивное чтение editor.isEditable в DictationGroup, #311), round 1, head `0210faab`, base develop Scope: реальная дельта — ТОЛЬКО `0210faab` поверх смердженного #305 (родитель `36b35395`); 2 файла, 87 строк (dictation-group.tsx +12, НОВЫЙ .test +75). **Вердикт: PASS** — точечный фикс реактивности верен, объективка зелёная, регрессий нет. Готово к мержу. Веер (stability, regressions, test-coverage, coherence, conventions). **Объективка запущена мной** (детач `0210faab`): client `tsc --noEmit` → **0**; `vitest dictation-group` → **1 passed**. ### Подтверждено по коду + прогоны - **Фикс верен.** `DictationGroup` читал НЕреактивное мутабельное `editor.isEditable` напрямую (`disabled={!editor.isEditable}`) → мик застревал disabled на editable-странице, пока не случался несвязанный ре-рендер. Стало: `useEditorState({ editor, selector: (ctx)=>ctx.editor?.isEditable ?? false })` — подписка на editor-события → ре-рендер при флипе после collab-sync. Зеркалит реактивное чтение самого body СИМВОЛ-В-СИМВОЛ (`page-editor.tsx:362-367`, тот же selector + `?? false`-guard). Инлайн-selector — без stale-closure/лишних ре-рендеров (tiptap сравнивает ВЫХОД selector'а; boolean стабилен); `?? false` null/destroyed-safe. - **Регрессий нет.** Steady editable → мик активен (как прежде); #218-намерение (pre-sync read-only → мик disabled) сохранено. Import `type {Editor}`→`{Editor, useEditorState}` — Editor всё ещё как тип, паттерн уже есть в соседях, линтов нет. Единственный вызыватель (full-editor.tsx:255) под `editor && …`-гвардом. - **Полнота.** `dictation-group` — ЕДИНСТВЕННАЯ группа fixed-toolbar с прямым `.isEditable`-чтением; остальные ~25 прямых чтений — ProseMirror NodeViews (другая render-модель через transaction-lifecycle, не тот баг). Фикс полон в своём scope, вне-scope хвостов нет. - **Тест не-вакуозен.** Мок `useEditorState` faithful (ре-ран ТОЛЬКО через `editor.on("update")`-подписку, не безусловно); против pre-fix (`!editor.isEditable`, нет подписки) emit не ре-рендерит → финальный `toBe(false)` падает. Fake-editor surface адекватен (следует sibling-harness `code-block-view.test`). Reverse/null-пути ниже DROP-floor для one-liner-фикса. <!-- state:review reviewed_head=0210faabea1b round=1 verdict=pass -->
agent_reviewer added review/approved and removed review/needs labels 2026-07-03 19:01:38 +03:00
Collaborator

(служебное: коррекция маркера — в прошлом маркере round-1 PASS я опечатался в укороченном sha; ниже полный. Код не менялся, вердикт прежний: PASS.)

_(служебное: коррекция маркера — в прошлом маркере round-1 PASS я опечатался в укороченном sha; ниже полный. Код не менялся, вердикт прежний: PASS.)_ <!-- state:review reviewed_head=0210faabeaa31ad8c8d8ac902937bb78bc57dd78 round=1 verdict=pass -->
vvzvlad merged commit 49c7c4bb64 into develop 2026-07-03 21:25:50 +03:00
vvzvlad deleted branch fix/311-reactive-editable 2026-07-03 21:25:55 +03:00
Sign in to join this conversation.