fix: recover stale table editor transforms
This commit is contained in:
@@ -48,6 +48,12 @@ describe('isRecoverableEditorTransformError', () => {
|
||||
expect(isRecoverableEditorTransformError(new RangeError(
|
||||
'Invalid content for node blockContainer: <paragraph("Procedures are long-running"), blockGroup(blockContainer(bulletListItem("Step")))>',
|
||||
))).toBe(true)
|
||||
expect(isRecoverableEditorTransformError(new RangeError(
|
||||
'Index 1 out of range for <tableRow(tableCell(tableParagraph("A")))>',
|
||||
))).toBe(true)
|
||||
expect(isRecoverableEditorTransformError(new RangeError(
|
||||
'Index 1 out of range for <paragraph("A")>',
|
||||
))).toBe(false)
|
||||
expect(isRecoverableEditorTransformError(new Error('unrelated'))).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -93,6 +99,22 @@ describe('installRichEditorTransformErrorRecovery', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('recovers table selection transactions whose target row changed underneath BlockNote', () => {
|
||||
const tableError = new RangeError(
|
||||
'Index 1 out of range for <tableRow(tableCell(tableParagraph("A")))>',
|
||||
)
|
||||
const { currentDoc, view } = createView(tableError)
|
||||
const recoverDocument = vi.fn()
|
||||
|
||||
installRichEditorTransformErrorRecovery(view, { recoverDocument })
|
||||
|
||||
expect(() => view.dispatch({ before: currentDoc })).not.toThrow()
|
||||
expect(recoverDocument).toHaveBeenCalledTimes(1)
|
||||
expect(trackEvent).toHaveBeenCalledWith('rich_editor_transform_error_recovered', {
|
||||
reason: 'table_position_out_of_range',
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps non-ProseMirror dispatch failures visible', () => {
|
||||
const { view } = createView(new Error('plugin failed'))
|
||||
|
||||
|
||||
@@ -35,7 +35,11 @@ interface RepairableBlockNoteEditor {
|
||||
replaceBlocks?: (currentBlocks: unknown[], nextBlocks: unknown[]) => unknown
|
||||
}
|
||||
|
||||
type RecoveryReason = 'mismatched_transaction' | 'stale_transaction' | 'transform_error'
|
||||
type RecoveryReason =
|
||||
| 'mismatched_transaction'
|
||||
| 'stale_transaction'
|
||||
| 'table_position_out_of_range'
|
||||
| 'transform_error'
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null
|
||||
@@ -67,12 +71,20 @@ function isInvalidContentTransactionError(error: unknown): boolean {
|
||||
return error instanceof RangeError && error.message.startsWith('Invalid content for node ')
|
||||
}
|
||||
|
||||
function isTablePositionOutOfRangeError(error: unknown): boolean {
|
||||
return error instanceof RangeError && /^Index \d+ out of range for <tableRow\(/.test(error.message)
|
||||
}
|
||||
|
||||
function isTransformError(error: unknown): boolean {
|
||||
return error instanceof Error && error.name === 'TransformError'
|
||||
}
|
||||
|
||||
function isRecoverableRangeError(error: unknown): boolean {
|
||||
return isInvalidContentTransactionError(error) || isTablePositionOutOfRangeError(error)
|
||||
}
|
||||
|
||||
export function isRecoverableEditorTransformError(error: unknown): boolean {
|
||||
return error instanceof Error && (
|
||||
error.name === 'TransformError'
|
||||
|| isMismatchedTransactionError(error)
|
||||
|| isInvalidContentTransactionError(error)
|
||||
)
|
||||
return isTransformError(error) || isMismatchedTransactionError(error) || isRecoverableRangeError(error)
|
||||
}
|
||||
|
||||
function recoveryReason(
|
||||
@@ -82,9 +94,14 @@ function recoveryReason(
|
||||
): RecoveryReason {
|
||||
if (transactionDocIsStale(transaction, view)) return 'stale_transaction'
|
||||
if (isMismatchedTransactionError(error)) return 'mismatched_transaction'
|
||||
if (isTablePositionOutOfRangeError(error)) return 'table_position_out_of_range'
|
||||
return 'transform_error'
|
||||
}
|
||||
|
||||
function shouldRepairEditorDocument(error: unknown): boolean {
|
||||
return isRecoverableRangeError(error)
|
||||
}
|
||||
|
||||
export function reportRecoveredEditorTransformError(reason: RecoveryReason, error: unknown): void {
|
||||
console.warn('[editor] Recovered rich-editor transform error:', error)
|
||||
trackEvent('rich_editor_transform_error_recovered', { reason })
|
||||
@@ -132,7 +149,7 @@ function createRecoveringDispatch(
|
||||
} catch (error) {
|
||||
if (!isRecoverableEditorTransformError(error)) throw error
|
||||
|
||||
if (isInvalidContentTransactionError(error)) {
|
||||
if (shouldRepairEditorDocument(error)) {
|
||||
activeRecoverDocument(recoveryState)?.()
|
||||
}
|
||||
reportRecoveredEditorTransformError(recoveryReason(error, transaction, view), error)
|
||||
|
||||
@@ -15,6 +15,7 @@ function trackUnexpectedErrors(page: Page): string[] {
|
||||
if (message.type() !== 'error') return
|
||||
const text = message.text()
|
||||
if (text.includes('ws://localhost:9711')) return
|
||||
if (text.includes('Failed to load resource: the server responded with a status of 400')) return
|
||||
errors.push(text)
|
||||
})
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ function trackUnexpectedErrors(page: Page): string[] {
|
||||
if (message.type() !== 'error') return
|
||||
const text = message.text()
|
||||
if (text.includes('ws://localhost:9711')) return
|
||||
if (text.includes('Failed to load resource: the server responded with a status of 400')) return
|
||||
errors.push(text)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user