Skip to content

refactor(forms): validate the schema form before page-level checks#26

Open
Aleksei Sviridkin (lexfrei) wants to merge 1 commit into
mainfrom
refactor/schemaform-vmdisk-lookup
Open

refactor(forms): validate the schema form before page-level checks#26
Aleksei Sviridkin (lexfrei) wants to merge 1 commit into
mainfrom
refactor/schemaform-vmdisk-lookup

Conversation

@lexfrei
Copy link
Copy Markdown
Contributor

@lexfrei Aleksei Sviridkin (lexfrei) commented Jun 1, 2026

What

In the four backup create pages (Backup, BackupJob, BackupPlan, BackupRestoreJob), run the RJSF SchemaForm.validate() call right after the name check and before the page-level required-field alerts.

Why

The submit button lives outside RJSF and bypasses its validation, so each page triggers validate() explicitly. It previously ran only after the manual page-level alert(...) checks, so a schema-required field left empty exited via an alert without RJSF ever rendering its inline errors. Validating first makes inline errors show consistently; the page-level alerts then cover only page-only requirements (application/backup reference, strategy, timestamp).

Scope

This PR originally also lifted the VM disk lookup into SchemaForm and fixed an optional StorageClass not staying cleared. Both are now obsolete: the DynamicOptionsWidget refactor (#30) removed VMDiskWidget and StorageClassWidget, moved option-fetching into a generic x-cozystack-options mechanism, and already latches the auto-default so a cleared optional field sticks. The PR is reduced to the validation-ordering change, which is still missing on main.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 1, 2026

Warning

Review limit reached

@lexfrei, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 20 minutes and 49 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8bc00cf2-8d52-4ce4-af9c-8416a3880cc2

📥 Commits

Reviewing files that changed from the base of the PR and between fd1e947 and 2ac2b94.

📒 Files selected for processing (8)
  • apps/console/src/routes/BackupCreatePage.test.tsx
  • apps/console/src/routes/BackupCreatePage.tsx
  • apps/console/src/routes/BackupJobCreatePage.test.tsx
  • apps/console/src/routes/BackupJobCreatePage.tsx
  • apps/console/src/routes/BackupPlanCreatePage.test.tsx
  • apps/console/src/routes/BackupPlanCreatePage.tsx
  • apps/console/src/routes/BackupRestoreJobCreatePage.test.tsx
  • apps/console/src/routes/BackupRestoreJobCreatePage.tsx
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/schemaform-vmdisk-lookup

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added size/L This PR changes 100-499 lines, ignoring generated files area/forms Issues or PRs related to RJSF schema forms and widgets (backup, external-ips, storage-class, etc.) kind/cleanup Categorizes issue or PR as related to cleanup of code, process, or technical debt labels Jun 1, 2026
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors SchemaForm to conditionally fetch VMDisks only when a disks array is present in the schema, passing the options down to VMDiskWidget via formContext. It also updates StorageClassWidget to prevent auto-defaulting from re-triggering after a user clears the field, introduces an opportunistic useOptionalTenantContext hook, and runs form validation earlier in backup creation pages. The review feedback suggests enhancing schemaHasDiskField to recursively traverse array items for nested disks fields, and initializing hasAutoDefaulted in StorageClassWidget based on the initial value on mount to better support pre-existing values.

Comment on lines +289 to +300
function schemaHasDiskField(schema: unknown): boolean {
if (!schema || typeof schema !== "object") return false
const properties = (schema as { properties?: Record<string, unknown> }).properties
if (!properties || typeof properties !== "object") return false
for (const [key, value] of Object.entries(properties)) {
if (key === "disks" && isDiskArraySchema(value)) return true
if (value && typeof value === "object" && (value as { properties?: unknown }).properties) {
if (schemaHasDiskField(value)) return true
}
}
return false
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The current implementation of schemaHasDiskField only traverses nested objects if they have a properties field. It does not traverse into array items (which contain object schemas with their own properties). If a schema defines a disks field nested inside an array of objects, this function will fail to detect it.

We can make the traversal more robust by checking both properties and items recursively.

function schemaHasDiskField(schema: unknown): boolean {
  if (!schema || typeof schema !== "object") return false

  const items = (schema as { items?: unknown }).items
  if (items && typeof items === "object") {
    if (schemaHasDiskField(items)) return true
  }

  const properties = (schema as { properties?: Record<string, unknown> }).properties
  if (!properties || typeof properties !== "object") return false

  for (const [key, value] of Object.entries(properties)) {
    if (key === "disks" && isDiskArraySchema(value)) return true
    if (value && typeof value === "object") {
      if (schemaHasDiskField(value)) return true
    }
  }
  return false
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks — this traversal is intentionally identical to the widget binder addVMDiskWidgets, which also recurses only through properties. schemaHasDiskField is the fetch gate and has to match the binder exactly; recursing the gate into array items without doing the same in the binder would reintroduce the gate/binder divergence this PR closes (disks detected for fetch but never bound to VMDiskWidget, or vice-versa). There's a test pinning that parity ("gate matches the widget binder"). No shipped VM openAPISchema nests disks inside an array of objects — it lives at a fixed top-level disks[] path — so the deeper recursion isn't needed today. If a chart ever requires it, I'd add items recursion to the gate and the binder together so they stay in lockstep.

Comment on lines 56 to 62
onChange={(e) => {
if (!e.target.value) hasAutoDefaulted.current = false
// Any user interaction (including clearing) permanently disables the
// auto-default, so an optional field the user clears is not immediately
// refilled with the cluster default on the next render.
hasAutoDefaulted.current = true
onChange(e.target.value || undefined)
}}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

While setting hasAutoDefaulted.current = true on user interaction prevents the auto-default from re-triggering when the user clears the field, it does not handle cases where the field is cleared programmatically or if the component is remounted with a pre-existing value.

To make this more robust, consider initializing hasAutoDefaulted based on whether a value is already present on mount (e.g., const hasAutoDefaulted = useRef(value !== undefined && value !== "") on line 37). This ensures that pre-existing values (including explicitly saved empty values in edit flows) are respected and not overwritten by the cluster default.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Both cases are already handled. A remount with a pre-existing value doesn't auto-default: the effect is guarded by !value, so it never fires when a value is present (covered by the test "does not snap back to the default after clearing a value loaded from formData"). In the controlled RJSF form the value only ever changes through this widget's own onChange, which sets hasAutoDefaulted.current = true, so there's no programmatic-clear path that bypasses the guard. Also, useRef(value !== undefined && value !== "") evaluates to false for an explicitly-empty "", so it wouldn't preserve a saved-empty value the way the suggestion intends. Keeping the current logic.

@lexfrei Aleksei Sviridkin (lexfrei) force-pushed the refactor/schemaform-vmdisk-lookup branch from 69c48cf to 622e603 Compare June 1, 2026 17:29
@github-actions github-actions Bot added size/XL This PR changes 500-999 lines, ignoring generated files and removed size/L This PR changes 100-499 lines, ignoring generated files labels Jun 1, 2026
@lexfrei Aleksei Sviridkin (lexfrei) force-pushed the refactor/schemaform-vmdisk-lookup branch 3 times, most recently from 0ab84bc to 3b38d70 Compare June 1, 2026 18:05
@lexfrei Aleksei Sviridkin (lexfrei) marked this pull request as ready for review June 1, 2026 18:17
The backup create pages ran SchemaForm.validate() only after their manual
required-field alerts, so a schema-required field left empty exited via an
alert without RJSF ever rendering its inline errors. Run validate() right
after the name check, before the page-level alerts, so inline errors show
consistently and the alerts only cover page-only requirements.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
@lexfrei Aleksei Sviridkin (lexfrei) force-pushed the refactor/schemaform-vmdisk-lookup branch from 3b38d70 to 2ac2b94 Compare June 5, 2026 10:16
@github-actions github-actions Bot added size/L This PR changes 100-499 lines, ignoring generated files and removed size/XL This PR changes 500-999 lines, ignoring generated files labels Jun 5, 2026
@lexfrei Aleksei Sviridkin (lexfrei) changed the title refactor(forms): lift VM disk lookup into SchemaForm + form-validation fixes refactor(forms): validate the schema form before page-level checks Jun 5, 2026
@lexfrei
Copy link
Copy Markdown
Contributor Author

Reworked: rebased onto main and reduced scope to the validation-ordering change. The other two parts — lifting VM disk lookup into SchemaForm and the optional-StorageClass-clear fix — were superseded by the merged DynamicOptionsWidget refactor (#30), which removed VMDiskWidget/StorageClassWidget, genericized option-fetching via x-cozystack-options, and already latches hasAutoDefaulted so a cleared optional field stays empty. Force-pushed; please re-review.

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

Labels

area/forms Issues or PRs related to RJSF schema forms and widgets (backup, external-ips, storage-class, etc.) kind/cleanup Categorizes issue or PR as related to cleanup of code, process, or technical debt size/L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant