Skip to content

Collaborative script editing: line and text editing on the draft (Phase 3, Step 3) - #1479

Merged
Tim020 merged 5 commits into
devfrom
feature/collab-editing-v3-p3-step3-line-editing
Oct 26, 2026
Merged

Tim020 merged 5 commits into
devfrom
feature/collab-editing-v3-p3-step3-line-editing

Conversation

@Tim020

@Tim020 Tim020 commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Draft-aware line editing for client-v3's collaborative script editor: a useYTextField composable for caret-anchored live text binding, CollabScriptLineEditor.vue/CollabScriptLinePart.vue as draft-aware siblings of the classic editor components (writes go straight through useDraftWriter the moment they happen), and _uid-keyed open-editor tracking plus add/insert/delete line in CollabScriptEditor.vue.
  • D1 decided (Tim): incomplete lines keep the atomic abort on save, implemented in Step 4; this step adds the client-side Vuelidate gating so Save can disable itself with a reason once it exists.
  • Found while landing this step's own E2E, not a bug: once a line is actually edited the draft becomes dirty and (per D9) persists across Stop Editing, so turning collaborative_script_editing back off is blocked until the draft is discarded — there's no Discard-draft UI yet (Step 5). Spec 15's own test cleanup now calls the existing admin REST endpoint directly; documented in the plan doc as expected behaviour while the setting stays hidden.

Test plan

  • 290 Vitest (27 new), tsc/eslint/prettier/build clean
  • Full Playwright suite, both chromium and firefox, 238/238, no retries
  • Spec 15 extended: add a dialogue line with act/scene/character/text; typed text survives a reload (proves it's written to the shared draft, not local state); insert and delete a line from a row's menu

🤖 Generated with Claude Code

https://claude.ai/code/session_01TjTnfchFvvQvKZUZv2jpT8

…se 3, Step 3)

Draft-aware line editing for client-v3's collaborative script editor, writing
straight through useDraftWriter the moment each change happens rather than
staging in local state:

- useYTextField: binds a text input to a live Y.Text, keeping it in sync with
  remote edits and anchoring a focused caret to its character (via Yjs relative
  positions) rather than a raw index, so a concurrent edit earlier in the field
  doesn't make the caret jump. Distinguishes local vs remote writes by
  transaction origin so only remote edits need the caret actively restored.
- CollabScriptLineEditor.vue / CollabScriptLinePart.vue: draft-aware siblings of
  the classic ScriptLineEditor/ScriptLinePart, since those are local-state and
  index-based with no Yjs awareness. Act/scene/character/style selects and part
  add/remove write directly through useDraftWriter; Vuelidate validation
  mirrors the classic rules.
- CollabScriptEditor.vue: wires ScriptLineViewer's existing edit/insert/delete
  emits to the new editor, _uid-keyed open-editor tracking (no shift-reindexing
  needed on insert/delete, unlike the classic index-keyed version), and the
  Add Dialogue/Stage Direction/Cue Line/Spacing button group.

D1 decided (Tim): incomplete lines keep the atomic abort on save (Step 4), with
client-side Vuelidate gating added here so Save can disable itself with a
reason — implemented in Step 4 since there's no Save button yet.

Found while landing this step's own E2E, not a bug: once a line is actually
edited the draft becomes dirty, and per D9 (Stop Editing keeps the draft) that
persists to disk when the last editor leaves. has_unsaved_changes() then blocks
turning collaborative_script_editing back off until the draft is discarded,
and there's no Discard-draft UI yet (Step 5) — spec 15's own cleanup now calls
the existing admin REST endpoint directly. Documented in the plan doc.

Verified: 290 Vitest (+27), tsc/eslint/prettier/build clean, full Playwright
238/238 in both chromium and firefox, no retries.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TjTnfchFvvQvKZUZv2jpT8
@Tim020 Tim020 added the claude Issues created by Claude label Oct 26, 2026
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Client Test Results

102 tests   102 ✅  0s ⏱️
  9 suites    0 💤
  1 files      0 ❌

Results for commit 2b7ebca.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Client V3 Test Results

299 tests   299 ✅  1s ⏱️
 26 suites    0 💤
  1 files      0 ❌

Results for commit 2b7ebca.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Python Test Results

    1 files      1 suites   1m 34s ⏱️
1 270 tests 1 270 ✅ 0 💤 0 ❌
1 301 runs  1 301 ✅ 0 💤 0 ❌

Results for commit 2b7ebca.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Playwright E2E Results (firefox)

238 tests   238 ✅  2m 29s ⏱️
 15 suites    0 💤
  1 files      0 ❌

Results for commit 2b7ebca.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Playwright E2E Results (chromium)

238 tests   238 ✅  2m 33s ⏱️
 15 suites    0 💤
  1 files      0 ❌

Results for commit 2b7ebca.

♻️ This comment has been updated with latest results.

Quality gate failed on new-code duplication (10.9%, required <=3%) and a
Reliability rating (missing input label). Fixes, no behaviour change:

- Extract computeValidActs/computeValidScenes (src/js/scriptLineActScene.ts),
  characterSelectOptions/characterGroupSelectOptions/
  stageDirectionStyleSelectOptions (src/js/scriptLinePartOptions.ts), and
  fieldState/combinedFieldState (src/js/vuelidateFieldState.ts) out of
  CollabScriptLineEditor.vue/CollabScriptLinePart.vue, which had copied these
  near-verbatim from the classic ScriptLineEditor.vue/ScriptLinePart.vue (same
  business rules, different write path). Pure functions, not touching the
  classic components.
- Associate CollabScriptLinePart.vue's text input with a label (was missing
  one entirely after swapping BFormInput for a native <input> in Step 3).
- Dedupe the two new component test files' stub boilerplate into
  testStubs.ts, and the two near-identical "add a dialogue line" E2E flows
  (spec 10's pre-existing one is untouched) behind a new
  fillOpenLineEditor() helper in e2e/helpers.ts.

Verified: 290 Vitest, tsc/eslint/prettier/build clean, full Playwright
238/238 in both chromium and firefox, no retries.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TjTnfchFvvQvKZUZv2jpT8

@Tim020 Tim020 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review round 1

Head SHA reviewed: cd77bac22703ffdcc1db71f396723325f8bb35ec

Verdict: changes needed. This is a well-structured step in the collaborative-editing rebuild (the Yjs text binding, room lifecycle, and reconnect handling are all solid), but there's one real functional regression and one real validation-correctness bug, plus a perf issue and several test gaps worth closing before merge.

Previous round follow-up: none — this is round 1.

SonarQube: Quality gate PASSED (all 5 conditions green: new reliability/security/maintainability ratings, duplication, hotspots-reviewed). 4 new MINOR code-smell issues were reported (none gate-blocking); all 4 are included as inline suggestions below (testStubs.ts:16, scriptLineActScene.ts:24/40/47). No security hotspots. Data confirmed fresh for this head SHA via anonymous SonarCloud API (MCP Sonar tools were unavailable — auth error on the MCP side only; anonymous API filled the gap cleanly since the project is public).

New findings: 2 Critical, 4 Important, 12 Suggestions (4 of which are SonarQube-sourced) posted inline, plus the non-line-anchored items below.

Findings without a specific diff line to anchor to:

  • (Important) scriptLineActScene.ts's computeValidActs/computeValidScenes — new act/scene chain-traversal logic extracted specifically to de-duplicate the classic and collaborative editors — has no test file at all. No test constructs a multi-act/multi-scene chain, so a traversal bug (e.g. a cycle via next_act, or previousLine.act_id not found in acts) wouldn't be caught. Suggest scriptLineActScene.test.ts.
  • (Important) CollabScriptLinePart.vue's combined-character/group dropdown path (combinedValue getter/setter, lines 201-216) is completely untested — no test ever sets character_combined_dropdown: true. This is also the exact path the testStubs.ts Number.parseInt issue (inline below) currently blocks from being tested.
  • (Important) The E2E spec's new Step-3 tests (adds a dialogue line..., typed text survives a reload..., inserts a line from a row menu...) never use the second browser (page2) that the earlier a second browser can edit... test opened — so the core claim of this step, "another editor sees it immediately" (per this PR's own doc comments), is never verified against a second real client over the actual WebSocket sync path, only in-process against a shared Y.Doc in useYTextField.test.ts.
  • (Suggestion) CollabScriptEditor.vue's Add Stage Direction / Add Cue Line / Add Spacing buttons, and the insert-dialogue/insert-cue-line viewer emits, are untested — only addNewLine(DIALOGUE) and insertLineAt(STAGE_DIRECTION) are exercised.
  • (Suggestion) scriptLinePartOptions.ts's characterGroupSelectOptions and stageDirectionStyleSelectOptions are never called by a test with real data.
  • (Suggestion) useYTextField.test.ts's observer-switch test never confirms the old Y.Text's observer was actually removed (only that the new one works) — an observer leak wouldn't be caught.
  • (Suggestion) CollabScriptLineEditor.vue:216-224 / CollabScriptEditor.vue:218: opening a zero-part line's editor writes a new part as a side effect of mounting, and a reconnect resync that remounts an already-open editor re-grabs input focus. Both are narrow edge cases (concurrent part-removal; resync timing) but worth a second look.
  • (Suggestion) useYTextField.ts's doc comment for inputRef ("Bind this to the input element (ref=\"inputRef\")") prescribes a destructured top-level binding that the only real consumer, CollabScriptLinePart.vue, doesn't use (it assigns .value manually in onMounted instead, since it needs its own separate template ref for defineExpose). Worth updating the comment to show the pattern actually in use.

Strengths:

  • The useYTextField Yjs binding itself is sound: local writes are tagged with LOCAL_EDIT_ORIGIN so the observer skips redundant caret restoration, remote updates restore the caret by character position (not raw index) via captureSelection/resolveSelection, and the observer is cleanly removed in onUnmounted.
  • CollabScriptEditor.vue's room/sync lifecycle tests (pre-existing from earlier steps) are unusually rigorous, several with comments naming the exact mutant they're designed to catch.
  • The E2E afterAll's handling of the unsaved-draft-blocks-mode-flip interaction is a well-reasoned, documented workaround for a real sequencing constraint, not a hack.
  • testStubs.ts's dedup of BVN component stand-ins across the three new test files is a clean, low-risk refactor.

🤖 Generated with Claude Code

Comment thread client-v3/src/js/vuelidateFieldState.ts Resolved
Comment thread client-v3/src/js/scriptLineActScene.ts Resolved
Comment thread client-v3/e2e/helpers.ts Resolved
Comment thread client-v3/src/js/scriptLineActScene.ts Resolved
Comment thread client-v3/src/js/scriptLineActScene.ts Resolved
Comment thread client-v3/src/js/scriptLineActScene.ts Resolved
Two real Criticals, found by review, not by me:

1. New lines never inherited the previous line's character. addLine()
   already seeds the first part in the same transaction, so
   CollabScriptLineEditor's onMounted seeding (guarded on
   line_parts.length === 0) never ran for a freshly-created line. Fixed by
   seeding the character directly in addNewLine/insertLineAt, the same
   lookup addLinePart() already does for a part added later.
2. combinedFieldState implemented OR-of-validity, not the XOR its own doc
   comment promised: a part with both character_id and character_group_id
   set (the state assertNotBoth forbids on write) reported as valid, since
   neither field had an active `required` rule once the other was set.
   Fixed at the source: a notBothCharacterFields validator on both fields,
   so $error correctly reflects the real invariant and the existing
   OR-of-no-error logic in combinedFieldState becomes correct automatically.

Also addressed, all Important/Suggestion:
- CollabScriptLinePart.vue's tmpScript conversion ran on every keystroke in
  every open part regardless of the MRU setting; gated behind it.
- scriptLineActScene.ts/scriptLinePartOptions.ts's doc comments claimed
  classic+collab sharing that was never actually wired up; reworded to
  "mirrors the classic version, keep in sync" instead, and the latter's
  duplicate NamedItem/TmpScript types now reuse mruSortUtils.ts's (now
  exported) originals.
- CollabScriptLineEditor.vue's actState/sceneState hand-rolled the logic
  the new fieldState() helper already exists for; now uses it.
- useYTextField.ts had no IME composition handling (intermediate CJK/
  dead-key state was written to the shared Y.Text, and a remote update
  mid-composition disrupted it); added compositionstart/compositionend
  handling, wired into CollabScriptLinePart.vue along with the missing
  @select caret-activity binding its own doc comment already promised.
  Also narrowed its `value` to Readonly<Ref<string>>.
- The two new test files' stub boilerplate overlap is gone (testStubs.ts
  already existed); SelectStub now renders <optgroup> and passes non-numeric
  values through, enabling a real combined-dropdown test that didn't exist
  before; a test promising three emits only checked one, split into three;
  CollabScriptLineEditor.test.ts's PartStub was missing the
  stage-direction-style-change emit declaration, with no test exercising it
  or try-finish-line's lineValid guard — both added.
- E2E: the cleanup DELETE's response went unchecked; now asserted. The
  helper's select.nth(0)/.nth(1) was a latent ambiguity; both line editors
  now carry data-testid="line-act-select"/"line-scene-select" and the
  helper uses those instead.

Verified: 299 Vitest (+7), tsc/eslint/prettier/build clean, full Playwright
238/238 in both chromium and firefox, no retries. The two Critical fixes
and the combined-dropdown/IME additions were each verified by reverting
the fix and confirming the new test fails, then restoring it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TjTnfchFvvQvKZUZv2jpT8
@Tim020

Tim020 commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Addressed round 1 in 7186fc4 — all 18 comments, replied inline on each thread.

Two real Criticals, found by review, not by me:

  1. New lines never inherited the previous line's character — addLine() already seeds the first part, so CollabScriptLineEditor's own onMounted seeding (guarded on line_parts.length === 0) never ran for it. Fixed by seeding directly in addNewLine/insertLineAt.
  2. combinedFieldState implemented OR-of-validity, not the XOR it documented — a part with both character_id and character_group_id set reported as valid. Fixed at the source with a notBothCharacterFields validator, so $error reflects the real invariant.

All Important/Suggestion items also addressed: the unconditional tmpScript recompute on every keystroke, two inaccurate "shared with classic" doc comments (reworded), duplicate types, actState/sceneState not using the extracted fieldState() helper, missing IME composition handling in useYTextField (a real gap for CJK/dead-key input), a missing @select binding, a non-readonly exposed Ref, test coverage gaps (including one that enabled a real combined-dropdown test that didn't exist before), an unchecked E2E cleanup response, and a latent .nth() selector ambiguity.

Verification: 299 Vitest (+7), tsc/eslint/prettier/build clean, full Playwright 238/238 in both chromium and firefox, no retries. The two Criticals and the new IME/combined-dropdown coverage were each verified by reverting the fix and confirming the new test fails, then restoring it.

@Tim020 Tim020 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review round 2

Head SHA reviewed: 7186fc4fcedc297ca76215662fb1b8d68abef9c4

Verdict: ready to merge, pending the one small new finding below (optional — not gate-blocking).

Previous round follow-up: 18/18 threads verified fixed and resolved. All fixes were traced against the actual code (not just taken on the author's word) and, where the original finding was a logic/correctness issue, against the new tests added to cover them:

  • Both Critical findings are genuinely fixed: seedFirstPart() correctly seeds the character from the anchor line for both addNewLine and insertLineAt (traced into draftWriter.addLine's part option and its assertNotBoth check), and notBothCharacterFields fixes combinedFieldState's invariant at the source (verified the Vuelidate v2 two-arg validator signature is used correctly) rather than patching the symptom.
  • All 4 Important findings fixed: the tmpScript perf issue is now correctly gated behind the MRU setting (no more phantom reactive dependency when it's off), both "shared between classic and collaborative" doc comments were rewritten to state the truth (duplication, not sharing) and verified still accurate against the unchanged classic components, NamedItem/TmpScript duplication was resolved by exporting from mruSortUtils.ts, and actState/sceneState now use the shared fieldState() helper.
  • All 12 Suggestions fixed, including two that turned into real new test coverage: the combined character/group dropdown is now actually testable (the stub needed <optgroup> support, which the author discovered empirically) and has a test, and IME composition handling landed with two tests covering intermediate-state isolation and remote-edit-during-composition safety.
  • Full links: see the resolved threads on this PR.

SonarQube: Quality gate PASSED (all 5 conditions green). The 4 prior MINOR issues are gone. One new MAJOR issue appeared, introduced by this round's own fix (see inline comment) — not gate-blocking, but worth a quick cleanup. Source: anonymous SonarCloud API (public project), confirmed fresh for head 7186fc4.

New findings: 1 Suggestion (SonarQube-sourced), posted inline. No other new issues found in the fix commit — draftWriter.addLine's part handling, the Vuelidate sibling-validator usage, and the new insertLineAt test's component-lookup logic were all traced through and check out.

Strengths: Every fix was accompanied by a targeted regression test, and the commit message states each test was confirmed failing before the fix and passing after — genuine TDD discipline on a review response, not just code changes. The data-testid fix for the locator-ambiguity finding was applied to both line editors (collaborative and classic) rather than just the one in scope, keeping the shared E2E helper's doc comment honest.

🤖 Generated with Claude Code

SonarQube typescript:S3358, introduced by round 1's own fix (the stub
needed to pass non-numeric combined-dropdown values through). Pulled the
inner numeric check into parseSelectValue() so the outer expression no
longer nests.

Verified: 299 Vitest, tsc/eslint/prettier/build clean, full Playwright
238/238 in both chromium and firefox, no retries.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TjTnfchFvvQvKZUZv2jpT8
@Tim020

Tim020 commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Addressed round 2's one new finding in 55b706d — extracted the nested ternary in testStubs.ts into parseSelectValue(). Verified: 299 Vitest, tsc/eslint/prettier/build clean, full Playwright 238/238 in both chromium and firefox, no retries.

@Tim020 Tim020 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review round 3

Head SHA reviewed: 55b706d0ea94f93f800ab8e1b235b3858fcbedb1

Verdict: ready to merge.

Previous round follow-up: 1/1 verified fixed and resolved. The round-2 SonarQube finding (nested ternary in testStubs.ts) was fixed with exactly the suggested extraction — parseSelectValue() now holds the inner check, and the outer expression is a single non-nested ternary.

Correction, unrelated to this round's code: a script bug on my end meant the 18 verification replies I posted in round 2 were saved to GitHub with the literal text @- instead of their intended content (the replies themselves had already correctly driven thread resolution — only the displayed text was wrong). All 18 have now been edited in place with the correct verification text and a note explaining the correction. No action was needed from the PR author for this; flagging it here for the record.

SonarQube: Quality gate PASSED. 0 new issues (down from 1). Source: anonymous SonarCloud API (public project), confirmed fresh for head 55b706d.

New findings: none. This commit is a minimal, mechanical extraction — verified it's the only change in the diff, and all CI checks (SonarCloud, typecheck, lint, Python tests) are green on this head.

Strengths: Three clean rounds — two real Critical bugs and a long tail of Important/Suggestion findings, each fixed precisely as scoped with no regressions introduced, and the one regression this process did introduce (the nested ternary) was itself caught and fixed within a round. This PR is in good shape to merge.

🤖 Generated with Claude Code

@sonarqubecloud

Copy link
Copy Markdown

@Tim020
Tim020 merged commit 12a5f72 into dev Oct 26, 2026
28 checks passed
@Tim020
Tim020 deleted the feature/collab-editing-v3-p3-step3-line-editing branch October 26, 2026 16:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude Issues created by Claude client-v3 xlarge-diff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant