feat(comment): Undo в тосте резолва (reopen) + сортировка Resolved по времени резолва (#542) #548
Reference in New Issue
Block a user
Delete Branch "feat/542-resolve-undo"
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?
Summary
Ключевые решения
variables.resolved===true), вonSuccess(после подтверждения сервером, НЕonMutate). Reopen — обычный тост без Undo.let done=false— reopen шлётся ровно один раз (notifications.hideасинхронный, кнопка кликабельна кадр-два).CommentListItemк клику уже размонтирован):editorRef(живойpageEditorAtom, обновляется каждый рендер) +mutationRef(объявлен доuseMutation, присвоен после, читается в момент клика — цикла нет). Снятие инлайн-метки под!isDestroyed+try/catch (тост живёт 10с, панель может закрыться).removeCommentFromCache+unsetComment(безусловно, иначе висячая.comment-mark) + нейтральноеComment no longer exists; НЕ откатывать (фантом в Resolved не воскресает), НЕ показывать Undo.Failed to re-open commentдля reopen vsFailed to resolve commentдля резолва (было всегда «resolve»).new Date(x.resolvedAt).getTime()DESC (обязательноnew Date(): типDate, но в рантайме ISO-строка из axios-JSON/WS — сырой.getTime()даёт NaN/throw). Open не тронут.RESOLVE_UNDO_AUTOCLOSE_MS = 10000.How verified (на стенде)
comment-query.resolve-undo.test.tsx(6): Undo только на резолв + autoClose=10000 (крит 1,2,5); guard шлёт reopen ровно раз + скрывает тост + снимает метку (крит 3); 404 → drop из кэша +unsetComment+ нейтрал, без отката/Undo (крит 4); 404 при editor=null не падает (крит 7); направленная copy (крит 6).comment-list-with-tabs.test.tsx+4: ISO-string DESC (крит 8), Date-instance, без мутации входа, стабильность равных меток; Open не тронут (крит 9).tsc0..getTime()→ ISO-sort краснеет; 404 data-integrity зелёный) → APPROVE.Comment no longer exists,Failed to re-open comment) в en-US + ru-RU. node_modules 0.Осознанная граница scope / SUGGESTION
COMMENT_MARK_UPDATE— вне юнит-контракта (тесты проверяют проводку/эффекты, не рендер Mantine).Checklist
Ревью — Undo в тосте резолва + сортировка Resolved (#542). Вердикт: changes-requested
Работа аккуратная: toast-closure переживает unmount
CommentListItemкорректно (RQ v5 зовёт onSuccess/onError даже после unmount observer;editorRef/mutationRefживы, читаются в момент клика — без stale-closure/цикла), double-click guard (doneсинхронно) и sort ([...resolved].sort— КОПИЯ, не мутирует RQ-кэш; DESC, стабильна, callers пре-фильтруют truthy resolvedAt) — оба залочены мутацией. i18n в обоих локалях, authz сервер перепроверяет. Regressions/test-coverage/conventions/security/simplification — LGTM. Эскалаций нет.Открыто:
📋 F1 — детали
F1 [warning · stability/coherence] eager mark-clear vs откат onError —
apps/client/src/features/comment/queries/comment-query.ts:534-541Undo-onClick чистит inline-марку СРАЗУ (
ed.commands.setCommentResolved(commentId, false),:537) в тот же момент, что запускает reopen-мутацию — ДО того как известен результат reopen. Если reopen падает non-404 (5xx/сеть):onError(:462-466) откатывает RQ-кэш обратно в resolved (коммент остаётся в Resolved-табе), НО марка в ProseMirror/collab-доке уже перевёрнута вresolved=falseи задиспатчена. Итог: документ показывает активный (unresolved) хайлайт для коммента, который панель числит resolved, и сервер ничего не коммитнул (500) → нечему примирить; марка живёт в доке, не в comments-query, поэтому рефетч комментов НЕ лечит (и, будучи collab-шагом, может разъехаться и у других клиентов).Асимметрично с устоявшимся БЕЗОПАСНЫМ паттерном
comment-list-item.tsx:108-118handleResolveComment: тамawait mutateAsync()иsetCommentResolvedТОЛЬКО после успеха → на фейле марка не тронута. Триггер: любой network/5xx на reopen после клика Undo. Это прямой ответ на крюкс «reopen и mark-clear происходят, но НЕ остаются консистентны на фейле».Fix: убрать eager-блок
setCommentResolved(commentId,false)из onClick (:534-541); чистить марку в reopen-веткеonSuccess— вif (!variables.resolved)(:493): прочитатьeditorRef.current, если есть и!isDestroyed—ed.commands.setCommentResolved(variables.commentId,false)в try/catch (зеркалит 404-ветку:445-452). Это привязывает смену марки к серверному подтверждению, откат-на-фейле бесплатно. Обычный кнопочный reopen и так ставит марку сам — лишний вызов там идемпотентный no-op. Добавь тест: reopen ПАДАЕТ → марка НЕ оставлена очищенной (doc↔панель консистентны).9 аспектов (2 агента + мой разбор крюкса). Гейт: client vitest 19/19 (double-click guard + sort — мутации краснеют). Дерево после мутаций чисто. База develop
03eafa6c. ⛔ кодеру НЕ делать: unmount не прогнан литерально черезunmount()(рисковый путь всё равно драйвится; дельта — библиотечное поведение RQ) + null-editorRef degrade-ветка не покрыта — below-threshold, но добавь во второй тест при фиксе F1.Ответ на ревью (F1)
F1 [warning · stability/coherence] — eager mark-clear vs откат onError — закрыто.
Причина: Undo-onClick чистил inline-марку СРАЗУ (
ed.commands.setCommentResolved(commentId,false)) в момент клика, ДО подтверждения reopen. При non-404 фейле (5xx/сеть)onErrorоткатывал RQ-кэш обратно вresolved, но марка в ProseMirror/collab-доке уже была перевёрнута — документ показывал активный хайлайт для коммента, который панель числит resolved, и рефетч комментов это не лечил (марка живёт в доке, не в comments-query).Фикс (
comment-query.ts): eager-блок удалён из onClick целиком (остались только триггер reopen-мутации, guard двойного кликаdoneиnotifications.hide). Очистка марки перенесена в reopen-веткуonSuccess— внутриif (!variables.resolved): читаемeditorRef.current, проверяем!isDestroyed, вызываемsetCommentResolved(variables.commentId,false)в try/catch — структурно идентично 404-ветке. Теперь смена марки привязана к серверному подтверждению: на пути ошибки марка не трогается вовсе, откат бесплатен, doc↔панель консистентны. На пути resolve (variables.resolved===true) не срабатывает — ошибочного снятия марки при резолве нет. Обычный кнопочный reopen и так ставит марку сам → лишний вызов идемпотентный no-op (конвергентная транзакция).Тесты (
comment-query.resolve-undo.test.tsx, +2):setCommentResolved(...,false)НЕ вызван, RQ-кэш откатывается в resolved (ассерт на факт-невызова, не на отсутствие тоста).editorRefна success-пути → graceful no-op (без throw, reopen-тост показан, кэш reopened).Существующий тест «марка снята при УСПЕШНОМ reopen» по-прежнему зелёный (теперь через onSuccess).
Верификация: comment-feature suite 106/106 (resolve-undo 8/8, было 6). Мутация (вернуть eager-clear в onClick) → краснеет ровно fail-path тест; восстановлено → зелено. Скоуп коммита:
comment-query.ts+ тест, 0 node_modules. Ветка на уровне develop (0 позади), конфликтов нет.внутренний цикл: 1 проход внутреннего ревью на этот фикс — APPROVE, изоляция подтверждена мутацией, happy-path и guard'ы двойного клика/hide сохранены.
Новый head:
d36021a1.Ре-ревью — resolve Undo/sort (#542), раунд 2. Вердикт: approved ✅
F1 (расхождение doc↔панель на фейле reopen) закрыт, проверил адверсариально:
mutate({resolved:false})+notifications.hide, с комментом «mark cleared in onSuccess, NOT here — clearing eagerly would desync doc from panel if reopen fails».comment-query.ts:499-502,if(!variables.resolved)→setCommentResolved(commentId,false)с editor-liveness guard + try/catch) — ПОСЛЕ серверного подтверждения. Кнопочный reopen уже ставит марку сам → идемпотентный no-op.Гейт: client vitest 21/21 (было 19 + 2 фейл-теста). Дерево после мутаций чисто. На фейле reopen кэш и марка теперь остаются консистентны. Готово к мержу.