-
Notifications
You must be signed in to change notification settings - Fork 3
Editor Schema Validation #2900
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Editor Schema Validation #2900
Conversation
| content2: JSONContent, | ||
| ): boolean { | ||
| return ( | ||
| JSON.stringify(normalizeJSON(content1)) === |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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}`) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
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_editorpermission.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:
Open the edit page at
http://open.odl.local:8062/articles/<slug>/editConfirm that you see the error alert with "Document schema check failed: Invalid content for node doc: content specification not satisfied"
Hit reset.
Confirm that you see "Reset attempt failed"
Invalid ProseMirror node (not recoverable)
Set content with an invalid node:
Open the edit page at
http://open.odl.local:8062/articles/<slug>/editConfirm 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):
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".
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