diff --git a/packages/app/components/document/form-actions.tsx b/packages/app/components/document/form-actions.tsx
index 9a1becc07..ecd95a896 100644
--- a/packages/app/components/document/form-actions.tsx
+++ b/packages/app/components/document/form-actions.tsx
@@ -1,10 +1,12 @@
+import Button from 'react-bootstrap/Button'
import cloneDeep from 'lodash.clonedeep'
import isEqual from 'lodash.isequal'
-import { useRouter } from 'next/router'
-import { useEffect, useState } from 'react'
-import Button from 'react-bootstrap/Button'
-import { clearEmpties } from '../../utils/object'
+import Spinner from 'react-bootstrap/Spinner'
import styles from './form-actions.module.css'
+import { clearEmpties } from '../../utils/object'
+import { useEffect, useState } from 'react'
+import { useRouter } from 'next/router'
+import { wait } from '../../utils/time'
const DELETED_STATE = 'Deleted'
const DELETE_CONFIRMATION =
@@ -22,12 +24,14 @@ const FormActions = ({
onDelete = null,
CustomActions = null,
allowValidationErrors = false,
+ largeModel = false,
}) => {
const canSave = privileges.includes('edit') || privileges.includes('create')
const router = useRouter()
const [initialFormData, setInitialFormData] = useState(null)
const [isDirty, setIsDirty] = useState(false)
+ const [isSaving, setIsSaving] = useState(false)
useEffect(() => {
if (!formData) return
@@ -79,7 +83,12 @@ const FormActions = ({
}
}
- function handleSave() {
+ async function handleSave(largeModel: boolean) {
+ if (largeModel) {
+ // This hack is in place to handle RJSF's ajv8 validator performance issues.
+ await wait(500)
+ }
+
let brokenLinks = localStorage.getItem('brokenLinks')
let hasBrokenLinks =
brokenLinks && Object.values(JSON.parse(brokenLinks)).includes('false')
@@ -90,6 +99,9 @@ const FormActions = ({
'There are broken links in your document, are you sure you want to save?'
)
) {
+ // not saving because we need to address broken links
+ setIsSaving(false)
+
return
}
@@ -106,6 +118,9 @@ const FormActions = ({
})
if (errors.length) {
+ // not saving because we need to address errors
+ setIsSaving(false)
+
// errors are printed above the save button, pushing it down. scroll it back
setTimeout(() => {
let errorPanel = document.querySelector('.rjsf > .panel.errors')
@@ -189,9 +204,24 @@ const FormActions = ({
)}
diff --git a/packages/app/components/document/form.tsx b/packages/app/components/document/form.tsx
index 9cb864f8c..4d2477697 100644
--- a/packages/app/components/document/form.tsx
+++ b/packages/app/components/document/form.tsx
@@ -1,6 +1,6 @@
+import Button from 'react-bootstrap/Button'
import dynamic from 'next/dynamic'
import { useState } from 'react'
-import Button from 'react-bootstrap/Button'
// JsonSchemaForm widgets rely heavily on global window, so we'll need to load them in separately
// as the server side doesn't have a window!
@@ -18,6 +18,7 @@ const Form = ({
onChange = (data: any) => {},
}) => {
const [expandAll, setExpandAll] = useState(false)
+ const largeModel = model?.largeModel || false
let layout = JSON.parse(model?.uiSchema || model?.layout || '{}')
let formData = document?.doc || document || {}
@@ -64,6 +65,7 @@ const Form = ({
imageUploadUrl={process.env.NEXT_PUBLIC_IMAGE_UPLOAD_URL}
linkCheckerUrl="/meditor/api/validate/url-resolves"
allowValidationErrors={allowValidationErrors}
+ largeModel={largeModel}
/>
>
)
diff --git a/packages/app/components/jsonschemaform/jsonschemaform.tsx b/packages/app/components/jsonschemaform/jsonschemaform.tsx
index 5d05042d2..900847302 100644
--- a/packages/app/components/jsonschemaform/jsonschemaform.tsx
+++ b/packages/app/components/jsonschemaform/jsonschemaform.tsx
@@ -1,4 +1,11 @@
-import Form from '@rjsf/core'
+import Form, { FormProps } from '@rjsf/core'
+import type {
+ FormContextType,
+ FormValidation,
+ RJSFSchema,
+ StrictRJSFSchema,
+} from '@rjsf/utils'
+import validator from '@rjsf/validator-ajv8'
import jp from 'jsonpath'
import filter from 'lodash/filter'
import isEqual from 'lodash/isEqual'
@@ -7,8 +14,115 @@ import { useEffect, useRef } from 'react'
import fields from './fields/'
import templates from './templates/'
import widgets from './widgets/'
-import validator from '@rjsf/validator-ajv8'
-import type { FormValidation } from '@rjsf/utils'
+
+class LargeModelForm<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any
+> extends Form {
+ // Set a reference to the super's onChange method as a class field to prevent intermediate value errors.
+ superOnChange = this.onChange
+ // A list of element node names that we will target for overriding default behavior.
+ targetNodeNames = ['INPUT']
+ // A store of events that we conditionally re-propagate to RJSF.
+ eventStore = {}
+ // A store of state locks that get set on various events.
+ nodeShouldPropagateStore = {}
+
+ constructor(props: FormProps) {
+ super(props)
+ }
+
+ componentDidMount() {
+ this.formElement.current.addEventListener('input', this.handleInput, {
+ capture: true,
+ })
+ this.formElement.current.addEventListener('change', this.handleChange, {
+ capture: true,
+ })
+ this.formElement.current.addEventListener('blur', this.handleBlur, {
+ capture: true,
+ })
+
+ if (super.componentDidMount) {
+ super.componentDidMount()
+ }
+ }
+
+ componentWillUnmount() {
+ this.formElement.current.removeEventListener('input', this.handleInput, {
+ capture: true,
+ })
+ this.formElement.current.removeEventListener('change', this.handleChange, {
+ capture: true,
+ })
+ this.formElement.current.removeEventListener('blur', this.handleBlur, {
+ capture: true,
+ })
+
+ if (super.componentWillUnmount) {
+ super.componentWillUnmount()
+ }
+ }
+
+ static debounce(fn: Function, delay = 600) {
+ let timerId: NodeJS.Timeout
+
+ return (...args: any[]) => {
+ globalThis.clearTimeout(timerId)
+
+ timerId = setTimeout(() => fn(...args), delay)
+ }
+ }
+
+ /*
+ * Targeting only nodes in our allowlist, we conditionally stop propagation so that the ajv8 validator does not get triggered.
+ * We state lock the event so that later logic does not propagate its change event.
+ */
+ handleInput = (event: any) => {
+ if (this.targetNodeNames.includes(event.target.nodeName)) {
+ event.stopPropagation()
+
+ this.nodeShouldPropagateStore[event.target.id] = false
+ }
+ }
+
+ /*
+ * Targeting only nodes in our allowlist, we conditionally stop propagation so that the ajv8 validator does not get triggered.
+ * We store the event for later propagation.
+ */
+ handleChange = (event: any) => {
+ if (
+ this.targetNodeNames.includes(event.target.nodeName) &&
+ !this.nodeShouldPropagateStore[event.target.id]
+ ) {
+ event.stopPropagation()
+
+ this.eventStore[event.target.id] = event
+
+ return
+ }
+ }
+
+ /*
+ * Targeting only nodes in our allowlist, we check to make sure node's event has been stored.
+ * Because the ajv8 validator is slow on large schemas, we wait until the `blur` event before triggering an update.
+ * We mark the node as ready to propagate, re-dispatching the event.
+ * Debounce this handler for rapid tabbing, which triggers unnecessary state updates.
+ */
+ handleBlur = LargeModelForm.debounce((event: any) => {
+ if (
+ this.targetNodeNames.includes(event.target.nodeName) &&
+ this.eventStore[event.target.id]
+ ) {
+ this.nodeShouldPropagateStore[event.target.id] = true
+
+ this.eventStore[event.target.id].target.dispatchEvent(
+ this.eventStore[event.target.id]
+ )
+ }
+ })
+}
const JsonSchemaForm = ({
schema,
@@ -18,9 +132,11 @@ const JsonSchemaForm = ({
layout,
liveValidate = false,
allowValidationErrors = false,
- onInit = (form: any) => {},
- onChange = (event: any) => {},
+ largeModel = false,
+ onInit = (_form: any) => {},
+ onChange = (_event: any) => {},
}) => {
+ const FormType = largeModel ? LargeModelForm : Form
const formEl: any = useRef(null)
useEffect(() => {
@@ -75,7 +191,7 @@ const JsonSchemaForm = ({
}
return (
-