Skip to content

Optional nested form sections and improved JSON merge semantics - #26

Merged
jinbagi merged 1 commit into
masterfrom
codex/fix-bad-request-error-on-form-save
Jun 24, 2026
Merged

Optional nested form sections and improved JSON merge semantics#26
jinbagi merged 1 commit into
masterfrom
codex/fix-bad-request-error-on-form-save

Conversation

@jinbagi

@jinbagi jinbagi commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Motivation

  • Provide a better UX for nested optional configuration blocks by allowing users to enable/disable sections (timeout, TLS, protocol, etc.) dynamically instead of always showing empty inputs.
  • Avoid accidentally overwriting unchanged parts of an existing resource when switching between visual and JSON editors by only merging fields that the user actually edited.
  • Fix a few form-control behaviors for JSON inputs and switches to improve validation and value handling.

Description

  • Introduced optional nested section components to toggle timeout, keepalive_pool, and tls in upstreams and to toggle protocol for stream routes and timeout for routes, with Button controls to enable or remove each section and appropriate useWatch, unregister, and setValue handling.
  • Added helper predicates hasFieldValue, hasProtocolValue, and hasTimeoutValue to detect whether nested objects/fields should be considered enabled.
  • Implemented mergeEditablePayloadByDirty in utils/apisixEditable.ts and wired it into FormJsonTabs so merges from form editor only include fields that are actually dirty, while JSON-source submissions still perform the full merge via mergeEditablePayload.
  • Adjusted form behaviors: FormItemJsonInput now passes controller defaults differently to support toObject, FormItemSwitch coerces checked state with !!value, and several upstream select inputs had their defaultValue removed to rely on form defaults.
  • Minor UI/logic changes in FormJsonTabs to pass a source flag to safeSubmit, to set the pending payload correctly, and to reset/submit JSON editor flows accordingly.

Testing

  • Ran TypeScript type check with tsc --noEmit and it completed successfully.
  • Ran the project's test suite with yarn test and existing unit tests passed.
  • Performed a local build (yarn build) to ensure components compile and no runtime type errors occurred.

Codex Task

Copilot AI review requested due to automatic review settings June 24, 2026 05:01
@jinbagi
jinbagi merged commit 4cb5bf3 into master Jun 24, 2026
1 check passed
@jinbagi
jinbagi deleted the codex/fix-bad-request-error-on-form-save branch June 24, 2026 05:01

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d501345d74

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 136 to 138
control={control}
name={np('scheme')}
label="Scheme"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restore valid defaults for upstream selects

With the defaultValue removed here, FormItemSelect falls back to genControllerProps(props, []), so a new upstream or inline upstream with no form default registers scheme (and the same pattern repeats for type, hash_on, and pass_host) as an empty array. APISIX.Upstream only accepts those fields as string literals when present, so submitting an otherwise untouched upstream form fails validation instead of letting APISIX defaults apply. Either keep a valid string default or make the single-select fallback undefined.

Useful? React with 👍 / 👎.

const merged: Record<string, unknown> = { ...originalValue };

for (const [key, value] of Object.entries(formValue)) {
if (value === undefined) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve dirty undefined removals

When the new remove controls clear a nested optional block, they mark it dirty by setting the value to undefined, but this guard drops that dirty value before merging with rawData. In edit mode FormJsonTabs submits mergeEditablePayloadByDirty for visual saves, so removing route timeout, stream-route protocol, or upstream timeout/keepalive_pool/tls keeps the original block in the saved payload and the user cannot actually delete it from the form. Dirty undefined values need to be represented as deletions instead of skipped.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR improves APISIX resource editing UX by introducing toggleable optional nested configuration sections in several forms, and by changing how visual-form submissions are merged with existing (raw) resource data to only apply user-edited (dirty) fields.

Changes:

  • Added optional enable/remove UI for nested config blocks (e.g., upstream timeout/keepalive_pool/tls, route timeout, stream route protocol).
  • Introduced mergeEditablePayloadByDirty and wired it into FormJsonTabs so visual-editor saves merge only dirty fields when editing existing resources.
  • Adjusted JSON/switch form controls and upstream select defaults handling.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/utils/apisixEditable.ts Adds dirty-field-aware merge helper used to avoid overwriting untouched nested fields.
src/components/form/Switch.tsx Switch value coercion and controller defaults adjustment.
src/components/form/JsonInput.tsx Changes controller default fallback behavior for JSON inputs when toObject is enabled.
src/components/form/FormJsonTabs.tsx Uses dirty-field merge for form-source saves and adds a submit “source” distinction.
src/components/form-slice/FormPartUpstream/index.tsx Adds optional nested sections for upstream config and removes several select defaultValues.
src/components/form-slice/FormPartStreamRoute/index.tsx Adds optional protocol section enable/remove behavior.
src/components/form-slice/FormPartRoute/index.tsx Adds optional timeout section enable/remove behavior for routes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +184 to +204
const dirtyRecord = isRecord(dirtyFields) ? dirtyFields : {};
const merged: Record<string, unknown> = { ...originalValue };

for (const [key, value] of Object.entries(formValue)) {
if (value === undefined) continue;

const keyDirty = dirtyRecord[key];
if (keyDirty === true) {
merged[key] = value;
continue;
}

if (isRecord(keyDirty) && isRecord(value)) {
const previous = merged[key];
const base = isRecord(previous) ? previous : {};
const nested = mergeEditablePayloadByDirty(base, value, keyDirty);
if (Object.keys(nested as Record<string, unknown>).length > 0) {
merged[key] = nested;
}
}
}
Comment on lines 41 to 46
const { objValue = {} } = props;
const {
controllerProps: rawControllerProps,
restProps: { toObject, label, description, ...restProps },
} = genControllerProps(props, props.toObject ? objValue : '');
} = genControllerProps(props, props.toObject ? undefined : '');
const controllerProps = useMemo(() => {
Comment on lines 135 to 138
<FormItemSelect
control={control}
name={np('scheme')}
label="Scheme"
Comment on lines 158 to 162
<FormItemSelect
control={control}
name={np('type')}
label="Type"
defaultValue={APISIX.UpstreamBalancer.options[0].value}
data={APISIX.UpstreamBalancer.options.map((v) => v.value)}
Comment on lines 165 to 169
<FormItemSelect
control={control}
name={np('hash_on')}
label="Hash On"
defaultValue={APISIX.UpstreamHashOn.options[0].value}
data={APISIX.UpstreamHashOn.options.map((v) => v.value)}
Comment on lines 188 to 192
<FormItemSelect
control={control}
name={np('pass_host')}
label="Pass Host"
defaultValue={APISIX.UpstreamPassHost.options[0].value}
data={APISIX.UpstreamPassHost.options.map((v) => v.value)}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants