Skip to content

feat: add schema-backed DAG param forms#2039

Merged
yottahmd merged 4 commits intomainfrom
feat/schema-backed-dag-param-forms
Apr 26, 2026
Merged

feat: add schema-backed DAG param forms#2039
yottahmd merged 4 commits intomainfrom
feat/schema-backed-dag-param-forms

Conversation

@yottahmd
Copy link
Copy Markdown
Collaborator

@yottahmd yottahmd commented Apr 26, 2026

Summary

  • expose a renderable resolved JSON Schema for schema-backed DAG params when the supported subset is safe for direct UI rendering
  • render schema-backed DAG start forms in the UI with RJSF while keeping the existing string params submission contract and legacy fallback paths
  • tune schema-form control styling so filled inputs, radios, checkboxes, and focus states are visually consistent in the start modal

Testing

  • go test ./internal/core/spec -run 'TestBuildInlineSchemaParamPlan_(ExposesRenderableParamSchema|OmitsUnsupportedNestedParamSchema)' -count=1
  • go test ./internal/service/frontend/api/v1 -run 'TestToDAGDetailsIncludesParamSchema' -count=1
  • pnpm exec tsc --noEmit
  • pnpm exec vitest run src/features/dags/components/dag-execution/tests/StartDAGModal.test.tsx src/features/dags/components/dag-execution/tests/paramSchemaForm.test.ts

Closes #2033

Summary by CodeRabbit

  • New Features
    • Added schema-driven parameter forms for DAG execution with JSON-schema validation and custom form widgets.
    • Parameter input now supports schema-based form rendering with fallback to traditional input fields when schema unavailable.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6a387501-acbd-4b56-92a8-3258c0477419

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request introduces support for JSON Schema-driven parameter rendering in DAG execution. It adds a new optional paramSchema field across the data model (backend DAG specs, API schema, and TypeScript types) and implements schema sanitization logic to render only subset-compatible constructs. The frontend integrates React JSON Schema Form (RJSF) with custom widgets and templates to provide radio buttons, dropdowns, and typed inputs based on the resolved parameter schema.

Changes

Cohort / File(s) Summary
API Schema & Generation
api/v1/api.yaml, api/v1/api.gen.go
Added paramSchema field (object type with additionalProperties) to DAGDetails schema in OpenAPI spec; generated struct field includes new optional ParamSchema property.
Core DAG Model & Metadata
internal/core/dag.go, internal/core/spec/dag.go
Extended DAG struct with ParamSchema json.RawMessage field; added param_schema transformer to DAG spec metadata to expose computed parameter schema.
Parameter Schema Sanitization
internal/core/spec/dag_param_schema.go
New module implementing schema sanitization pipeline: validates and reduces JSON Schema to only renderable object/property constructs, rejects unsupported features (conditionals, arrays, patternProperties, boolean combinators), and supports constrained oneOf with scalar typing. Enforces type-aware constraint preservation (pattern, bounds, length for strings/numbers).
Parameter Processing & Tests
internal/core/spec/dag_params.go, internal/core/spec/dag_params_inline_schema_test.go
Modified buildDAGParamsResult to compute renderable param schema via buildRenderableParamSchema and include it in the result. Added test cases verifying schema population with property metadata and handling of unsupported nested structures.
Service Layer Transformation
internal/service/frontend/api/v1/transformer.go, internal/service/frontend/api/v1/transformer_test.go
Added toJSONObject helper to unmarshal DAG.ParamSchema into map[string]any for API response; wired into toDAGDetails transformation. Test validates schema propagation and correct JSON decoding.
Frontend Type Definitions & Dependencies
ui/src/api/v1/schema.ts, ui/package.json
Added paramSchema?: Record<string, unknown> field to TypeScript DAGDetails schema. Added RJSF core, utils, shadcn adapter, and AJV v8 validator to npm dependencies.
Parameter Form Utilities
ui/src/features/dags/components/dag-execution/paramSchemaForm.ts, ui/src/features/dags/components/dag-execution/paramSchemaForm.test.ts
New module with buildParamSchemaFormData (coerces default params to inferred scalar types), buildParamSchemaUiSchema (assigns radio widget for short choice lists), and stringifyParamSchemaFormData (JSON serialization). Tests verify type coercion, radio widget assignment logic, and stable stringification.
RJSF Template & Widget Customization
ui/src/features/dags/components/dag-execution/schemaFormTemplates.tsx, ui/src/features/dags/components/dag-execution/schemaFormWidgets.tsx
Created SchemaBaseInputTemplate wrapper around shadcn template with bg-card styling. Introduced schemaFormWidgets registry with custom Checkbox, Radio, Select, and Textarea widgets; radio supports horizontal/vertical layout, checkbox includes optional description, all apply shadcn styling and event handling.
Modal Parameter Input
ui/src/features/dags/components/dag-execution/StartDAGModal.tsx, ui/src/features/dags/components/dag-execution/__tests__/StartDAGModal.test.tsx
Enhanced StartDAGModal to detect paramSchema and render RJSF form with custom widgets/templates when available; submission validates via form ref, serializes with stringifyParamSchemaFormData, and falls back to existing typed-fields logic when schema absent. Tests verify schema form rendering, validation, submission payload, and fallback to typed inputs.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add schema-backed DAG param forms' directly summarizes the main change: adding schema-backed parameter form support for DAG execution in the UI.
Linked Issues check ✅ Passed The PR implements all objectives from issue #2033: radio buttons for short option lists, dropdown selects for longer lists, per-parameter labels/descriptions, and enforced valid value selection from predefined options.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing schema-backed DAG parameter forms: backend schema generation, API contracts, frontend form rendering with RJSF, and comprehensive test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/schema-backed-dag-param-forms

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (9)
internal/service/frontend/api/v1/transformer_test.go (1)

119-134: LGTM — test covers happy-path propagation cleanly.

Optional follow-up: consider also asserting the negative path (e.g., core.DAG{} with no ParamSchema, or with malformed JSON) yields details.ParamSchema == nil, to lock in toJSONObject's nil-return contract against future regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/service/frontend/api/v1/transformer_test.go` around lines 119 - 134,
Add a negative-path unit test alongside TestToDAGDetailsIncludesParamSchema that
calls toDAGDetails with a core.DAG that has no ParamSchema (e.g., core.DAG{})
and another case with an invalid ParamSchema (malformed JSON) and assert that
details.ParamSchema == nil for both; locate and update the test in
transformer_test.go and reference the existing toDAGDetails/toJSONObject
behavior so the new test locks in the nil-return contract against regressions.
ui/src/features/dags/components/dag-execution/__tests__/StartDAGModal.test.tsx (1)

50-52: Minor: prefer beforeEach reset or vi.clearAllMocks().

afterEach only clears renderedFormProps. If more vi.fn() instances accumulate state across tests later, they'd be missed. Consider beforeEach(() => vi.clearAllMocks()) for symmetry.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ui/src/features/dags/components/dag-execution/__tests__/StartDAGModal.test.tsx`
around lines 50 - 52, The test teardown currently only calls
renderedFormProps.mockClear() in afterEach, which can miss other vi.fn() state;
replace or complement this with a beforeEach(() => vi.clearAllMocks()) to ensure
all mocks (including renderedFormProps and any future vi.fn() instances) are
reset before each test, or alternatively call vi.clearAllMocks() in afterEach
for symmetry; update the test file to add the beforeEach/afterEach call
referencing renderedFormProps and vi.clearAllMocks accordingly.
internal/core/spec/dag_param_schema.go (2)

45-47: Improve readability of the unsupported-construct rejection.

Long boolean conjunctions like this (and the analogous one at lines 75-77) are hard to scan and easy to miss when adding new JSON Schema keywords. Consider extracting a small helper.

♻️ Suggested refactor
func hasUnsupportedRootConstruct(s *jsonschema.Schema) bool {
    return len(s.PatternProperties) > 0 ||
        len(s.AllOf) > 0 || len(s.AnyOf) > 0 || len(s.OneOf) > 0 ||
        s.Not != nil || s.If != nil || s.Then != nil || s.Else != nil ||
        s.Items != nil || len(s.PrefixItems) > 0 || s.Contains != nil
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/spec/dag_param_schema.go` around lines 45 - 47, Extract the
long conjunctive check into a small helper like hasUnsupportedRootConstruct that
accepts *jsonschema.Schema (referencing the variable root used in the current
checks) and returns a bool; replace the current long boolean expression in the
early-return (the block using root.PatternProperties, root.AllOf, root.AnyOf,
root.OneOf, root.Not, root.If, root.Then, root.Else, root.Items,
root.PrefixItems, root.Contains) with a call to that helper, and do the same for
the analogous check later (the similar expression at the other return site) so
both places use the new function.

99-99: Type assertion is safe, but prefer explicit element cloning for clarity.

cloneAny(schema.Enum) safely returns []any since Enum is a slice and cloneAny explicitly handles the []any case. However, the type assertion can be avoided by directly cloning elements, which is clearer and follows the same defensive approach used elsewhere:

Suggested change
	if len(schema.Enum) > 0 {
-		result.Enum = cloneAny(schema.Enum).([]any)
+		enum := make([]any, len(schema.Enum))
+		for i, v := range schema.Enum {
+			enum[i] = cloneAny(v)
+		}
+		result.Enum = enum
	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/spec/dag_param_schema.go` at line 99, The assignment
result.Enum = cloneAny(schema.Enum).([]any) relies on a type assertion; instead,
iterate over schema.Enum, clone each element via cloneAny and append to a new
[]any slice, then assign that slice to result.Enum so element-level cloning is
explicit and matches the defensive pattern used elsewhere (update code handling
of schema.Enum in the logic around cloneAny, result.Enum, and schema.Enum).
ui/src/features/dags/components/dag-execution/paramSchemaForm.ts (1)

60-82: Edge case: empty string coerces to 0 for integer/number schemas.

Number('') is 0 and Number.isInteger(0) is true, so coerceParamSchemaValue('', integerSchema) returns 0 rather than the original empty string. Whether this is a problem depends on whether parseParams can produce empty-string values; if so, the user could see unexpected zeros pre-filled. Consider returning value when value.trim() === ''.

♻️ Suggested change
 function coerceParamSchemaValue(value: string, schema: JSONSchema): unknown {
+  if (value === '') {
+    return value;
+  }
   switch (inferScalarType(schema)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/features/dags/components/dag-execution/paramSchemaForm.ts` around
lines 60 - 82, The function coerceParamSchemaValue incorrectly coerces an empty
string to 0 for 'integer'/'number' schemas; update coerceParamSchemaValue to
first check if the incoming value is an empty string (e.g., value.trim() === '')
and return the original value unchanged before calling inferScalarType /
attempting Number(value) conversion; keep this check at the top of
coerceParamSchemaValue so integer/number/boolean branches don't convert blank
input and retain existing behavior for non-empty strings.
ui/src/features/dags/components/dag-execution/StartDAGModal.tsx (1)

256-262: Defensive guard reads well; minor: also reject empty properties.

paramSchema will be set whenever dagDetails.paramSchema is a plain object, including {} or one without properties. The backend sanitizer rejects schemas without properties so this is unlikely in practice, but a UI-side guard would prevent rendering a useless empty form if a malformed schema ever leaks through.

♻️ Suggested change
-    const schema = dagDetails?.paramSchema;
-    if (!schema || Array.isArray(schema) || typeof schema !== 'object') {
-      return undefined;
-    }
-    return schema as JSONSchema;
+    const schema = dagDetails?.paramSchema as JSONSchema | undefined;
+    if (!schema || Array.isArray(schema) || typeof schema !== 'object') {
+      return undefined;
+    }
+    if (!schema.properties || Object.keys(schema.properties).length === 0) {
+      return undefined;
+    }
+    return schema;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/features/dags/components/dag-execution/StartDAGModal.tsx` around lines
256 - 262, The memoized paramSchema currently accepts any plain object from
dagDetails.paramSchema; update the guard in the React.useMemo so it also rejects
schemas that lack a non-empty properties object (e.g., ensure schema.properties
is an object and Object.keys(schema.properties).length > 0) before returning
schema as JSONSchema; this prevents returning {} or schemas without properties
and avoids rendering an empty form.
ui/src/features/dags/components/dag-execution/schemaFormWidgets.tsx (1)

142-163: Consider whether currentValue should fall back to the encoded raw value.

When value doesn't match any enumOptions entry (e.g., a stale or out-of-range default), enumOptionsIndexForValue returns nothing and currentValue becomes undefined, leaving the radio group visually unselected even though formData holds a value. RJSF's stock RadioWidget handles this similarly, so this is consistent — but worth a brief inline comment to flag the intent so future readers don't "fix" it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/features/dags/components/dag-execution/schemaFormWidgets.tsx` around
lines 142 - 163, The currentValue computation returns undefined when value
doesn't match any enumOptions (via enumOptionsIndexForValue) which leaves the
radio group unselected; add a brief inline comment inside the React.useMemo
(near currentValue, enumOptionsIndexForValue, enumOptionValueEncoder,
enumOptions, and value references) stating this is intentional and that we
intentionally do not fall back to encoding the raw value so stale/out-of-range
defaults remain visually unselected, to prevent future maintainers from "fixing"
this behavior.
internal/service/frontend/api/v1/transformer.go (1)

443-453: Consider logging on unmarshal failure.

toJSONObject collapses both "empty" and "malformed JSON" into the same nil result. Since dag.ParamSchema is produced internally by buildRenderableParamSchema (which already json.Marshals a sanitized schema), an unmarshal error here indicates an internal inconsistency that would otherwise be invisible to operators. Consider at least logging the error.

♻️ Suggested change
 func toJSONObject(raw json.RawMessage) *map[string]any {
 	if len(raw) == 0 {
 		return nil
 	}

 	var value map[string]any
 	if err := json.Unmarshal(raw, &value); err != nil {
+		// Should not happen: paramSchema is produced via json.Marshal upstream.
+		logger.Warn("toJSONObject: failed to unmarshal param schema", "error", err)
 		return nil
 	}
 	return &value
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/service/frontend/api/v1/transformer.go` around lines 443 - 453,
toJSONObject currently hides JSON unmarshal failures by returning nil for both
empty and malformed input; update to log the unmarshal error (including the
error value and a brief context such as "failed to unmarshal dag.ParamSchema
produced by buildRenderableParamSchema") before returning nil. Locate the
toJSONObject function and add a logging statement that includes the raw input
length or a snippet and the error (so operators can see internal inconsistencies
between buildRenderableParamSchema and dag.ParamSchema) while preserving the
existing nil return behavior for len(raw)==0 and on error.
ui/src/features/dags/components/dag-execution/schemaFormTemplates.tsx (1)

1-1: Verify use of @rjsf/shadcn/lib/... deep import.

Importing from @rjsf/shadcn/lib/BaseInputTemplate/BaseInputTemplate.js reaches into the package's internal layout. If the package uses exports field gating in its package.json, this can break on a minor upgrade. Prefer a public re-export (often available via @rjsf/shadcn or its Templates/getDefaultRegistry) if exposed.

#!/bin/bash
# Inspect `@rjsf/shadcn`'s package.json to see whether deep '/lib/...js' subpaths are part of its public 'exports' map.
fd -i 'package.json' -t f node_modules/@rjsf/shadcn -d 2 2>/dev/null | head -5
# Fall back to repo-local lockfile to identify version & published exports surface
rg -nP '"@rjsf/shadcn"' -g 'package.json' -g 'pnpm-lock.yaml' | head -20
`@rjsf/shadcn` 6.5.1 BaseInputTemplate public export path
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/features/dags/components/dag-execution/schemaFormTemplates.tsx` at
line 1, The import is using a deep path into `@rjsf/shadcn`
("@rjsf/shadcn/lib/BaseInputTemplate/BaseInputTemplate.js") which can break on
upgrades; replace it with the package's public export instead (e.g., import
BaseInputTemplate from the top-level "@rjsf/shadcn" or pull it from the
package's exported Templates/getDefaultRegistry API) so that the code uses an
official public symbol (referencing BaseInputTemplate and any
Templates/getDefaultRegistry helpers) rather than a private lib subpath.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@ui/src/features/dags/components/dag-execution/__tests__/StartDAGModal.test.tsx`:
- Around line 106-113: Add an end-to-end test in StartDAGModal.test.tsx that
exercises the typed-fields mode submission flow: render StartDAGModal, switch to
typed-fields mode, fill the typed fields to produce the JSON array parameter
serialization, trigger submit, and assert onSubmit was invoked with the JSON
array payload (instead of the JSON object string). Also add a server-side
integration test for parseStringParams that sends the JSON array representation
and asserts it is parsed into the expected parameters; reference the
StartDAGModal test helper that sets onSubmit and the parseStringParams function
on the backend to locate the relevant code paths.

In `@ui/src/features/dags/components/dag-execution/StartDAGModal.tsx`:
- Line 319: Replace the untyped ref declaration `const schemaFormRef =
React.useRef<any>(null);` with a properly typed ref so `validateForm()` is
type-safe: import or reference the `Form` component and declare `const
schemaFormRef = React.useRef<React.ComponentRef<typeof Form> | null>(null);` (or
use `React.ComponentRef<typeof Form>` directly where appropriate), then use
`schemaFormRef.current?.validateForm()` to call validation—this removes `any`
and ensures `validateForm` is recognized by TypeScript.

---

Nitpick comments:
In `@internal/core/spec/dag_param_schema.go`:
- Around line 45-47: Extract the long conjunctive check into a small helper like
hasUnsupportedRootConstruct that accepts *jsonschema.Schema (referencing the
variable root used in the current checks) and returns a bool; replace the
current long boolean expression in the early-return (the block using
root.PatternProperties, root.AllOf, root.AnyOf, root.OneOf, root.Not, root.If,
root.Then, root.Else, root.Items, root.PrefixItems, root.Contains) with a call
to that helper, and do the same for the analogous check later (the similar
expression at the other return site) so both places use the new function.
- Line 99: The assignment result.Enum = cloneAny(schema.Enum).([]any) relies on
a type assertion; instead, iterate over schema.Enum, clone each element via
cloneAny and append to a new []any slice, then assign that slice to result.Enum
so element-level cloning is explicit and matches the defensive pattern used
elsewhere (update code handling of schema.Enum in the logic around cloneAny,
result.Enum, and schema.Enum).

In `@internal/service/frontend/api/v1/transformer_test.go`:
- Around line 119-134: Add a negative-path unit test alongside
TestToDAGDetailsIncludesParamSchema that calls toDAGDetails with a core.DAG that
has no ParamSchema (e.g., core.DAG{}) and another case with an invalid
ParamSchema (malformed JSON) and assert that details.ParamSchema == nil for
both; locate and update the test in transformer_test.go and reference the
existing toDAGDetails/toJSONObject behavior so the new test locks in the
nil-return contract against regressions.

In `@internal/service/frontend/api/v1/transformer.go`:
- Around line 443-453: toJSONObject currently hides JSON unmarshal failures by
returning nil for both empty and malformed input; update to log the unmarshal
error (including the error value and a brief context such as "failed to
unmarshal dag.ParamSchema produced by buildRenderableParamSchema") before
returning nil. Locate the toJSONObject function and add a logging statement that
includes the raw input length or a snippet and the error (so operators can see
internal inconsistencies between buildRenderableParamSchema and dag.ParamSchema)
while preserving the existing nil return behavior for len(raw)==0 and on error.

In
`@ui/src/features/dags/components/dag-execution/__tests__/StartDAGModal.test.tsx`:
- Around line 50-52: The test teardown currently only calls
renderedFormProps.mockClear() in afterEach, which can miss other vi.fn() state;
replace or complement this with a beforeEach(() => vi.clearAllMocks()) to ensure
all mocks (including renderedFormProps and any future vi.fn() instances) are
reset before each test, or alternatively call vi.clearAllMocks() in afterEach
for symmetry; update the test file to add the beforeEach/afterEach call
referencing renderedFormProps and vi.clearAllMocks accordingly.

In `@ui/src/features/dags/components/dag-execution/paramSchemaForm.ts`:
- Around line 60-82: The function coerceParamSchemaValue incorrectly coerces an
empty string to 0 for 'integer'/'number' schemas; update coerceParamSchemaValue
to first check if the incoming value is an empty string (e.g., value.trim() ===
'') and return the original value unchanged before calling inferScalarType /
attempting Number(value) conversion; keep this check at the top of
coerceParamSchemaValue so integer/number/boolean branches don't convert blank
input and retain existing behavior for non-empty strings.

In `@ui/src/features/dags/components/dag-execution/schemaFormTemplates.tsx`:
- Line 1: The import is using a deep path into `@rjsf/shadcn`
("@rjsf/shadcn/lib/BaseInputTemplate/BaseInputTemplate.js") which can break on
upgrades; replace it with the package's public export instead (e.g., import
BaseInputTemplate from the top-level "@rjsf/shadcn" or pull it from the
package's exported Templates/getDefaultRegistry API) so that the code uses an
official public symbol (referencing BaseInputTemplate and any
Templates/getDefaultRegistry helpers) rather than a private lib subpath.

In `@ui/src/features/dags/components/dag-execution/schemaFormWidgets.tsx`:
- Around line 142-163: The currentValue computation returns undefined when value
doesn't match any enumOptions (via enumOptionsIndexForValue) which leaves the
radio group unselected; add a brief inline comment inside the React.useMemo
(near currentValue, enumOptionsIndexForValue, enumOptionValueEncoder,
enumOptions, and value references) stating this is intentional and that we
intentionally do not fall back to encoding the raw value so stale/out-of-range
defaults remain visually unselected, to prevent future maintainers from "fixing"
this behavior.

In `@ui/src/features/dags/components/dag-execution/StartDAGModal.tsx`:
- Around line 256-262: The memoized paramSchema currently accepts any plain
object from dagDetails.paramSchema; update the guard in the React.useMemo so it
also rejects schemas that lack a non-empty properties object (e.g., ensure
schema.properties is an object and Object.keys(schema.properties).length > 0)
before returning schema as JSONSchema; this prevents returning {} or schemas
without properties and avoids rendering an empty form.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e39c5913-127d-4045-827a-b0ce6155f24b

📥 Commits

Reviewing files that changed from the base of the PR and between fb6fbf6 and 4278c86.

⛔ Files ignored due to path filters (1)
  • ui/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (17)
  • api/v1/api.gen.go
  • api/v1/api.yaml
  • internal/core/dag.go
  • internal/core/spec/dag.go
  • internal/core/spec/dag_param_schema.go
  • internal/core/spec/dag_params.go
  • internal/core/spec/dag_params_inline_schema_test.go
  • internal/service/frontend/api/v1/transformer.go
  • internal/service/frontend/api/v1/transformer_test.go
  • ui/package.json
  • ui/src/api/v1/schema.ts
  • ui/src/features/dags/components/dag-execution/StartDAGModal.tsx
  • ui/src/features/dags/components/dag-execution/__tests__/StartDAGModal.test.tsx
  • ui/src/features/dags/components/dag-execution/__tests__/paramSchemaForm.test.ts
  • ui/src/features/dags/components/dag-execution/paramSchemaForm.ts
  • ui/src/features/dags/components/dag-execution/schemaFormTemplates.tsx
  • ui/src/features/dags/components/dag-execution/schemaFormWidgets.tsx

Comment on lines +106 to +113
await waitFor(() =>
expect(onSubmit).toHaveBeenCalledWith(
'{"region":"us-west-2","count":5}',
undefined,
true
)
);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look at how the DAG-run start handler parses params to confirm it accepts: KEY="VAL" strings, JSON arrays, and JSON objects.
rg -nP -C5 '\bParams\s*[:=]|defaultParams|parseParams' --type=go -g 'internal/**' -g '!**/*_test.go' | head -200
ast-grep --pattern 'func ($_ $_) Start($$$) ($$$)'

Repository: dagucloud/dagu

Length of output: 28659


🏁 Script executed:

# Find the HTTP handler for POST /dags/.../start endpoint
rg -n 'start|Start' --type=go internal/service/handler -A5 -B2 | grep -E '(func|Start|POST|Dags)' | head -50

Repository: dagucloud/dagu

Length of output: 123


🏁 Script executed:

# Search for the API endpoint handler or router that maps to /dags/{id}/start
rg -n '/dags.*start|StartDAG|StartRun' --type=go --iglob '!**/*_test.go' | head -40

Repository: dagucloud/dagu

Length of output: 1024


🏁 Script executed:

# Look at the test file to see what formats are being tested
cat -n ui/src/features/dags/components/dag-execution/__tests__/StartDAGModal.test.tsx | head -150

Repository: dagucloud/dagu

Length of output: 4731


🏁 Script executed:

# Find the ExecuteDAG handler implementation
rg -n 'ExecuteDAG|func.*ExecuteDAG' --type=go --iglob '!**/*_test.go' -A10 | head -80

Repository: dagucloud/dagu

Length of output: 5776


🏁 Script executed:

# Find the StartDAGModal component implementation
cat -n ui/src/features/dags/components/dag-execution/StartDAGModal.tsx | head -200

Repository: dagucloud/dagu

Length of output: 7175


🏁 Script executed:

# Search for other test files or tests that test different parameter modes
fd -e test.tsx -e test.ts ui/src/features/dags/ | xargs rg -l 'onSubmit|params' | head -5

Repository: dagucloud/dagu

Length of output: 618


🏁 Script executed:

# Find paramSchemaForm.tsx to see how schema-mode params are serialized
fd . ui/src/features/dags -name 'paramSchemaForm*' -type f

Repository: dagucloud/dagu

Length of output: 289


🏁 Script executed:

# Find the stringifyParamSchemaFormData function
rg -n 'stringifyParamSchemaFormData' ui/src -A15 | head -40

Repository: dagucloud/dagu

Length of output: 3862


🏁 Script executed:

# Find the actual ExecuteDAG handler implementation in the API service
rg -n 'func.*ExecuteDAG.*\(.*w.*http|func.*\(.*API.*\).*ExecuteDAG' --type=go -A20 | head -100

Repository: dagucloud/dagu

Length of output: 7198


🏁 Script executed:

# Read the full ExecuteDAG handler implementation
sed -n '893,950p' internal/service/frontend/api/v1/dags.go

Repository: dagucloud/dagu

Length of output: 1685


🏁 Script executed:

# See how params are passed through the system
rg -n 'request.Body.Params' internal/service/frontend/api/v1/dags.go -A5 -B5 | head -80

Repository: dagucloud/dagu

Length of output: 1632


🏁 Script executed:

# Look at the complete flow - check what ExecuteDAGJSONBody expects
rg -n 'type ExecuteDAGJSONBody' api/v1/api.gen.go -A10

Repository: dagucloud/dagu

Length of output: 672


🏁 Script executed:

# Find executeDAGRun implementation
rg -n 'func.*executeDAGRun|func.*enqueueDAGRun' internal/service/frontend/api/v1/dags.go -A30 | head -100

Repository: dagucloud/dagu

Length of output: 1189


🏁 Script executed:

# Look for where params are validated or processed
rg -n 'ValidateStartParams|startparams|parseParams' internal/service/frontend/api/v1/dags.go -B3 -A3

Repository: dagucloud/dagu

Length of output: 436


🏁 Script executed:

# Find ResolveRuntimeParams implementation
rg -n 'func.*ResolveRuntimeParams' --type=go -A30 | head -100

Repository: dagucloud/dagu

Length of output: 7091


🏁 Script executed:

# Also search for the parseParamValue function which handles parsing different formats
rg -n 'func.*parseParamValue' --type=go -A20 | head -80

Repository: dagucloud/dagu

Length of output: 1390


🏁 Script executed:

# Find actual ResolveRuntimeParams implementation (not test)
rg -n 'func ResolveRuntimeParams' --type=go --iglob '!**/*_test.go' -A30

Repository: dagucloud/dagu

Length of output: 2325


🏁 Script executed:

# Look at dag_params_runtime.go to see the full implementation
cat -n internal/core/spec/dag_params_runtime.go | head -200

Repository: dagucloud/dagu

Length of output: 7169


🏁 Script executed:

# Find parseStringParams function to see how it handles string input
rg -n 'func parseStringParams' --type=go -A20 | head -50

Repository: dagucloud/dagu

Length of output: 1367


🏁 Script executed:

# Search for JSON unmarshaling of params
rg -n 'json.Unmarshal.*params|json.Decode.*params' --type=go -B3 -A3

Repository: dagucloud/dagu

Length of output: 1946


🏁 Script executed:

# Find tryParseJSONParams to verify it handles both objects and arrays
rg -n 'func tryParseJSONParams' --type=go -A30

Repository: dagucloud/dagu

Length of output: 2005


🏁 Script executed:

# Search for server-side tests that cover all three parameter serialization modes
fd . internal -name '*test.go' -type f | head -20 | xargs rg -l 'ExecuteDAG|startDAG' | head -5

Repository: dagucloud/dagu

Length of output: 467


🏁 Script executed:

# Search for integration tests that test ExecuteDAG with different param formats
rg -n 'ExecuteDAG|startDAG' internal/intg --type=go -B5 -A10 | head -150

Repository: dagucloud/dagu

Length of output: 4333


🏁 Script executed:

# Check the E2E tests for parameter handling
rg -n 'params|Params' ui/e2e/dag-run-actions.spec.ts -B2 -A2 | head -80

Repository: dagucloud/dagu

Length of output: 40


🏁 Script executed:

# Search for tests specifically for typed-fields mode (JSON array)
rg -n 'serializeParamFields|JSON.stringify.*items' ui/src --type=ts -B5 -A5

Repository: dagucloud/dagu

Length of output: 3367


🏁 Script executed:

# Look for all tests in StartDAGModal test file to see if there are other test cases
wc -l ui/src/features/dags/components/dag-execution/__tests__/StartDAGModal.test.tsx

Repository: dagucloud/dagu

Length of output: 141


🏁 Script executed:

# Check if there are other test files for StartDAGModal or parameter handling
fd . ui/src/features/dags -name '*test*' -o -name '*spec*' | grep -i 'param\|start'

Repository: dagucloud/dagu

Length of output: 289


🏁 Script executed:

# Check the full StartDAGModal test file to see all tests
cat ui/src/features/dags/components/dag-execution/__tests__/StartDAGModal.test.tsx

Repository: dagucloud/dagu

Length of output: 3758


Add end-to-end test for JSON array parameter format sent by typed-fields mode.

The test asserts onSubmit is invoked with the JSON object '{"region":"us-west-2","count":5}' (schema mode). The backend's parseStringParams correctly handles all three serialization formats: JSON objects, JSON arrays (from typed-fields mode), and KEY="VALUE" strings (from raw mode). However, the JSON array format lacks end-to-end coverage—the typed-fields rendering test doesn't verify submission, and no backend integration test confirms JSON array parameters work correctly. Consider adding a test case where typed-fields submits a JSON array payload and a corresponding server-side integration test.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ui/src/features/dags/components/dag-execution/__tests__/StartDAGModal.test.tsx`
around lines 106 - 113, Add an end-to-end test in StartDAGModal.test.tsx that
exercises the typed-fields mode submission flow: render StartDAGModal, switch to
typed-fields mode, fill the typed fields to produce the JSON array parameter
serialization, trigger submit, and assert onSubmit was invoked with the JSON
array payload (instead of the JSON object string). Also add a server-side
integration test for parseStringParams that sends the JSON array representation
and asserts it is parsed into the expected parameters; reference the
StartDAGModal test helper that sets onSubmit and the parseStringParams function
on the backend to locate the relevant code paths.

: undefined,
[paramSchema]
);
const schemaFormRef = React.useRef<any>(null);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

@rjsf/core 6.5.1 Form ref imperative handle TypeScript type

💡 Result:

In @rjsf/core version 6.5.1, the Form component is a class component that supports ref forwarding for imperative handle access. The ref type is RefObject<Form<T, S, F>> where Form is the class itself, generic over T (formData type, default any), S (schema type extending StrictRJSFSchema, default RJSFSchema), F (formContext type extending FormContextType, default any). The Form instance exposes these imperative methods: - submit: void - Programmatically submits the form - validateForm: boolean - Validates current form data and returns validity - reset: void - Resets form to initial state - focusOnError(error: RJSFValidationError): void - Focuses on field with given error Usage example: import React, { useRef } from 'react'; import Form from '@rjsf/core'; import { RJSFSchema } from '@rjsf/utils'; import validator from '@rjsf/validator-ajv8'; const schema: RJSFSchema = { /* your schema */ }; const MyForm = => { const formRef = useRef

(null); const handleCustomSubmit = => { if (formRef.current?.validateForm) { formRef.current.submit; } }; return ( <Form ref={formRef} schema={schema} validator={validator} onSubmit={({ formData }) => console.log(formData)} /> Custom Submit ); }; Note: FormProps<T,S,F> interface does not explicitly list 'ref' as it is handled by React's ref forwarding mechanism for class components. The internal formElement is RefObject due to variable tagName or _internalFormWrapper.

Citations:


🏁 Script executed:

# Check the Form import and typing in StartDAGModal.tsx
head -50 ui/src/features/dags/components/dag-execution/StartDAGModal.tsx | grep -E "import|Form"

Repository: dagucloud/dagu

Length of output: 1120


🏁 Script executed:

# Also check around line 319 for context
sed -n '310,330p' ui/src/features/dags/components/dag-execution/StartDAGModal.tsx

Repository: dagucloud/dagu

Length of output: 610


🏁 Script executed:

# Check package.json for `@rjsf/core` version
cat ui/package.json | grep -A 2 -B 2 "@rjsf"

Repository: dagucloud/dagu

Length of output: 341


🏁 Script executed:

# Find all usages of schemaFormRef in the file
grep -n "schemaFormRef" ui/src/features/dags/components/dag-execution/StartDAGModal.tsx

Repository: dagucloud/dagu

Length of output: 222


🏁 Script executed:

# Search for the validateForm call and surrounding context
grep -n "validateForm" ui/src/features/dags/components/dag-execution/StartDAGModal.tsx -A 3 -B 3

Repository: dagucloud/dagu

Length of output: 325


🏁 Script executed:

# Check what `@rjsf/shadcn` exports as Form
node << 'EOF'
const fs = require('fs');
// Check if we can inspect the package structure
const packagePath = 'ui/node_modules/@rjsf/shadcn/package.json';
try {
  if (fs.existsSync(packagePath)) {
    const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
    console.log('Main export:', pkg.main);
    console.log('Module export:', pkg.module);
    console.log('Exports field:', JSON.stringify(pkg.exports || {}, null, 2));
  } else {
    console.log('Package not found in node_modules');
  }
} catch (e) {
  console.log('Error:', e.message);
}
EOF

Repository: dagucloud/dagu

Length of output: 92


🏁 Script executed:

# Check the Form usage at line 536 and surrounding context
sed -n '530,545p' ui/src/features/dags/components/dag-execution/StartDAGModal.tsx

Repository: dagucloud/dagu

Length of output: 606


🏁 Script executed:

# Look for any other type hints or patterns in similar files
find ui/src -name "*.tsx" -type f -exec grep -l "useRef.*Form" {} \; | head -5

Repository: dagucloud/dagu

Length of output: 40


🏁 Script executed:

# Check if there are any TypeScript definitions or type declarations
find ui -name "*.d.ts" -o -name "tsconfig.json" | head -5

Repository: dagucloud/dagu

Length of output: 145


🏁 Script executed:

# Check tsconfig.json for TypeScript configuration
cat ui/tsconfig.json | head -40

Repository: dagucloud/dagu

Length of output: 774


🏁 Script executed:

# Search for other ref patterns with Form in the codebase
rg "useRef.*Form|ref.*Form" ui/src --type tsx -A 2 -B 2 | head -40

Repository: dagucloud/dagu

Length of output: 86


🏁 Script executed:

# Try searching for useRef patterns with proper regex
rg "useRef.*Form" ui/src --max-count 20 -A 1 -B 1

Repository: dagucloud/dagu

Length of output: 40


🏁 Script executed:

# Check if Form is explicitly typed anywhere else
rg "Form\>" ui/src/features/dags/components/dag-execution/StartDAGModal.tsx -B 2 -A 2 | head -50

Repository: dagucloud/dagu

Length of output: 135


🏁 Script executed:

# Search for Form usage in the file more carefully
grep "Form" ui/src/features/dags/components/dag-execution/StartDAGModal.tsx | head -20

Repository: dagucloud/dagu

Length of output: 960


🏁 Script executed:

# Check the documentation or type hints for `@rjsf/shadcn`
rg "export.*Form" ui --max-count 10

Repository: dagucloud/dagu

Length of output: 1084


🏁 Script executed:

# Check the actual type definition or documentation for `@rjsf/shadcn` Form
# Try to find any documentation in the project or package
find ui -name "*.md" -type f -exec grep -l "Form.*ref\|useRef" {} \;

Repository: dagucloud/dagu

Length of output: 40


🏁 Script executed:

# Let's check if there are any compilation errors currently or type checking
cd ui && npx tsc --noEmit 2>&1 | grep -i "schemaFormRef\|StartDAGModal" | head -20

Repository: dagucloud/dagu

Length of output: 40


Replace useRef<any> with a properly typed ref.

The ref should be typed against the Form component to ensure type safety for the validateForm() call and comply with TypeScript strict mode. Since Form is a class component from @rjsf/shadcn, use React.ComponentRef<typeof Form> to extract its instance type.

♻️ Suggested change
-  const schemaFormRef = React.useRef<any>(null);
+  const schemaFormRef = React.useRef<React.ComponentRef<typeof Form>>(null);

This eliminates the any type and makes the validateForm() call at line 375 self-documenting, as required by the coding guideline: "Use type annotations in TypeScript files".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const schemaFormRef = React.useRef<any>(null);
const schemaFormRef = React.useRef<React.ComponentRef<typeof Form>>(null);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/features/dags/components/dag-execution/StartDAGModal.tsx` at line 319,
Replace the untyped ref declaration `const schemaFormRef =
React.useRef<any>(null);` with a properly typed ref so `validateForm()` is
type-safe: import or reference the `Form` component and declare `const
schemaFormRef = React.useRef<React.ComponentRef<typeof Form> | null>(null);` (or
use `React.ComponentRef<typeof Form>` directly where appropriate), then use
`schemaFormRef.current?.validateForm()` to call validation—this removes `any`
and ensures `validateForm` is recognized by TypeScript.

@yottahmd yottahmd merged commit dabaa11 into main Apr 26, 2026
17 of 18 checks passed
@yottahmd yottahmd deleted the feat/schema-backed-dag-param-forms branch April 26, 2026 13:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Add UI Parameter Controls (Radio Buttons & Dropdowns) for Workflow Input

1 participant