Skip to content

Conversation

@jonkafton
Copy link
Contributor

@jonkafton jonkafton commented Jan 27, 2026

What are the relevant tickets?

Description (What does it do?)

  • Provides validation for editor documents against base node schema and content expressions (the article template).

  • Notifies editors if schema is not valid in the view and edit screen.

  • Adds a reset button for editors to attempt to use the TipTap editor to automatically fix invalid documents.

  • Refactors the article specific extensions and content expression to a new hook.

How can this be tested?

Test scenarios by setting the example content in the articles_article table in Postgres.

Log in with a user with is_article_editor permission.

Generally ensure that users without the permission do not see any error alerts on the detail page.

Invalid ProseMirror content (not recoverable)

  • Set random JSON on the article:

    {
      "some": "random"
    }
    
  • Open the edit page at http://open.odl.local:8062/articles/<slug>/edit

  • Confirm that you see the error alert with "Document schema check failed: Invalid content for node doc: content specification not satisfied"

    image
  • Hit reset.

  • Confirm that you see "Reset attempt failed"

    image

Invalid ProseMirror node (not recoverable)

  • Set content with an invalid node:

    {
      "type": "doc",
      "content": [
        {
          "type": "not-valid"
        }
      ]
    }
    
  • Open the edit page at http://open.odl.local:8062/articles/<slug>/edit

  • Confirm that you see the error alert with "Document schema check failed: Invalid content for node doc: node type "not-valid" not found in schema".

  • Hit reset.

  • Confirm that you see "Reset attempt failed".

Invalid content expression (recoverable)

  • Set content that does not meet the article template specification (e.g. missing banner and byline):

    {
      "type": "doc",
      "content": [
        {
          "type": "paragraph",
          "attrs": {
            "textAlign": null
          },
          "content": [
            {
              "text": "Content paragraph",
              "type": "text"
            }
          ]
        }
      ]
    }
    
  • Open the detail page at http://open.odl.local:8062/articles/<slug>

  • Confirm that the document renders.

  • Confirm that you see the schema error alert (editor only) with "Document schema check failed: Invalid content for node doc: paragraph is not allowed in this position".

    image
  • Hit Edit.

  • On the edit page, hit the "Reset" button.

  • Confirm that the article is updated with the banner and byline and still contains its node content.

  • Add a title and publish it.

  • Confirm the view and edit pages now render without error.

Invalid JSON

  • Test is not needed as this is enforced by Postgres.

content2: JSONContent,
): boolean {
return (
JSON.stringify(normalizeJSON(content1)) ===
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion

Problem: This creates sorted copies AND stringifies on EVERY render when content changes. For large documents, this is expensive.

Suggestion: Use a memoized comparison or a library like fast-deep-equal:

import { deepEqual } from "fast-deep-equal"

export function contentsMatch(content1: JSONContent, content2: JSONContent): boolean {
  return deepEqual(content1, content2)
}

const [title, setTitle] = React.useState(article?.title)
const [isPublishing, setIsPublishing] = useState(false)
const [uploadError, setUploadError] = useState<string | null>(null)
const [resetAttempted, setResetAttempted] = useState(false)
Copy link
Contributor

Choose a reason for hiding this comment

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

In my guess, Once resetAttempted is set to true, it never resets. If the user fixes the error another way (manual editing), then encounters a new schema error later, they'll see "Reset attempt failed" even though they haven't tried resetting the new error.

Suggestion: Reset this flag when schema error changes:

useEffect(() => {
  if (!schemaError) {
    setResetAttempted(false)
  }
}, [schemaError])

* This provides a checkpoint for editors to accept schema changes on existing articles
* which may otherwise break them.
*/
editor.commands.setContent(article?.content)
Copy link
Contributor

Choose a reason for hiding this comment

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

Here I think we have issue

The Problem is stale Closures in Event Handlers
When we have something like this

<Button
  onClick={() => {
    editor.commands.setContent(article?.content)
    setResetAttempted(true)
  }}
>
  Reset
</Button>

The arrow function captures the article] variable from the surrounding scope when the component renders. This is called a "closure."

In my opinion it would break here

Real-World Scenario Where This Breaks
Imagine this sequence:

1- User opens Article 1 (ID: 123)

  • Component renders with [article = { id: 123, content: { ... } }]
  • Button's onClick captures this article object

2 - Schema error appears for Article 1

  • Button is visible but NOT re-rendered (React optimization)
  • onClick still has reference to Article 1's content

3 - User navigates to Article #2 (ID: 456) while error dialog is still open

  • [article] prop updates to [{ id: 456, content: { ... } }]
  • BUT the onClick closure still has the OLD article (Article 1)
    User clicks "Reset"

💥 BUG: It resets to Article 1's content instead of Article 2!

Why This Happens
React doesn't always re-create the JSX element when props change. If the Button is wrapped in memoization or the parent only re-renders certain parts, the onClick function keeps its original captured values.

I think this would solve the problem using ref

const ArticleEditor = ({ article, ... }: ArticleEditorProps) => {
  const articleRef = useRef(article)
  
  // Keep ref updated with latest article
  useEffect(() => {
    articleRef.current = article
  }, [article])

  return (
    <Button
      onClick={() => {
        // ✅ Always gets the LATEST article
        editor.commands.setContent(articleRef.current?.content)
        setResetAttempted(true)
      }}
    >
      Reset
    </Button>
  )
}

if (typeof nodeTypeName !== "string") {
const errorMessage =
"Invalid content for node doc: node type must be a string"
setSchemaError(`Document schema check failed: ${errorMessage}`)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this error message would be more helpful

setSchemaError(
  `Document schema check failed: Invalid content for node doc: ${jsonNode.type} is not allowed at position ${i}. Expected one of: ${match.validEnd ? 'end' : 'more content'}`,
)

const currentContent = editor.getJSON()
if (JSON.stringify(article.content) !== JSON.stringify(currentContent)) {
if (!contentsMatch(article.content, currentContent)) {
setContent(article.content)
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: Is setContent(article.content) necessary here if editor.commands.setContent is being called? The editor might already update the content state through onUpdate.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants