Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 34 additions & 10 deletions lib/page/_shared/components/layout_blocks/row_blocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,16 @@ class NetworkBadgeWidget extends StatelessWidget {
///
/// Uses [AppListTile] from UI Kit for consistent styling.
/// Use for DHCP reservations, port forwarding rules, etc.
///
/// When [isLoading] is true, displays a spinner in place of the switch.
class ToggleRow extends StatelessWidget {
final bool value;
final ValueChanged<bool>? onChanged;
final String title;
final String? subtitle;
final Widget? trailing;
final VoidCallback? onTap;
final bool isLoading;

const ToggleRow({
super.key,
Expand All @@ -150,6 +153,7 @@ class ToggleRow extends StatelessWidget {
this.subtitle,
this.trailing,
this.onTap,
this.isLoading = false,
});

@override
Expand All @@ -162,11 +166,16 @@ class ToggleRow extends StatelessWidget {
leading: SizedBox(
width: 44,
child: Center(
child: AppSwitch(
value: value,
onChanged: onChanged,
scale: 0.8,
),
child: isLoading
? SizedBox.square(
dimension: 26,
child: AppLoader(strokeWidth: 2),
)
: AppSwitch(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Warning W-1: AppLoader called without semanticLabel -- spinner is invisible to VoiceOver/TalkBack. Pass semanticLabel: loc(context).loading here and at the equivalent NetworkRow site.

value: value,
onChanged: onChanged,
scale: 0.8,
),
),
),
title: AppText.bodyMedium(
Expand Down Expand Up @@ -195,6 +204,8 @@ class ToggleRow extends StatelessWidget {
/// Network row block for WiFi networks with band badges, client count, and toggle.
///
/// Uses [AppListTile] from UI Kit for consistent styling.
///
/// When [isLoading] is true, displays a spinner in place of the switch.
class NetworkRow extends StatelessWidget {
final String ssidName;
final List<String> bands;
Expand All @@ -203,6 +214,7 @@ class NetworkRow extends StatelessWidget {
final int clientCount;
final ValueChanged<bool>? onChanged;
final VoidCallback? onShareTap;
final bool isLoading;

const NetworkRow({
super.key,
Expand All @@ -213,6 +225,7 @@ class NetworkRow extends StatelessWidget {
required this.clientCount,
this.onChanged,
this.onShareTap,
this.isLoading = false,
});

@override
Expand Down Expand Up @@ -261,14 +274,25 @@ class NetworkRow extends StatelessWidget {
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isEnabled && onShareTap != null) ...[
if (!isLoading && isEnabled && onShareTap != null) ...[
_ShareButton(onTap: onShareTap!),
AppGap.sm(),
],
AppSwitch(
value: isEnabled,
onChanged: onChanged,
),
isLoading
? SizedBox(
width: 52,
height: 32,
child: Center(
child: SizedBox.square(
dimension: 24,
child: AppLoader(strokeWidth: 2),
),
),
)
: AppSwitch(
value: isEnabled,
onChanged: onChanged,
),
],
),
),
Expand Down
19 changes: 11 additions & 8 deletions lib/page/dashboard/models/usp_dashboard_preset.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ extension UspDashboardPresetX on UspDashboardPreset {
'system_status',
'connected_devices',
'wifi_status',
'wifi_networks',
'time_settings',
'dhcp_reservations',
'port_forwarding',
Expand Down Expand Up @@ -176,7 +177,7 @@ List<LayoutItem> _standardLayout() => [
_item('firewall_overview', x: 6, y: 23, w: 6, h: 4),
];

/// Professional: all 17 cards — full feature set.
/// Professional: all 18 cards — full feature set.
///
/// ```
/// y=0: StatsPanel (12×1)
Expand All @@ -185,9 +186,10 @@ List<LayoutItem> _standardLayout() => [
/// y=9: TrafficAnalysis (6×5) | LanInfo (6×3)
/// y=14: EthernetPorts (6×3) | ConnectedDevices (6×4)
/// y=18: Topology (6×5) | DeviceAnalytics (6×5)
/// y=23: WiFiStatus (6×6) | WiFiPerformance (6×5)
/// y=29: FirewallOverview (6×4) | TimeSettings (6×3)
/// y=33: DhcpReservations (6×4) | PortForwarding (6×4)
/// y=23: WiFiStatus (6×4) | WiFiPerformance (6×5)
/// y=28: WiFiNetworks (6×4) | FirewallOverview (6×4)
/// y=32: TimeSettings (6×3) | DhcpReservations (6×4)
/// y=36: PortForwarding (6×4)
/// ```
List<LayoutItem> _professionalLayout() => [
_item('stats_panel', x: 0, y: 0, w: 12, h: 1),
Expand All @@ -203,10 +205,11 @@ List<LayoutItem> _professionalLayout() => [
_item('device_analytics', x: 6, y: 18, w: 6, h: 5),
_item('wifi_status', x: 0, y: 23, w: 6, h: 4),
_item('wifi_performance', x: 6, y: 23, w: 6, h: 5),
_item('firewall_overview', x: 0, y: 29, w: 6, h: 4),
_item('time_settings', x: 6, y: 29, w: 6, h: 3),
_item('dhcp_reservations', x: 0, y: 33, w: 6, h: 4),
_item('port_forwarding', x: 6, y: 33, w: 6, h: 4),
_item('wifi_networks', x: 0, y: 28, w: 6, h: 4),
_item('firewall_overview', x: 6, y: 28, w: 6, h: 4),
_item('time_settings', x: 0, y: 32, w: 6, h: 3),
_item('dhcp_reservations', x: 6, y: 32, w: 6, h: 4),
_item('port_forwarding', x: 0, y: 36, w: 6, h: 4),
];

/// Monitoring: 8 cards — performance & analytics prominent.
Expand Down
3 changes: 2 additions & 1 deletion lib/page/local_network/cards/usp_dhcp_reservations_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class UspDhcpReservationsCard extends ConsumerWidget {
DhcpReservationUIModel reservation, bool isLoading) {
return ToggleRow(
value: reservation.enable,
isLoading: isLoading,
onChanged: isLoading || reservation.instancePath == null
? null
: (value) => performUspMutation(
Expand All @@ -87,7 +88,7 @@ class UspDhcpReservationsCard extends ConsumerWidget {
subtitle: reservation.ip,
trailing: AppIconButton(
icon: AppIcon.font(Icons.delete_outline, size: 18),
onTap: isLoading
onTap: isLoading || reservation.instancePath == null
? null
: () => _confirmDeleteDhcp(context, ref, reservation),
),
Expand Down
6 changes: 4 additions & 2 deletions lib/page/port_forwarding/cards/usp_port_forwarding_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ class UspPortForwardingCard extends ConsumerWidget {
PortForwardingRuleUIModel rule, bool isLoading) {
return ToggleRow(
value: rule.enabled,
onChanged: isLoading
isLoading: isLoading,
onChanged: isLoading || rule.instancePath == null
? null
: (value) => performUspMutation(
context,
Expand All @@ -95,7 +96,8 @@ class UspPortForwardingCard extends ConsumerWidget {
PortTriggeringRuleUIModel trigger, bool isLoading) {
return ToggleRow(
value: trigger.enabled,
onChanged: isLoading
isLoading: isLoading,
onChanged: isLoading || trigger.instancePath == null
? null
: (value) => performUspMutation(
context,
Expand Down
7 changes: 4 additions & 3 deletions lib/page/wifi_settings/cards/usp_wifi_networks_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class UspWifiNetworksCard extends ConsumerWidget {
final data = wifiData ?? ref.watch(wifiDataProvider).valueOrNull;
if (data == null) return const CardSkeleton.list(rows: 3);

final isLoading = ref.watch(uspMutationLoadingProvider) == 'wifi_network';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Warning W-2: Single 'wifi_network' loading key means all SSID rows spin simultaneously on any one toggle. Consider per-SSID keys or document that card-level locking is intentional.

final networks = _aggregateBySSID(
data.radioModels,
data.connectionDetailMap,
Expand All @@ -69,7 +70,7 @@ class UspWifiNetworksCard extends ConsumerWidget {
: Column(
children: [
for (var i = 0; i < networks.length; i++) ...[
_buildNetworkRow(context, ref, networks[i]),
_buildNetworkRow(context, ref, networks[i], isLoading),
if (i < networks.length - 1) AppGap.sm(),
],
],
Expand All @@ -93,15 +94,15 @@ class UspWifiNetworksCard extends ConsumerWidget {
BuildContext context,
WidgetRef ref,
_WifiNetworkEntry network,
bool isLoading,
) {
final isLoading = ref.watch(uspMutationLoadingProvider) == 'wifi_network';

return NetworkRow(
ssidName: network.ssidName,
bands: network.bands,
isGuest: network.isGuest,
isEnabled: network.isEnabled,
clientCount: network.clientCount,
isLoading: isLoading,
onChanged: isLoading
? null
: (value) => _confirmToggleNetwork(context, ref, network, value),
Expand Down
4 changes: 2 additions & 2 deletions test/page/dashboard/models/usp_dashboard_preset_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ void main() {
expect(UspDashboardPreset.standard.cardIds.length, 12);
});

test('professional has 17 cards (all)', () {
expect(UspDashboardPreset.professional.cardIds.length, 17);
test('professional has 18 cards (all)', () {
expect(UspDashboardPreset.professional.cardIds.length, 18);
});

test('monitoring has 8 cards', () {
Expand Down
4 changes: 2 additions & 2 deletions test/page/dashboard/providers/usp_layout_controller_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ void main() {
expect(layout.length, 8);
});

test('professional → 17 items', () async {
test('professional → 18 items', () async {
final container = await createInitializedContainer();
addTearDown(container.dispose);

Expand All @@ -593,7 +593,7 @@ void main() {

final layout =
container.read(uspSliverDashboardControllerProvider).exportLayout();
expect(layout.length, 17);
expect(layout.length, 18);
});

test('saves preset layout to prefs', () async {
Expand Down
Loading