Skip to content

feat(editor): migrate rich text editing to Tiptap#5148

Open
rschlaefli wants to merge 29 commits into
v3from
migrate-editor-to-tiptap
Open

feat(editor): migrate rich text editing to Tiptap#5148
rschlaefli wants to merge 29 commits into
v3from
migrate-editor-to-tiptap

Conversation

@rschlaefli

@rschlaefli rschlaefli commented Jul 7, 2026

Copy link
Copy Markdown
Member

Summary

This PR replaces the Slate-based lecturer editor with Tiptap while keeping Markdown as the persisted format.

  • Adds GFM tables and syntax-highlighted code blocks to authoring and shared previews.
  • Removes the Slate conversion utility, type shim, and unused Slate dependencies.
  • Keeps programmatic content and disabled-state updates from dirtying Formik or triggering recovery prompts.
  • Limits table controls to structures that survive a Markdown save and reload. Cell merging and resizing are intentionally not exposed.
  • Uses Lowlight's common language set and covers JavaScript, TypeScript, and R highlighting in the editor and preview.
  • Stabilizes the answer-collection accordion and manage preview Playwright paths affected by the editor migration.
  • Stops generating the legacy <br> empty sentinel and treats break-only legacy rows as empty when reopened.
  • Refreshes empty-editor placeholder translations after an in-place locale change without recreating the editor.
  • Keeps toolbar marks and contextual table controls synchronized after selection-only cursor movement.

Implementation notes

  • ContentInput parses and serializes Markdown through Tiptap 3.27.1.
  • External content synchronization uses emitUpdate: false, preserves Markdown-significant leading whitespace, and applies focused parent resets instead of dropping them.
  • The Placeholder extension reads a ref-backed value and refreshes decorations on translation changes without emitting a document update.
  • Empty editor bindings pass '' or undefined. A single editor-boundary normalizer maps sentinel-only <br>, <br/>, and <br /> variants to an empty document without changing mixed Markdown.
  • The shared Markdown renderer enables remark-gfm and rehype-prism-plus. Raw HTML remains disabled and the sanitizer only admits language-* code classes.
  • Toolbar buttons use one localized label for the tooltip and accessible name, expose stable test hooks, and disable table insertion while the cursor is already inside a table.
  • ContentInput uses useEditorState for selection-dependent controls and sets immediatelyRender: false for Next.js SSR.
  • Manage and PWA preview styles use the same .markdown-content table and code selectors.
  • The upstream Tiptap skill is installed in the repo. Its source and documentation clones stay under ignored .reference/.

No schema migration or database write is included. Tiptap's programmatic synchronization suppresses updates, so the removed fallback did not write new sentinels. Historic Slate rows may still exist in scalar Markdown fields and JSON snapshots. Existing rows now reopen correctly. Any database cleanup should start with read-only production counts. If confirmed optional-field counts justify remediation, use an explicit idempotent batched job with single-owner locking, dry-run output, and progress/failure observability. Required content and ElementInstance snapshots need targeted handling, not a blind replacement.

Branch coverage

  • Base: v3 at current origin/v3
  • Head: 9b18a0444c
  • Diff: 31 files, 2,062 insertions, 882 deletions
  • Covered work: editor migration, Markdown renderer, table and code styling, i18n, dependency cleanup, empty-state compatibility, Playwright stabilization and rich-feature coverage, wiki and skill updates, security review, and final maintainability fixes

Review focus

  • Markdown round trips and the decision to omit merged cells and table resizing.
  • Form state behavior during editor mount, external prop updates, disabled-state changes, and legacy break-only content.
  • Toolbar and table-control updates after selection-only transactions in reopened content.
  • The sanitizer and syntax-highlighting pipeline in packages/markdown.
  • The ZA-editor-rich-features assertions for table dimensions, JavaScript, TypeScript, and R tokens, saved-editor reopen behavior, plus new and legacy empty-editor placeholders.
  • The query-first database decision. This branch intentionally does not perform an unmeasured data migration.

Verification

Current head:

  • pnpm run check:all: passed.
  • Repository pre-push hook: full production build passed, 21/21 tasks.
  • Playwright TypeScript check and Chromium discovery for ZA-editor-rich-features: passed.
  • Frozen lockfile verification and Prettier checks: passed.
  • Independent cleanup review: passed after adding a persisted legacy-row reopen assertion with database restoration in finally.
  • Independent follow-up review: passed for localized placeholder refresh and controlled database-remediation guidance.
  • Parallel standards, spec, and strict maintainability reviews: passed after removing unrelated scope, dead dependencies, repeated toolbar props, the remaining renderer sentinel default, and false table-toggle semantics.
  • Upstream Tiptap source review and independent fix review: passed. The reopened-table test now selects a persisted cell before asserting contextual controls.
  • Tiptap 3.27.3 was not adopted because the packages are five days old and the repo enforces a fourteen-day minimum release age. The branch remains consistently pinned to 3.27.1.
  • GitHub Actions: all code, build, analysis, and test jobs passed on 9b18a0444c (Playwright run 29186014786). Playwright shard 4 initially hit a random seeded ParticipantGroup(courseId, code) collision before tests started; rerunning the failed job passed seeding and the complete shard.

Earlier branch verification:

  • GitHub Actions run 29112473952 passed the shared Playwright build and all eight shards on 59453d5eee.
  • GitHub Actions run 29145816203 passed the shared Playwright build and all eight shards on c2849f526d.
  • GitHub Actions run 29147103660 passed the shared Playwright build and all eight shards on 9e0438039e.
  • OKF wiki validation passed; scoped opengrep scan --config auto reported 0 findings on the editor files before the final simplification.
  • Devrouter preview confirmed no recovery prompt after an untouched close and reopen.
  • Answer options opened with one click after untouched metadata.
  • A merged-cell save and reload lost colspan, which is why merge and split controls were removed.
  • Post-change editor showed add-row and add-column controls, no merge or resize UI, no nested buttons, and visible keyboard focus at desktop and narrow widths.

Warnings and gaps:

  • The first shard-7 attempt timed out in an unchanged T-resources answer-collection action. Its full failed-job rerun passed without a branch change, so this is classified as a test flake rather than an editor regression.
  • Focused local Playwright runtime remains blocked by the devcontainer's synthetic auth routing before any editor selector. Remote Playwright is the runtime gate.
  • The in-app Browser reached the pr5148-final devrouter preview. Workspace authentication then posted to the base auth host, which belongs to another active worktree, so no authenticated browser assertion or screenshot is claimed for this commit.
  • Screenshots inspected during earlier browser work are not yet attached in a reviewer-visible location.
  • No production database audit was run. The branch contains no data mutation.

Security / privacy

  • Raw HTML remains disabled in the Markdown pipeline.
  • rehype-sanitize remains active before syntax highlighting.
  • No new secret-bearing file was added by the feature diff.
  • GitGuardian still reports the known development-test credential commit inherited from v3; an authorized maintainer must classify or clear that incident.

Blocking before merge

  • Fresh required CI, including all Playwright shards, passes on 9b18a0444c.
  • GitGuardian incident is cleared or accepted by an authorized maintainer.
  • Reviewer-visible desktop and narrow screenshots are attached.
  • Required human review is approved.

Follow-up after merge

  • Run a read-only production audit for sentinel-only scalar and JSON values. Design a controlled idempotent remediation job only if measured counts justify it.
  • Consider a shared package-level Markdown stylesheet if more consumers need the same preview theme.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR migrates the management rich-text editor from Slate to Tiptap, adds Markdown GFM and syntax highlighting, standardizes empty-content handling, updates styling and localization, expands Playwright coverage, and documents editor and workflow contracts.

Changes

Tiptap editor migration

Layer / File(s) Summary
Tiptap dependencies and Slate removal
apps/frontend-manage/package.json, packages/shared-components/package.json
Tiptap and lowlight dependencies replace Slate and Slate Markdown tooling.
ContentInput Tiptap editor
apps/frontend-manage/src/components/common/ContentInput.tsx, apps/frontend-manage/src/components/**/Element*.tsx, apps/frontend-manage/src/components/courses/groupActivity/GroupActivityGradingStack.tsx
ContentInput now uses Tiptap Markdown synchronization, command-based toolbar actions, table controls, image insertion, disabled-state handling, and empty-string content values.
Markdown rendering and presentation
packages/markdown/src/Markdown.tsx, apps/frontend-manage/src/globals.css, apps/frontend-pwa/src/globals.css, packages/i18n/messages/*.ts, apps/frontend-manage/src/pages/questions/[id].tsx
Markdown rendering enables GFM and Prism, while editor and preview styles support tables, code blocks, syntax tokens, placeholders, and localized controls.
Rich editor Playwright coverage
playwright/tests/ZA-editor-rich-features.spec.ts
End-to-end coverage verifies formatting, tables, code blocks, persistence, preview rendering, and legacy <br> handling.

Test interaction stabilization

Layer / File(s) Summary
Answer collection options helper
playwright/util/actions.ts, playwright/tests/K-elements-selection.spec.ts, playwright/tests/L-elements-case-study.spec.ts, playwright/tests/S-group-activity.spec.ts
Shared answer-collection opening logic waits for Tiptap layout readiness and avoids redundant toggles; affected tests use the helper and update one submission assertion.

Workflow and documentation

Layer / File(s) Summary
Workflow documentation and finalization guidance
.agents/skills/*, docs/frontend-conventions.md, docs/log.md, project/2026-07-10-pr-5148-tiptap-editor-finalization-plan.md, skills-lock.json, .gitignore
Documentation records Markdown boundaries, empty-state conventions, highlighting checks, validation commands, and PR finalization steps.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Author
  participant ContentInput
  participant Tiptap
  participant StoredMarkdown
  participant MarkdownPreview
  Author->>ContentInput: edit rich content
  ContentInput->>Tiptap: execute editor commands
  Tiptap->>ContentInput: emit Markdown update
  ContentInput->>StoredMarkdown: save Markdown
  StoredMarkdown->>MarkdownPreview: render persisted content
Loading

Possibly related PRs

Suggested labels: feature, dependencies

Suggested reviewers: sjschlapbach

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately summarizes the main change: migrating the rich text editor to Tiptap.
Description check ✅ Passed The description is comprehensive and covers summary, implementation notes, verification, and rollout/gaps, though it doesn't follow the exact template headings or include a ClickUp link.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
apps/frontend-manage/src/components/common/ContentInput.tsx Replaces the Slate editor with Tiptap and keeps editor input, prop sync, and saved output on the Markdown path.
packages/markdown/src/Markdown.tsx Adds GFM and code highlighting support while keeping raw HTML disabled and sanitized rendering in place.
playwright/tests/ZA-editor-rich-features.spec.ts Adds coverage for formatting, tables, code blocks, preview rendering, reopening saved content, and legacy empty content.

Reviews (17): Last reviewed commit: "fix(editor): react to Tiptap selection s..." | Re-trigger Greptile

Comment thread apps/frontend-manage/src/components/common/ContentInput.tsx Outdated
Comment thread apps/frontend-manage/src/components/common/ContentInput.tsx Outdated
Comment thread apps/frontend-manage/src/components/common/ContentInput.tsx Outdated
Comment thread apps/frontend-manage/src/components/common/ContentInput.tsx Outdated
Comment thread apps/frontend-manage/src/components/common/ContentInput.tsx Outdated
Comment thread apps/frontend-manage/src/components/common/ContentInput.tsx
Comment thread apps/frontend-manage/src/components/common/ContentInput.tsx Outdated
@rschlaefli rschlaefli marked this pull request as ready for review July 7, 2026 21:57
Copilot AI review requested due to automatic review settings July 7, 2026 21:57
@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. enhancement labels Jul 7, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@gitguardian

gitguardian Bot commented Jul 7, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
1509424 Triggered Generic Password a259f06 .devcontainer/docker-compose.yml View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
playwright/tests/K-elements-selection.spec.ts (1)

22-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate helper across two spec files — extract to shared test utils.

openAnswerCollectionOptions is defined identically in both K-elements-selection.spec.ts and L-elements-case-study.spec.ts. Extract it to a shared helper module (e.g. playwright/tests/utils/answerCollection.ts) and import it in both specs to avoid drift when the retry logic or selector needs updating later.

♻️ Proposed extraction
+// playwright/tests/utils/answerCollection.ts
+import { Page } from '`@playwright/test`'
+
+export async function openAnswerCollectionOptions(page: Page) {
+  await page.locator('.ProseMirror').first().waitFor({ state: 'visible' })
+  for (let attempt = 0; attempt < 3; attempt++) {
+    await page.getByTestId('open-answer-collection-options').click()
+    if (
+      await page
+        .getByTestId('search-answer-options')
+        .isVisible({ timeout: 1_000 })
+        .catch(() => false)
+    ) {
+      return
+    }
+  }
+  throw new Error('Failed to open answer collection options')
+}

Then in both spec files:

-async function openAnswerCollectionOptions(page: Page) {
-  await page.locator('.ProseMirror').first().waitFor({ state: 'visible' })
-  for (let attempt = 0; attempt < 3; attempt++) {
-    await page.getByTestId('open-answer-collection-options').click()
-    if (
-      await page
-        .getByTestId('search-answer-options')
-        .isVisible({ timeout: 1_000 })
-        .catch(() => false)
-    ) {
-      return
-    }
-  }
-  throw new Error('Failed to open answer collection options')
-}
+import { openAnswerCollectionOptions } from './utils/answerCollection'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@playwright/tests/K-elements-selection.spec.ts` around lines 22 - 37, The
helper openAnswerCollectionOptions is duplicated in multiple Playwright specs,
so extract the shared retry/click logic into a common test utility module and
import it from both K-elements-selection.spec and L-elements-case-study. Keep
the behavior identical by moving the page/locator logic and the failure throw
into one reusable function so selector or retry changes only need to be updated
in one place.
apps/frontend-manage/src/components/common/ContentInput.tsx (2)

28-28: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the configured path alias for this local import.

./MediaLibrary violates the repository import convention for TS/TSX files; switch it to the equivalent @/~ alias path. As per coding guidelines, "**/*.{ts,tsx,js,jsx}: Use @ and ~ path aliases for imports".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/frontend-manage/src/components/common/ContentInput.tsx` at line 28, The
local import in ContentInput.tsx should use the repository’s configured path
alias instead of a relative path. Update the MediaLibrary import to the
equivalent @ or ~ alias form so it matches the TS/TSX import convention used
across the codebase.

Source: Coding guidelines


159-167: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move placeholder styling out of the component.

The inline <style> injects raw CSS per ContentInput instance; move this rule to the shared/global stylesheet or express it through the project’s Tailwind styling layer. As per coding guidelines, "**/*.{ts,tsx}: Use TailwindCSS utilities only; use twMerge for conditional classes".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/frontend-manage/src/components/common/ContentInput.tsx` around lines 159
- 167, The placeholder CSS is being injected inline inside ContentInput, which
violates the shared styling approach and duplicates styles per instance. Move
the .ProseMirror p.is-editor-empty:first-child::before rule out of the component
into the global/shared stylesheet or Tailwind layer, and keep ContentInput
limited to Tailwind utility classes with twMerge for any conditional styling.

Source: Coding guidelines

playwright/tests/ZA-editor-rich-features.spec.ts (1)

79-102: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Locators rely on fragile, incidental UI details instead of data-testid.

The table button (svg[data-icon="table"]), row-toolbar (hasText: '+R'), and preview container (.w-256) are selected via icon attributes, visible shorthand text, and a Tailwind utility class, respectively. This is inconsistent with the data-testid convention used elsewhere in this same test (create-question, insert-question-title, insert-question-text). Any icon swap, label change, or Tailwind refactor unrelated to editor behavior would silently break this test.

Also applies to: 133-141

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@playwright/tests/ZA-editor-rich-features.spec.ts` around lines 79 - 102, The
rich-features Playwright test is using fragile UI selectors instead of the
existing data-testid pattern. Update the table button, row toolbar action, and
preview/container lookups in the editor test to use stable test ids or other
durable hooks already used elsewhere in this spec, so the assertions in the
table interaction flow continue to target the same elements even if icons,
labels, or Tailwind classes change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/frontend-manage/src/components/common/ContentInput.tsx`:
- Around line 523-545: The ToolbarButton component currently uses a clickable
span, which is not keyboard-accessible and can trigger lint warnings; update
ToolbarButton to render a native button element instead, set type="button" to
avoid unintended form submission, and keep the same styling/active behavior.
Also refactor the component declaration to use a function-based component
definition to match the component guideline, and preserve the existing
forwardRef, onClick, active, className, and children handling when making the
change.
- Around line 74-124: ContentInput is treating Markdown as generic content,
which can lose formatting on load and sync. Update the useEditor setup and the
external sync effect in ContentInput so both the initial content and
editor.commands.setContent use markdown parsing explicitly via contentType:
'markdown'. Keep the fix localized around useEditor and the content-sync
useEffect so tables, lists, and other Markdown formatting are preserved.

---

Nitpick comments:
In `@apps/frontend-manage/src/components/common/ContentInput.tsx`:
- Line 28: The local import in ContentInput.tsx should use the repository’s
configured path alias instead of a relative path. Update the MediaLibrary import
to the equivalent @ or ~ alias form so it matches the TS/TSX import convention
used across the codebase.
- Around line 159-167: The placeholder CSS is being injected inline inside
ContentInput, which violates the shared styling approach and duplicates styles
per instance. Move the .ProseMirror p.is-editor-empty:first-child::before rule
out of the component into the global/shared stylesheet or Tailwind layer, and
keep ContentInput limited to Tailwind utility classes with twMerge for any
conditional styling.

In `@playwright/tests/K-elements-selection.spec.ts`:
- Around line 22-37: The helper openAnswerCollectionOptions is duplicated in
multiple Playwright specs, so extract the shared retry/click logic into a common
test utility module and import it from both K-elements-selection.spec and
L-elements-case-study. Keep the behavior identical by moving the page/locator
logic and the failure throw into one reusable function so selector or retry
changes only need to be updated in one place.

In `@playwright/tests/ZA-editor-rich-features.spec.ts`:
- Around line 79-102: The rich-features Playwright test is using fragile UI
selectors instead of the existing data-testid pattern. Update the table button,
row toolbar action, and preview/container lookups in the editor test to use
stable test ids or other durable hooks already used elsewhere in this spec, so
the assertions in the table interaction flow continue to target the same
elements even if icons, labels, or Tailwind classes change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 171c2f80-56dd-4c11-b3b3-6541c374b48d

📥 Commits

Reviewing files that changed from the base of the PR and between 20fb022 and c6bbe2c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (14)
  • .github/scripts/get-shard-files.js
  • apps/frontend-manage/package.json
  • apps/frontend-manage/src/components/common/ContentInput.tsx
  • apps/frontend-manage/src/globals.css
  • apps/frontend-pwa/src/globals.css
  • packages/i18n/messages/de.ts
  • packages/i18n/messages/en.ts
  • packages/markdown/src/Markdown.tsx
  • packages/shared-components/package.json
  • packages/shared-components/src/@types/slate.d.ts
  • packages/shared-components/src/utils/slateMdConversion.ts
  • playwright/tests/K-elements-selection.spec.ts
  • playwright/tests/L-elements-case-study.spec.ts
  • playwright/tests/ZA-editor-rich-features.spec.ts
💤 Files with no reviewable changes (2)
  • packages/shared-components/src/@types/slate.d.ts
  • packages/shared-components/src/utils/slateMdConversion.ts

Comment thread apps/frontend-manage/src/components/common/ContentInput.tsx
Comment thread apps/frontend-manage/src/components/common/ContentInput.tsx Outdated
Comment thread apps/frontend-manage/src/components/common/ContentInput.tsx Outdated
Comment thread apps/frontend-manage/src/components/common/ContentInput.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/frontend-manage/src/components/common/ContentInput.tsx (1)

78-104: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep onChange fresh in useEditor's onUpdate callback. useEditor can keep the initial callback, so a new onChange prop may be missed unless the editor is recreated. Use a ref or stable callback wrapper here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/frontend-manage/src/components/common/ContentInput.tsx` around lines 78
- 104, The onUpdate handler in ContentInput’s useEditor setup can capture a
stale onChange prop because the editor instance may keep the initial callback.
Update the ContentInput component to keep onChange fresh by using a ref or a
stable callback wrapper, and have onUpdate call that current function instead of
closing over the prop directly.
🧹 Nitpick comments (1)
apps/frontend-manage/src/components/common/ContentInput.tsx (1)

558-586: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

{...props} spread can override the computed disabled attribute.

disabled is not destructured from the component props, so it remains in the rest props object. Since {...props} is spread after disabled={isDisabled}, a caller passing disabled={false} would override the context-derived isDisabled, making the button clickable even when the editor is disabled. No current caller passes disabled, but this is a latent bug.

🔧 Destructure `disabled` to prevent override
 const ToolbarButton = React.forwardRef<HTMLButtonElement, ToolbarButtonProps>(
   function ToolbarButton(
-    { className, active, children, onClick, ...props },
+    { className, active, children, onClick, disabled: propDisabled, ...props },
     ref
   ) {
-    const { disabled } = React.useContext(ToolbarContext)
-    const isDisabled = disabled || props.disabled
+    const { disabled: contextDisabled } = React.useContext(ToolbarContext)
+    const isDisabled = contextDisabled || propDisabled
     return (
       <button
         ref={ref}
         type="button"
         onClick={onClick}
         aria-pressed={active}
         disabled={isDisabled}
         className={twMerge(
           'outline-hidden my-auto flex h-7 w-7 items-center justify-center rounded border-0 bg-transparent p-0',
           isDisabled
             ? 'cursor-not-allowed opacity-50'
             : 'hover:bg-uzh-grey-20 cursor-pointer',
           active && 'bg-uzh-grey-40',
           className
         )}
         {...props}
       >
         {children}
       </button>
     )
   }
 )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/frontend-manage/src/components/common/ContentInput.tsx` around lines 558
- 586, The ToolbarButton component in ContentInput should not let the spread
props override the computed disabled state. In ToolbarButton, destructure
disabled out of the incoming props and compute isDisabled from ToolbarContext
plus that prop, then ensure the rendered button’s disabled attribute is
controlled only by that computed value before spreading the remaining props.
This keeps caller-supplied props from making the button clickable when the
editor is disabled.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/frontend-manage/src/components/common/ContentInput.tsx`:
- Line 30: The ContentInput editor is pulling in the full lowlight language
bundle via createLowlight(all), which unnecessarily increases bundle size.
Update the lowlight setup in ContentInput to use createLowlight(common) or
explicitly register only the languages the editor actually needs, keeping the
same editor integration while reducing the imported syntax-highlighting
footprint.

---

Outside diff comments:
In `@apps/frontend-manage/src/components/common/ContentInput.tsx`:
- Around line 78-104: The onUpdate handler in ContentInput’s useEditor setup can
capture a stale onChange prop because the editor instance may keep the initial
callback. Update the ContentInput component to keep onChange fresh by using a
ref or a stable callback wrapper, and have onUpdate call that current function
instead of closing over the prop directly.

---

Nitpick comments:
In `@apps/frontend-manage/src/components/common/ContentInput.tsx`:
- Around line 558-586: The ToolbarButton component in ContentInput should not
let the spread props override the computed disabled state. In ToolbarButton,
destructure disabled out of the incoming props and compute isDisabled from
ToolbarContext plus that prop, then ensure the rendered button’s disabled
attribute is controlled only by that computed value before spreading the
remaining props. This keeps caller-supplied props from making the button
clickable when the editor is disabled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d1d7ffee-d09d-4382-9750-a2029b29a9de

📥 Commits

Reviewing files that changed from the base of the PR and between c6bbe2c and b40fc33.

📒 Files selected for processing (14)
  • .agents/skills/klicker-playwright-e2e/SKILL.md
  • AGENTS.md
  • apps/frontend-manage/src/components/common/ContentInput.tsx
  • apps/frontend-manage/src/globals.css
  • apps/frontend-manage/src/pages/questions/[id].tsx
  • apps/frontend-pwa/src/globals.css
  • docs/log.md
  • packages/i18n/messages/de.ts
  • packages/i18n/messages/en.ts
  • packages/markdown/src/Markdown.tsx
  • playwright/tests/K-elements-selection.spec.ts
  • playwright/tests/L-elements-case-study.spec.ts
  • playwright/tests/ZA-editor-rich-features.spec.ts
  • playwright/util/actions.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/i18n/messages/de.ts
  • apps/frontend-manage/src/pages/questions/[id].tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/i18n/messages/en.ts
  • apps/frontend-pwa/src/globals.css
  • packages/markdown/src/Markdown.tsx
  • playwright/tests/ZA-editor-rich-features.spec.ts

Comment thread apps/frontend-manage/src/components/common/ContentInput.tsx Outdated
@rschlaefli rschlaefli changed the title refactor(manage): migrate rich text editor from Slate to Tiptap feat(editor): migrate rich text editing to Tiptap Jul 10, 2026
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
putComment timed out

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
project/2026-07-10-pr-5148-tiptap-editor-finalization-plan.md (1)

37-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Clarify the dependency non-goal.

This PR explicitly introduces Tiptap/Lowlight dependencies, so “No new dependencies” is ambiguous. Reword it to mean no additional dependencies beyond the migration already in this PR.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@project/2026-07-10-pr-5148-tiptap-editor-finalization-plan.md` at line 37,
Reword the dependency non-goal in the migration plan so it excludes only
dependencies beyond the Tiptap/Lowlight additions already introduced by this PR,
while retaining the intent to avoid major upgrades.
apps/frontend-manage/src/components/common/ContentInput.tsx (1)

175-175: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Memoize the ToolbarContext provider value.

value={{ disabled }} creates a new object every render, so every ToolbarButton consumer re-renders on each keystroke even when disabled hasn't changed. SonarCloud flags this too.

♻️ Suggested fix
+  const toolbarContextValue = React.useMemo(() => ({ disabled }), [disabled])
+
   ...
-        <ToolbarContext.Provider value={{ disabled }}>
+        <ToolbarContext.Provider value={toolbarContextValue}>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/frontend-manage/src/components/common/ContentInput.tsx` at line 175,
Memoize the value passed to ToolbarContext.Provider in ContentInput using the
existing React memoization pattern, with disabled as its dependency. Keep the
provider behavior unchanged while ensuring ToolbarButton consumers do not
re-render when disabled is unchanged.

Source: Linters/SAST tools

apps/frontend-manage/src/globals.css (1)

314-415: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Table/code-highlighting CSS is duplicated verbatim across two apps.

This block (borders, padding, pre theme, hljs-*/token.* color mappings) is nearly identical to the block added in apps/frontend-pwa/src/globals.css (Lines 313-395). Since the PR objective calls out "shared table and code styles," consider extracting this into a shared CSS partial (e.g. under packages/shared-components or packages/markdown) imported by both apps, so future theme/color tweaks don't need to be kept in sync manually across two files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/frontend-manage/src/globals.css` around lines 314 - 415, Extract the
duplicated table and code-highlighting styles, including the table, pre, hljs,
and token selectors, into a shared CSS partial in the appropriate shared
package. Import that partial from both apps’ globals.css files and remove the
duplicated definitions, preserving the existing selectors and styling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/frontend-manage/src/components/common/ContentInput.tsx`:
- Around line 88-96: Update the editor configuration in ContentInput so the
Placeholder extension reads the current placeholder after locale changes instead
of retaining the initial string. Use a ref-backed placeholder function, or
recreate the editor when placeholder changes, while preserving the existing
emptyEditorClass behavior.

In `@project/2026-07-10-pr-5148-tiptap-editor-finalization-plan.md`:
- Around line 441-446: Update the “Next Steps” and rollout guidance to prohibit
boot-time backfills and require a controlled migration job for any writes. Keep
the read-only production audit first, then specify that remediation must be
idempotent, batched, ownership/lock protected, and observable to prevent
concurrent application instances from applying it.

---

Nitpick comments:
In `@apps/frontend-manage/src/components/common/ContentInput.tsx`:
- Line 175: Memoize the value passed to ToolbarContext.Provider in ContentInput
using the existing React memoization pattern, with disabled as its dependency.
Keep the provider behavior unchanged while ensuring ToolbarButton consumers do
not re-render when disabled is unchanged.

In `@apps/frontend-manage/src/globals.css`:
- Around line 314-415: Extract the duplicated table and code-highlighting
styles, including the table, pre, hljs, and token selectors, into a shared CSS
partial in the appropriate shared package. Import that partial from both apps’
globals.css files and remove the duplicated definitions, preserving the existing
selectors and styling.

In `@project/2026-07-10-pr-5148-tiptap-editor-finalization-plan.md`:
- Line 37: Reword the dependency non-goal in the migration plan so it excludes
only dependencies beyond the Tiptap/Lowlight additions already introduced by
this PR, while retaining the intent to avoid major upgrades.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: db66074c-dab4-49df-97c0-43bf5a6bcc1c

📥 Commits

Reviewing files that changed from the base of the PR and between b40fc33 and c2849f5.

📒 Files selected for processing (20)
  • .agents/skills/klicker-frontend-ui/SKILL.md
  • .agents/skills/klicker-playwright-e2e/SKILL.md
  • .agents/skills/klicker-wiki-maintenance/SKILL.md
  • AGENTS.md
  • apps/frontend-manage/src/components/common/ContentInput.tsx
  • apps/frontend-manage/src/components/courses/groupActivity/GroupActivityGradingStack.tsx
  • apps/frontend-manage/src/components/elements/manipulation/ElementContentInput.tsx
  • apps/frontend-manage/src/components/elements/manipulation/ElementExplanationField.tsx
  • apps/frontend-manage/src/components/elements/manipulation/options/CaseStudyCasesFields.tsx
  • apps/frontend-manage/src/components/elements/manipulation/options/ChoicesOptions.tsx
  • apps/frontend-manage/src/globals.css
  • apps/frontend-pwa/src/globals.css
  • docs/frontend-conventions.md
  • docs/log.md
  • packages/i18n/messages/de.ts
  • packages/i18n/messages/en.ts
  • playwright/tests/S-group-activity.spec.ts
  • playwright/tests/ZA-editor-rich-features.spec.ts
  • playwright/util/actions.ts
  • project/2026-07-10-pr-5148-tiptap-editor-finalization-plan.md
💤 Files with no reviewable changes (2)
  • packages/i18n/messages/de.ts
  • packages/i18n/messages/en.ts
✅ Files skipped from review due to trivial changes (2)
  • docs/frontend-conventions.md
  • docs/log.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • AGENTS.md

Comment thread apps/frontend-manage/src/components/common/ContentInput.tsx
Comment thread project/2026-07-10-pr-5148-tiptap-editor-finalization-plan.md Outdated

@Zganz404 Zganz404 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Logic looks correct. One suggestion: the early return on line prevents the cleanup function from running — worth adding a finally block.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/markdown/src/Markdown.tsx (1)

111-111: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Pass ignoreMissing: true to rehypePrism Unsupported fenced languages currently throw, so one bad code block can drop the whole markdown render into the "Failed to parse content." fallback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/markdown/src/Markdown.tsx` at line 111, Update the rehypePrism
plugin configuration in the Markdown rendering pipeline to pass the
ignoreMissing option as true, ensuring unsupported fenced languages do not throw
and trigger the failed-content fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/markdown/src/Markdown.tsx`:
- Line 111: Update the rehypePrism plugin configuration in the Markdown
rendering pipeline to pass the ignoreMissing option as true, ensuring
unsupported fenced languages do not throw and trigger the failed-content
fallback.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5f2afca9-a709-4b03-8b03-1ebc5e0940e0

📥 Commits

Reviewing files that changed from the base of the PR and between c2849f5 and 837b057.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • apps/frontend-manage/package.json
  • apps/frontend-manage/src/components/common/ContentInput.tsx
  • docs/log.md
  • packages/markdown/src/Markdown.tsx
  • playwright/tests/ZA-editor-rich-features.spec.ts
  • project/2026-07-10-pr-5148-tiptap-editor-finalization-plan.md
💤 Files with no reviewable changes (2)
  • docs/log.md
  • apps/frontend-manage/package.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • playwright/tests/ZA-editor-rich-features.spec.ts
  • apps/frontend-manage/src/components/common/ContentInput.tsx

@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@skills-lock.json`:
- Around line 34-38: Update the tiptap entry in skills-lock.json to reference
the existing local .agents/skills/tiptap/SKILL.md file, using the matching local
sourceType, source, and skillPath values, and recompute computedHash from that
local file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 744141c3-ed89-4987-a279-97633ac1b45e

📥 Commits

Reviewing files that changed from the base of the PR and between 837b057 and 9b18a04.

📒 Files selected for processing (8)
  • .agents/skills/klicker-frontend-ui/SKILL.md
  • .agents/skills/tiptap/SKILL.md
  • .gitignore
  • apps/frontend-manage/src/components/common/ContentInput.tsx
  • docs/frontend-conventions.md
  • playwright/tests/ZA-editor-rich-features.spec.ts
  • project/2026-07-10-pr-5148-tiptap-editor-finalization-plan.md
  • skills-lock.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • docs/frontend-conventions.md
  • playwright/tests/ZA-editor-rich-features.spec.ts
  • project/2026-07-10-pr-5148-tiptap-editor-finalization-plan.md
  • apps/frontend-manage/src/components/common/ContentInput.tsx

Comment thread skills-lock.json
Comment on lines +34 to +38
"tiptap": {
"source": "ueberdosis/tiptap",
"sourceType": "github",
"skillPath": "skills/tiptap/SKILL.md",
"computedHash": "d3abb7a7e4cb1eb4064e8dcb0928771d58052c7d92e2ec3725a3a9a3c6cd338b"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1. Check if the local skill file exists and compute its SHA-256 hash
echo "=== Local file hash ==="
sha256sum .agents/skills/tiptap/SKILL.md

echo ""
echo "=== Expected hash from skills-lock.json ==="
echo "d3abb7a7e4cb1eb4064e8dcb0928771d58052c7d92e2ec3725a3a9a3c6cd338b"

echo ""
echo "=== Other local skills for comparison ==="
grep -A 4 'klicker-playwright-e2e' skills-lock.json

Repository: uzh-bf/klicker-uzh

Length of output: 621


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== skills-lock.json tiptap entry ==='
grep -A 6 -n '"tiptap"' skills-lock.json || true

echo
echo '=== local skill files ==='
find .agents/skills -maxdepth 2 -name SKILL.md | sort || true

echo
echo '=== tiptap file info ==='
python3 - <<'PY'
from pathlib import Path
import hashlib

p = Path('.agents/skills/tiptap/SKILL.md')
print('exists:', p.exists())
if p.exists():
    data = p.read_bytes()
    print('bytes:', len(data))
    print('sha256:', hashlib.sha256(data).hexdigest())
    print('first line:', data.splitlines()[0].decode('utf-8', 'replace') if data.splitlines() else '')
PY

Repository: uzh-bf/klicker-uzh

Length of output: 1377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '=== skills-lock.json tiptap entry ===\n'
python3 - <<'PY'
import json
from pathlib import Path

data = json.loads(Path('skills-lock.json').read_text())
entry = data.get('tiptap')
print(json.dumps(entry, indent=2, sort_keys=True))
PY

printf '\n=== local path checks ===\n'
python3 - <<'PY'
from pathlib import Path
import hashlib

for path in [
    Path('.agents/skills/tiptap/SKILL.md'),
    Path('skills/tiptap/SKILL.md'),
]:
    print(path, 'exists=', path.exists())
    if path.exists():
        data = path.read_bytes()
        print('  sha256=', hashlib.sha256(data).hexdigest())
        print('  bytes=', len(data))
PY

Repository: uzh-bf/klicker-uzh

Length of output: 396


Fix the tiptap lock entry to point at the local skill. .agents/skills/tiptap/SKILL.md exists, skills/tiptap/SKILL.md does not, and the stored hash does not match the local file. Use the local sourceType/source/skillPath pair here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills-lock.json` around lines 34 - 38, Update the tiptap entry in
skills-lock.json to reference the existing local .agents/skills/tiptap/SKILL.md
file, using the matching local sourceType, source, and skillPath values, and
recompute computedHash from that local file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement size:XXL This PR changes 1000+ lines, ignoring generated files.

Development

Successfully merging this pull request may close these issues.

3 participants