Skip to content

feature(web): redesign network connection form with improved UX#3386

Merged
dgdavid merged 99 commits intomasterfrom
enhance-network-connection-form
Apr 14, 2026
Merged

feature(web): redesign network connection form with improved UX#3386
dgdavid merged 99 commits intomasterfrom
enhance-network-connection-form

Conversation

@dgdavid
Copy link
Copy Markdown
Contributor

@dgdavid dgdavid commented Apr 14, 2026

Redesign network connection form, delivering a significantly improved user experience for creating and editing network connections in Agama.

The new form has been implemented using TanStack Form for robust field state management, provides independent IP configuration modes, integrated device binding options and better validation feedback among other aspects. It also defers form validation until submission to avoid interrupting users while typing.

For more details check all changes sent to the feature branch until now:

dgdavid and others added 30 commits March 18, 2026 20:57
Adds @tanstack/react-form as a project dependency. TanStack Form will be
used to replace the current ad-hoc form state management, starting with
the network connection form.

Check https://tanstack.com/form/latest
This commit starts the reimplementation of the network connection form
using TanStack Form, replacing the previous ad-hoc form state management
with a structured, library-backed approach.

The form is intentionally minimal in this first iteration, name and
interface fields only, to establish the pattern cleanly before adding
complexity. The full form (IP settings, bond/bridge/VLAN config, etc)
will be built incrementally in subsequent commits.

TanStack Form was chosen for:

  - Declarative field registration with built-in state (dirty, touched,
    errors) per field, without manual useState plumbing
  - Type-safe form values inferred from defaultValues
  - A clean model for handling server errors via onSubmitAsync: the error
    is surfaced in the UI without throwing, keeping the form interactive
    so the user can read the message, adjust, and retry
  - A composable field component model that will allow reusable,
    form-aware widgets to be introduced as the form grows

As patterns emerge across iterations (reusable field components, subform
composition, complex validation), they will be extracted and documented
as part of this reimplementation effort, or as part of the
reimplementation of other forms in the codebase. Whichever comes first.
Replaces the former AddressesDataList with a simple textarea as an
intermediate step. The goal is to revisit the multi-value input later
with a more fluent and accessible approach.

Both IPv4 and IPv6 addresses are handled in a single field. Addresses
without a prefix default to /24 for IPv4 and /64 for IPv6. The field
label reflects the current requirement based on the selected methods,
following the field labelling conventions in
src/components/form/conventions.md (temporary file will be moved
elsewhere later).
Gateway fields are only shown when the corresponding method is manual,
which might suggest they are always required when visible. However,
according to the NetworkManager documentation, the gateway is not
mandatory even in manual mode: it is only meaningful when addresses are
set, and can be replaced by an explicit default route.

Following the conventions in src/components/form/conventions.md, fields
that are conditionally shown but can still be left blank should carry the
(optional) suffix to make that clear to the user.

See: https://networkmanager.dev/docs/api/latest/settings-ip4-config.html
Introduces a reusable component for rendering form field labels with an
optional suffix styled to be visually dimmer than the main label text.
This makes the suffix clearly secondary for sighted users while keeping
the full string accessible to screen readers as a single label.
Introduces a "Use custom DNS servers" checkbox that reveals a textarea
for entering nameservers. When unchecked, an empty array is submitted.
When checked, the field is shown and its value is submitted.

The field value is preserved in form state when the checkbox is
unchecked, so re-checking restores what the user previously typed.
Validation will be added in a later commit.

This establishes the checkbox opt-in as a pattern for advanced or rarely
needed fields, documented in src/components/form/conventions.md.
Formerly named FormLabel but renamed in this commit to LabelText because
it is a more accurate name due the fact that the component renders text
content passed to a FormGroup label prop, not a label element itself.

Adds a hidden prop that visually hides the text content while keeping
it in the DOM. This is used for fields that have no visible label due
to their context, such as the DNS textarea inside checkbox body.
Keeping real DOM text is preferred over aria-label because it is able to
be translated by browser tools and does not depend on ARIA support.
Adds a "Use custom DNS search domains" checkbox that reveals a textarea
for entering search domains, following the same checkbox opt-in pattern
as DNS servers.
Reverts the use of LabelText hidden for the DNS servers and DNS search
domains textareas. PatternFly's FormGroup reserves visual space for the
label area whenever a label prop is provided, even when its content is
visually hidden, leaving an unwanted gap in the layout.

Working around that with CSS overrides would be hacky and fragile for
little practical benefit in an application that manages its own
translations via _().

aria-label is used instead. It is widely supported and works correctly
in this context.
The Checkbox body prop renders its content inside a span element, which
is invalid HTML for block content like a FormGroup with a TextArea.

Replaces it with a NestedContent sibling rendered conditionally when the
checkbox is checked, which is both semantically correct and consistent
with how nested content is handled elsewhere in the application.
…m (part 1) (#3300)

This PR starts the reimplementation of the network connection form using
[TanStack Form](https://tanstack.com/form/latest), replacing the
previous ad-hoc form state management with a structured, library-backed
approach.

## What is included

In order to avoid an endless PR and make it easier to follow for review,
the whole reimplementation will be addressed in multiple PRs against a
feature branch. This first PR covers the following fields:

- Connection name and interface
- IPv4 and IPv6 method and gateway
- IP addresses (single textarea for both protocols, with dynamic label
reflecting the current requirement based on the selected methods)
- DNS servers (hidden behind a checkbox opt-in)
- DNS search domains (hidden behind a checkbox opt-in)

Additionally, two files have been included as part of this PR:

- `src/components/form/LabelText`: a reusable component that renders
text content for a FormGroup label prop, with an optional styled suffix
(e.g. "(optional)") and a hidden prop for fields that need an accessible
label without visible text.
- `src/components/form/conventions.md`: a set of form conventions
documented as patterns arose, covering field visibility patterns, label
suffixes, and accessibility guidance. It should be moved elsewhere after
reaching the master branch, but kept in the newly created form namespace
for now. The idea is to keep it up to date as the project evolves and
form patterns that suit it well arise.

## What is deferred to follow-up PRs

- **Field validation.** No client-side validation is done yet. The
server acts as the validator via `onSubmitAsync`. Empty required fields,
invalid addresses, and required DNS servers when checked will be added
incrementally.
- **Enhanced multi-value input.** IP addresses, DNS servers, and DNS
search domains are currently plain textareas allowing values separated
by space or new lines. These will be replaced with a more fluent, easy
to understand/work with, and accessible multi-value input.
- **Auto-generated connection name.** The name field has no default
value yet. It will derive a suggestion from the selected interface
and/or connection type.
- **Type-specific subforms.** Bond, bridge, and VLAN configuration.
- **Edition mode.** Once everything is in place, the form will be
adapted to manage the edition of a connection too, which has some
additional restrictions like Interface and Name not editable.
- **Reusable TanStack field components.** As patterns stabilize, field
components will be registered in `useAppForm` and shared across forms.

## Notes for reviewers

The conventions document (`src/components/form/conventions.md`) is worth
reading first, as it explains the reasoning behind field visibility and
label choices, and will apply to all future form work across the
application (on agreement, of course).

_Changelog entry postponed for the final PR from feature branch against
master branch._


---

Related to https://trello.com/c/rUEeqOkf (protected link)
Introduces `ChoiceField`, a generic TanStack Form-aware select component
for mode/behavior selection. Registered in `fieldComponents` so it is
accessed as `field.ChoiceField` inside `form.AppField` render props.

Splits form contexts into `hooks/form-contexts.tsx` to avoid the
circular import that would arise from field components importing
`useFieldContext` while `hooks/form.tsx` imports those same components
for registration.
Adds Pattern 5 (choice selector) and renumbers the former patterns 2–5
to make room for it. Reorders patterns from least to most intrusive and
rewrites the "Combining patterns" and "Choosing the right pattern"
sections accordingly.
Renders a protocol-specific IP configuration block using a three-level
structure: mode selector (Default/Custom), method selector
(Automatic/Manual), and the corresponding fields. Manual mode shows
required addresses plus optional gateway and DNS. Automatic mode offers
an opt-in checkbox for extra static settings on top of DHCP.
Uses `useFormContext` following the TanStack Form composition guide, so
it can be dropped into any `useAppForm`-backed form without
prop-drilling. Field names are passed explicitly via `fieldNames`.
Replace the flat IPv4/IPv6 method selectors, the combined IP Addresses
textarea, and the top-level "Use custom DNS servers" checkbox with two
IpSettings components, one per protocol. Each manages its own mode
(Default/Custom), method (Automatic/Manual), addresses, gateway,
nameservers, and the opt-in "With extra IPv4/IPv6 settings" toggle.

Wrap the form body in form.AppForm so child components can access the
form context via useFormContext. The submit handler merges per-protocol
addresses and nameservers into the flat arrays expected by the
Connection constructor, including values only from protocols with active
custom configuration.

All IpSettings field labels are prefixed with the protocol name (e.g.
"IPv4 Gateway" instead of "Gateway") so each label is self-sufficient
when announced by a screen reader navigating outside the visual
grouping. Sighted users benefit too: the prefix removes any ambiguity
when both protocols are visible at once. See WCAG 2.4.6 (Headings and
Labels):

https://www.w3.org/WAI/WCAG21/Understanding/headings-and-labels.html

The gateway field in DHCP+extra mode carries the suffix "(optional,
ignored without a static IP)" to clarify that a gateway alone has no
effect without at least one static address alongside it.
Replace the two-level Default/Custom mode + Auto/Manual method + "With
extra settings" checkbox structure with a single three-option selector:
Default, Automatic, and Manual.

  - IpSettings now takes only three field names: mode, addresses, gateway
  - Addresses are optional in Automatic mode and required in Manual mode
  - Gateway is shown for both non-default modes; omitted at submit time
    unless at least one address is present
  - DNS servers moved back to the top level in ConnectionForm as a shared
    checkbox+textarea
Most users choosing Automatic (DHCP) do not need to set static
addresses or a gateway alongside it. Showing those fields upfront
adds visual noise and may confuse users who are not aware that
combining DHCP with static settings is even a valid configuration.

A "Show advanced settings" toggle now appears alongside the mode
selector in Automatic mode, hiding the extra fields until explicitly
requested. Manual mode is unaffected and always shows addresses and
gateway, since those are the point of choosing it.

The submit handler only collects addresses and gateway for Automatic
mode when the advanced toggle is enabled.
…pproach 2)

Previous iterations either showed static address fields for all
Automatic users (too much upfront complexity) or hid them behind a
checkbox toggle alongside the selector (two controls where one should
suffice). Both violated the principle of progressive disclosure.

This approach keeps a single selector but adds a fourth option, Mixed,
which makes the uncommon DHCP+static combination an explicit choice
rather than a hidden extra. Users who just want DHCP pick Automatic and
see nothing else. Users who need static addresses on top of DHCP pick
Mixed and get exactly that. The right complexity is revealed only when
the user actively asks for it.

Descriptions do the heavy lifting: instead of hiding complexity behind
a toggle, they guide the user toward the right choice, including a
"not needed for most setups" nudge on Mixed.

Each option carries a protocol-aware description where relevant:
Automatic shows DHCP for IPv4 and SLAAC or DHCPv6 for IPv6, reflecting
how NetworkManager's auto method behaves differently per protocol.
Replace the plain FormSelect-based interface field with two dedicated
ChoiceField components for interface binding UI:

- BindingModeSelector: wraps the binding mode selection (Unbound / By
  device name / By MAC address) with descriptions that clarify each
option's effect on the connection.

- DeviceSelector: picks a network device by interface name or MAC
  address; the non-selected identifier is shown as a styled description
for context.

Both components use useFormContext internally and are laid out
side-by-side via Flex/FlexItem in ConnectionForm, with DeviceSelector
appearing only when the mode is not Unbound (via form.Subscribe).
The Default option is dropped from IP settings. It represented a
NetworkManager implementation detail (no method written to the profile)
that most users would not understand. Automatic now covers that common
case, with NetworkManager deciding how to configure the interface.
Mixed is renamed to Advanced DHCP (IPv4) or Advanced Automatic (IPv6),
making it clear it extends the automatic case rather than a separate
alternative.

Option descriptions are rewritten to be concise and neutral: no trailing
periods, no user-targeting language, no protocol jargon.

"Interface binding" renamed to "Device" and its options drop technical
vocabulary in favor of plain language: Any, Chosen by name, and Chosen
by MAC. The device picker that appears next to the binding selector gets
a visually hidden label, since the selector already provides enough
context.
The IPv4 and IPv6 mode selectors used different labels for the same
option: "Advanced DHCP" and "Advanced Automatic". The distinction was
technically accurate: IPv4 automatic addressing uses DHCP, while IPv6
uses SLAAC, making "DHCP" wrong for IPv6. Hence "Advanced Automatic".

However, such asymmetry could raise the exact question it tried to
avoid: why does one selector say DHCP and the other say Automatic? Both
words describe the underlying mechanism, not the outcome, conflicting
with the plain-language description style adopted for all other options.
This commit changes both labels to "Advanced". The description still
conveys the relationship to Automatic without encoding protocol-specific
implementation details in the label.
…-components

IpSettings, BindingModeSelector, and DeviceSelector previously used
useFormContext(), which is designed for generic leaf field components
and is deliberately untyped. Since these components render slices of a known
form, withForm() is the correct fit. The mismatch forced "as any" casts on
every field name and subscribe selector, silently bypassing TypeScript's
checks and leaving field renames undetected at compile time.

Rewriting the three components with withForm() removes all "as any"
casts and restores full type safety. The form options were extracted from
ConnectionForm using formOptions() and exported to allow sub-components
spread it in their withForm() definition for type inference.

Also, a convenience mergeFormDefaults helper has been added for handling
the case where some defaults depend on runtime hook values and cannot be
declared statically.
And use <Text srOnly> directly instead. HiddenLabel implied a <label>
element but just rendered <Text srOnly>.
FlexItem with no props adds no value. Flex lays out its direct children
as flex items regardless.
dgdavid and others added 25 commits April 8, 2026 00:13
Rename the `template` prop to `sentence` and the `children` render prop
argument from `label` to `text`.

Broaden printf placeholder support from `%s` only to `%s`, `%d`, `%f`,
and `%i` via a single inlined regex, covering the full set of common
gettext specifiers.
This is the final PR of series of changes for reimplementing the network
connection form using [TanStack Form](https://tanstack.com/form/). Built
on top of #3353, it removes the old IP settings form and its
dependencies, completes the field component set, improves the name
auto-sync approach, and fixes a validation gap in advanced mode.

### Removal of IpSettingsForm

`IpSettingsForm` and its supporting components (`AddressesDataList`,
`DnsDataList`, `DnsSearchDataList`, `IpAddressInput`, `IpPrefixInput`,
and their tests) are deleted. The new `ConnectionForm` based on TanStack
Form fully replaces them.

### Bug fix: address validation in advanced mode

Addresses are optional in advanced mode, but if entered they must still
be valid. Previously only manual mode validated them, so invalid entries
could silently reach the server. The fix ensures to also validate
entries when they are present in advanced mode, even though none are
required.

### `generateConnectionName` as a pure function

`useConnectionName` was a React hook that had no real React dependency:
it computed a name from its arguments and returned a string. It has been
replaced by `generateConnectionName`, a plain function in
`src/utils/network.ts` that is easier to test and works anywhere without
a component context.

### Name auto-sync via form-level listeners

The previous approach used `useStore` to watch binding fields and a
`useEffect` to write the derived name back. Both are replaced by
TanStack Form's own [`listeners`
API](https://tanstack.com/form/latest/docs/framework/react/guides/listeners)),
which is the idiomatic way to handle field side effects in this library.

The listener fires on mount and on every change and, as in previous
approach, it skips the update when the name field is already dirty,
[using `isDirty` rather than
`isTouched`](https://tanstack.com/form/latest/docs/framework/react/guides/basic-concepts#field-state)
so that focusing and blurring the field without typing does not stop
auto-generation. [`dontRunListeners:
true`](https://github.com/TanStack/form/blob/42761767cf0662559d4f2c1a51b45b3720dbd451/packages/form-core/src/FieldApi.ts#L1446-L1449)
prevents the name write from re-triggering the listener.

### DropdownField rename

`ChoiceField` was renamed to `DropdownField` since the old name gave no
hint about what kind of input it renders. The TSDoc now explains that
the component uses a PatternFly menu-based combobox rather than a native
`<select>`, why the keyboard interaction differs from what users may
expect, and includes a TODO for implementing the W3C-recommended
arrow-key compromise.

### TextField and CheckboxField

Two new field components following the same `useFieldContext` pattern as
`DropdownField` and `ArrayField`:

- `TextField`: a text input that shows validation errors inline below
the input.
- `CheckboxField`: a checkbox with an optional description.

Both are registered in `useAppForm` and wired into `ConnectionForm`
(name field, DNS toggles) and `IpSettings` (gateway field), replacing
the inline markup that was previously scattered across those components.

A `## Field component conventions` section was added to
`form-contexts.ts` to document the shared contract: `onChange` is wired
internally, `onBlur` is intentionally not wired (submit-only validation,
revisit when a use case arises), and lifecycle events belong on
`form.AppField` via `listeners`, not on the component itself.

### Action button form components

Two action button components are registered under `formComponents` so
they are available on any typed form instance without form-specific
wiring:

- `form.SubmitButton`: reads `isSubmitting` via `useFormContext` and
renders a submit button that shows a loading indicator and disables
itself while the form is submitting.
- `form.CancelButton`: encapsulates `navigate(-1)` via `useNavigate`,
rendering a link button that returns to the previous page.

`ConnectionForm`'s action group reduces to:

```tsx
<ActionGroup>
  <form.SubmitButton />
  <form.CancelButton />
</ActionGroup>
```
### Form conventions update

`src/components/form/conventions.md` has been updated to reflect the
implemented state of `ConnectionForm`. Stale notes are replaced with
accurate descriptions, conditional rendering is consistently described
as "not rendered" rather than "hidden" throughout, and a new Validation
section explains the submit-only approach, the cross-field
`onSubmitAsync` pattern, and the `setErrorMap` workaround needed to
re-enable submission after a failed attempt.
Adds `mergeArrays` option to concatenate array properties instead of
replacing them based on precedence.

When merging, arrays from both sources are concatenated with duplicates
removed. Non-array properties continue following precedence rules.

This allows merging network connections where config single values
(method4, gateway4) must override system, but arrays (addresses,
nameservers) should combine so users see existing system data even when
config has empty arrays.
Updates ConnectionForm to properly display Advanced mode when config has
no method but system already has addresses.

This fixes the issue where editing a connection with undefined method
but existing system addresses would incorrectly show Automatic instead
of Advanced mode. On the other way around, shows Automatic (unset)
instead of Advance when the method is "auto" but there are no addresses
at all.
Simplifies connection name generation to only use the connection type,
removing device-specific details (interface name, MAC address). Remove
the no longer needed onChange listener to avoid unnecessary re-execution
on every form field change.
When users enter an IP address without a CIDR prefix, the system now
automatically appends a default value: /24 for IPv4 and /64 for IPv6.

This normalization only applies to valid IP addresses; invalid input
remains unchanged and is handled later by existing validation.

Applying the prefix at this stage ensures users can immediately see the
final value that will be saved, even if they did not specify a prefix
manually.
When users enter IP addresses in the connection form without specifying
a prefix (like /24), the system now adds one automatically using
standard networking conventions:

 - 10.0.0.1: 10.0.0.1/8 (Class A)
 - 172.16.0.1: 172.16.0.1/16 (Class B)
 - 192.168.1.1: 192.168.1.1/24 (Class C)
 - 2001:db8::1: 2001:db8::1/64 (IPv6)
Replace hardcoded /24 and /64 prefixes with addDefaultIPPrefix utility
which applies classful networking rules (Class A: /8, Class B: /16,
Class C: /24, IPv6: /64).
Addresses now get default prefixes both when entered by users and when
loaded from the backend, providing consistent UX. Removed the withPrefix
defensive check at form submission since all addresses already have
prefixes.
Show format examples for IP addresses, gateways, DNS servers, and DNS
search domains. IP address fields explain that prefix is auto-added if
omitted.

Also fixed ArrayField to always show helper text instead of only when
there are errors.
Replaces "unset" IP mode with explicit "auto" and introduces typed
FormIpMode enum for better type safety and code clarity.
Introduces ADDRESS_REQUIRED_MODES constant to replace repetitive mode
comparisons throughout the codebase.
Fixes a bug where IP addresses appeared twice when editing a connection                                          that was previously saved with addresses.

The issue occurred because extendCollection merges config and system
connections with mergeArrays: true, concatenating addresses from both
sources. After saving a connection, the same addresses exist in both
config (user's explicit settings) and system (NetworkManager's active
state), causing duplication when editing.

Thus, this commit deduplicate addresses, nameservers, and DNS search
domains in connectionToFormValues before populating the form.
Add regression test to verify addresses are not duplicated when config
and system have the same address.
# TL;DR

This bonus PR for the network connection form series includes polish
improvements and bug fixes.

The IP configuration modes have been restructured: the "unset" mode
(undefined method) has been replaced with "auto" (method.auto) as the
new default, and all modes have been renamed for clarity. Additional,
Manual mode now requires both addresses and gateway (gateway was
optional before).

The PR also fixes a bug where IP addresses, DNS servers, and search
domains appeared duplicated when editing saved connections, caused by
merging config and system arrays without deduplication. Additional
improvements include better connection name generation, proper IP mode
inference when editing connections, comprehensive translator comments
across all files, and code cleanup.

---

## The original description

A bonus PR for the network connection revamp series for addressing few
minor things found while reviewing notes.

### Minor Cleanup

Removes unnecessary props and type casts.

### Add missing translator comments

All touched and new files in this series has been reviewed for adding
missing `// TRANSLATORS:` comments. These provide translators with
context for format strings, abbreviations, UI roles, and otherwise
ambiguous strings.
### Introduce a new `Interpolate` component.

A utility component for rendering translated strings that include a
single placeholder, allowing arbitrary React content to be inserted via
a render prop.

#### Purpose

Some translated sentences need to include React elements (like links,
buttons, or styled text). While it's possible to manually split strings
or build sentences in fragments (the scattered approach across the
codebase), it makes translations harder to maintain and future updates
more error-prone. Thus, this small component:

*   Centralizes the pattern in a reusable component.
*   Prevents accidental fragmented translation units.
*   Makes future improvements easier and safer.
* Closes an old debt that had been postponed, reducing ongoing risks and
disadvantages.

#### Usage example

```tsx
<Interpolate template={_("Or [remove all invalid entries.]")}>  
  {(label) => <Button onClick={clearInvalid}>{label}</Button>}  
</Interpolate>
```

#### Supported placeholder

*   `[label]`: text inside brackets is passed to `children` as `label`.
*   `%s` / `%d` / `%f` / `%i`:  standard gettext printf placeholders.

Only one placeholder per template is allowed by now. Strings without
placeholders render as plain text.

## 📝  Additions on 10th of April

### Add `mergeArrays` option to `extendCollection`

Adds a `mergeArrays` option to the `extendCollection` utility to
concatenate array properties instead of replacing them based on
precedence. It was needed in order to be able to compute and display the
IpAddresses and others from the system configuration when they are not
in config configuration. Reading the code could have a better insight of
that need.

### Infer IP mode from addresses when editing connections

Updates ConnectionForm to properly display "Advanced" mode when config
has no method but system already has addresses. Same in the other way
around, make the form able to display "Automatic" mode when method set
to auto but no addresses configured.

It fixes the issue where editing a connection with undefined method but
existing system addresses would incorrectly show Automatic instead of
Advanced mode.

### Simplify auto-generated connection name

Simplifies connection name generation to only use the connection type,
removing device-specific details (interface name, MAC address), which
produces a more human readable connection name.

## 📝 Additions on 14th of April

### IP Configuration Mode Improvements

**Mode Restructuring**
- Default mode changed from `"unset"` (undefined method) to `"auto"`
(method.auto)
- Renamed modes for clarity:
- "Default (automatic)" → "Auto": automatic configuration, no fields
shown
- "Explicit (automatic)" → "Advanced auto": automatic with required
addresses
  - "Manual": static configuration with required addresses and gateway

**Gateway Requirements**
- Manual mode: gateway now **required** (was optional)
- Advanced auto mode: gateway remains optional
- Added validation with error messages: "IPv4/IPv6 gateway is required"

### Bug Fix: Deduplication When Editing Connections

Fixed a bug where IP addresses, DNS servers, and search domains appeared
twice when editing a connection that was previously saved with those
values.

**Root Cause**: `extendCollection` with `mergeArrays: true` concatenates
config and system arrays. After saving, the same values exist in both
sources, causing duplication on re-edit.

**Solution**: Deduplicate addresses, nameservers, and DNS search domains
in `connectionToFormValues()` using Radashi's `unique()` function.

**Test Coverage**: Added regression tests for all three deduplication
scenarios.
Copy link
Copy Markdown
Contributor

@ancorgs ancorgs left a comment

Choose a reason for hiding this comment

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

LGTM

@dgdavid dgdavid merged commit feae9b2 into master Apr 14, 2026
26 checks passed
@dgdavid dgdavid deleted the enhance-network-connection-form branch April 14, 2026 16:06
@imobachgs imobachgs mentioned this pull request Apr 14, 2026
imobachgs added a commit that referenced this pull request Apr 14, 2026
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.

3 participants