Content - Fix Meta Title validation racing the field-debounce commit - #4324
Content - Fix Meta Title validation racing the field-debounce commit#4324geodem127 wants to merge 5 commits into
Conversation
Meta Title had no dataset exemption anywhere (hardcoded required in MetaTitle.tsx, unconditionally in Meta/index.tsx's REQUIRED_FIELDS, and unconditionally checked in createItem()'s hasMissingRequiredSEOFields), unlike Meta Description which was already made optional for datasets in #2988. A stale-closure race in ItemCreate.tsx's save callback could also intermittently force the URL path part to be required for datasets. Dataset items have no URL/page, so neither should ever be required. Refs #4276
The previous commit on this branch worked around #4276 by exempting dataset models from the Meta Title requirement. The actual bug is a race: Field.tsx debounces each field's onChange commit to the store by 500ms, so Editor.js's first-text-field auto-population of Meta Title (which applies to every non-block model, datasets included) doesn't reach the store until 500ms after the last keystroke. A save clicked before then validates against a value that hasn't committed yet. Revert the dataset-only exemption (content.js, Meta/index.tsx, MetaTitle.tsx are back to their pre-#4276-fix state) and fix the race instead: - useDebouncedInput now exposes flush(), wrapping lodash debounce's built-in flush. - Field.tsx adds that flush to the handle it already registers into the engine's refRegistry (the same registry the AI drawer uses), rather than introducing a new ref-forwarding path through Editor. - ItemCreate.tsx's save() flushes every registered field inside flushSync before validating. flushSync is required: flushing commits the value to the store, but React 18 batches that update, so Meta's validateMetaFields closure would otherwise stay stale until a re-render happens after this call already read it. - ItemCreate.tsx also now uses validateMetaFields()'s return value to gate the save (matching ItemEdit.js's existing pattern), instead of discarding it and checking possibly-stale SEOErrors state. - meta.spec.js's dataset test now asserts the created item's metaTitle actually matches what was auto-populated, instead of asserting Meta Title can be left blank. Refs #4276
QA Review — ✅ PASSValidates #4276: Manager UI - Creating New Content in a Dataset Model Requires SEO Information
Suggested Cypress coverage
|
Code Review — ✅ No blockers |
Negative QA🔵 Console warning: MetaTitle input switches from uncontrolled to controlled on Create save
Also checked and working correctly
|
No new e2e coverage needed for this fix; the existing meta.spec.js suite (including "Does not validate meta description for dataset items", which creates a dataset item the same way) already exercises the save path and passes with the debounce-flush fix in place. Refs #4276
Code Review — ✅ No blockers · 🟡 2 warning(s) — see inline comments |
…ly required fields Per #2984, Meta Title (like Meta Description already does) should be required only for single/multi-page items and optional for datasets, with the asterisk removed on the label. Restore that behavior: it was dropped when #4276 was originally "fixed" by requiring Meta Title but relying on auto-population, which took the wrong approach per that stated product intent. The debounce/flushSync fix from the previous commit is unaffected and still needed for pathPart/Meta Title auto-population on page and multi-page items. Also fixes a regression found by the PR's negative-QA review: whitespace-only text (e.g. " ") passed every required-field check here since `!value` treats a non-empty string of spaces as truthy. A dataset item's Meta Title bypassed this by not being required, but the same bug independently affects Meta Title on single/multi-page items, where it stays required. Added an isBlank() helper that trims before checking, used everywhere these fields are validated (content.js's createItem thunk, and Meta/index.tsx's live handleOnChange and validateMetaFields checks). Also fixed handleOnChange's useCallback missing REQUIRED_FIELDS and metaFields in its dependency array, which now varies by model type. Refs #4276, #2984
|
Addressed both review items:
|
Code Review — ✅ No blockers · 🟡 1 warning(s) — see inline comments |
Addresses two review comments on 1aae4aa's flushSync block: - refRegistry is a single app-wide registry keyed by field name, not by item. "Create & Add New Related Item" (RelationalFieldBase -> CreateNewItemDialog) portals a full nested ContentEditor/ItemCreate on top of a still-mounted parent page, so saving the nested dialog was flushing every registered field app-wide -- including any debounced edit still in progress on the parent's fields, force- committing it early. - flushSync forcing a synchronous re-render of every mounted field on every Save click was flagged as a jank risk; scoping the flush to just this model's fields bounds that re-render to what Save was already about to touch, rather than the whole app. Filter to entries whose registered contentModelZUID matches this ItemCreate's own modelZUID before flushing. This resolves the reported cross-model case (the common one for relational fields); a nested dialog creating an item of the *same* model as the parent is a narrower remaining edge case that would need itemZUID-level scoping in refRegistry itself to fully close. Refs #4276
|
Re: the negative-QA finding about manually-entered Meta Title being clobbered by a later edit to the first field — acknowledging this, leaving it out of scope for this PR. The bot's own report labels it pre-existing on the touched surface: the root cause is `Editor.js`'s unconditional `SET_ITEM_WEB metaTitle` on every edit to the first field while `isNewItem`, which predates this PR. This PR's flush-on-save does make it reproduce deterministically instead of depending on winning/losing the old debounce race, but the underlying bug — auto-population overwriting a manual edit — is a separate fix (tracking whether the user has manually touched Meta Title so auto-population stops once they have) and out of scope for a PR about the debounce race and restoring #2984's dataset-optional intent. |
Code Review — ✅ No blockers · 🟡 1 warning(s) — see inline comments |
Summary
Fixes #4276 ("Creating New Content in a Dataset Model Requires SEO Information") by fixing the actual race condition, and restores the product direction stated in #2984 (Meta Title optional for datasets, like Meta Description already is), rather than making Meta Title required-but-auto-populated everywhere.
Root cause of #4276:
Field.tsxdebounces every field'sonChangecommit to the store by 500ms (useDebouncedInput).Editor.js's first-text-field auto-population of Meta Title/Meta Link Text/path part runs inside that same debounced commit. If Save is clicked within 500ms of the last keystroke, the auto-populated value hasn't reached the Redux store yet, so client-side validation sees it as missing and blocks the save — even though the field visibly shows a value on screen. (The existingmeta.spec.jstest "Does not validate meta description for dataset items" already worked around this exact race with a hardcodedcy.wait(500)before Save.)A separate, compounding bug:
ItemCreate.tsx'ssave()calledmetaRef.current.validateMetaFields()but discarded its return value, instead gating the save on possibly-staleSEOErrorsstate. This let some invalid saves reach thecreateItemthunk, which returned{ err: "VALIDATION_ERROR" }with none of the fieldsItemCreate.tsx's error handling recognizes — so the failure was swallowed silently, leaving the user stuck on/newwith no visible error.Fix for the race (first commit):
useDebouncedInputnow exposesflush()(wraps lodash debounce's built-in flush).Field.tsxadds thatflushto the handle it already registers into the engine'srefRegistry(the same registry the AI drawer already uses to drive fields imperatively) — no new ref-forwarding path needed throughEditor.ItemCreate.tsx'ssave()flushes every registered field, wrapped influshSync, before validating.flushSyncis required: flushing commits the value to the Redux store, but React 18 batches that update, soMeta'svalidateMetaFieldsclosure would otherwise stay stale for the rest of the synchronoussave()call.ItemCreate.tsxnow usesvalidateMetaFields()'s return value to gate the save, matching the pattern already used inItemEdit.js.Restoring #2984's intent (second commit): an earlier commit on this branch fixed #4276 by making Meta Title required for all types and relying on the auto-population fix above. That contradicts explicit, still-relevant product direction from #2984: Meta Title (like Meta Description already does) should be required only for single/multi-page items, optional for datasets, with the asterisk removed. Restored that —
content.js,Meta/index.tsx'sREQUIRED_FIELDS, andMetaTitle.tsx'srequiredprop once again treat dataset models as SEO-exempt. The debounce/flushSync fix is unaffected and still needed for page/multi-page item auto-population.Whitespace-only regression (third commit, found by this PR's own negative-QA review): every required-field check here used
!value, which treats a string of spaces as truthy. This let a whitespace-only Meta Title through validation on models where it's still required (single/multi-page items), leaving items with no visible title anywhere. Added anisBlank()helper that trims before checking presence, applied everywhere Meta Title/parentZUID/pathPart/dynamic OG-TC fields are validated (content.js'screateItemthunk,Meta/index.tsx's livehandleOnChangeandvalidateMetaFields). Also fixedhandleOnChange'suseCallbackmissingREQUIRED_FIELDS/metaFieldsfrom its dependency array now thatREQUIRED_FIELDSvaries by model type.No new e2e coverage added — the existing
meta.spec.jssuite already exercises dataset item creation and covers this path.Test plan
cypress/e2e/content/meta.spec.js— all 4 existing tests pass, including "Does not validate meta description for dataset items" which creates a dataset item via the same save flow.cypress/e2e/content/content.spec.js— full regression pass against theField.tsxchange (46/46 passing, 5 pending as expected).npx tsc --noEmit— clean.🤖 Generated with Claude Code