Skip to content

Commit 380cdac

Browse files
committed
fix(ui): route paused-row retry through onRetryPaused in SyncErrorsScreen
M3 (round 2): add an optional onRetryPaused callback; the per-row Retry button now calls it for OutboxState.paused rows (wire to SyncController.retryPaused) and falls back to onRetry otherwise. Makes the paused-recovery path explicit at the UI layer. Backward compatible — onRetryPaused is optional.
1 parent 70cdc6d commit 380cdac

2 files changed

Lines changed: 84 additions & 3 deletions

File tree

lib/src/ui/screens/sync_errors_screen.dart

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,18 @@ import '../widgets/screen_helpers.dart';
1313
/// `retryAllRunning` disables per-row Retry buttons (as the engine
1414
/// drains the queue in priority order) and swaps the header button
1515
/// from `Retry all``Stop`.
16+
///
17+
/// For a [OutboxState.paused] row, the per-row Retry uses [onRetryPaused] when
18+
/// supplied (wire it to `SyncController.retryPaused`) so the paused-row
19+
/// recovery path is taken explicitly rather than routing through the generic
20+
/// [onRetry]. When [onRetryPaused] is null, paused rows fall back to [onRetry].
1621
class SyncErrorsScreen extends StatelessWidget {
1722
final List<OutboxRow> rows;
1823
final Future<void> Function(int outboxId) onRetry;
24+
25+
/// Recovery action for [OutboxState.paused] rows. Optional for backward
26+
/// compatibility; falls back to [onRetry] when not provided.
27+
final Future<void> Function(int outboxId)? onRetryPaused;
1928
final Future<void> Function() onRetryAll;
2029
final Future<void> Function() onStop;
2130
final void Function(OutboxRow) onOpen;
@@ -26,13 +35,22 @@ class SyncErrorsScreen extends StatelessWidget {
2635
super.key,
2736
required this.rows,
2837
required this.onRetry,
38+
this.onRetryPaused,
2939
required this.onRetryAll,
3040
required this.onStop,
3141
required this.onOpen,
3242
required this.onViewError,
3343
required this.retryAllRunning,
3444
});
3545

46+
/// Routes a row's per-row Retry to the state-appropriate action.
47+
Future<void> _retryRow(OutboxRow r) {
48+
if (r.state == OutboxState.paused && onRetryPaused != null) {
49+
return onRetryPaused!(r.id);
50+
}
51+
return onRetry(r.id);
52+
}
53+
3654
@override
3755
Widget build(BuildContext context) {
3856
final byDoctype = <String, List<OutboxRow>>{};
@@ -78,7 +96,7 @@ class SyncErrorsScreen extends StatelessWidget {
7896
? null
7997
: () => _runAndSurface(
8098
context,
81-
() => onRetry(r.id),
99+
() => _retryRow(r),
82100
'Retry',
83101
),
84102
child: const Text('Retry'),

test/ui/sync_errors_screen_test.dart

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@ import 'package:flutter_test/flutter_test.dart';
33
import 'package:frappe_mobile_sdk/src/models/outbox_row.dart';
44
import 'package:frappe_mobile_sdk/src/ui/screens/sync_errors_screen.dart';
55

6-
OutboxRow row(int id, String doctype, ErrorCode code) => OutboxRow(
6+
OutboxRow row(
7+
int id,
8+
String doctype,
9+
ErrorCode code, {
10+
OutboxState state = OutboxState.failed,
11+
}) => OutboxRow(
712
id: id,
813
doctype: doctype,
914
mobileUuid: '$id',
1015
operation: OutboxOperation.insert,
11-
state: OutboxState.failed,
16+
state: state,
1217
errorCode: code,
1318
retryCount: 1,
1419
errorMessage: 'msg',
@@ -59,6 +64,64 @@ void main() {
5964
expect(tapped, 1);
6065
});
6166

67+
testWidgets(
68+
'paused row routes Retry to onRetryPaused; failed row uses onRetry (M3)',
69+
(tester) async {
70+
int? retried;
71+
int? retriedPaused;
72+
await tester.pumpWidget(
73+
MaterialApp(
74+
home: SyncErrorsScreen(
75+
rows: [
76+
row(1, 'X', ErrorCode.VALIDATION, state: OutboxState.paused),
77+
row(2, 'X', ErrorCode.NETWORK),
78+
],
79+
onRetry: (id) async => retried = id,
80+
onRetryPaused: (id) async => retriedPaused = id,
81+
onRetryAll: () async {},
82+
onStop: () async {},
83+
onOpen: (_) {},
84+
onViewError: (_) {},
85+
retryAllRunning: false,
86+
),
87+
),
88+
);
89+
final retryButtons = find.widgetWithText(OutlinedButton, 'Retry');
90+
// Row order follows insertion: paused row 1 first, failed row 2 second.
91+
await tester.tap(retryButtons.at(0));
92+
await tester.pump();
93+
expect(retriedPaused, 1, reason: 'paused row must use onRetryPaused');
94+
expect(retried, isNull);
95+
96+
await tester.tap(retryButtons.at(1));
97+
await tester.pump();
98+
expect(retried, 2, reason: 'non-paused row must use onRetry');
99+
},
100+
);
101+
102+
testWidgets(
103+
'paused row falls back to onRetry when onRetryPaused is not provided (M3)',
104+
(tester) async {
105+
int? retried;
106+
await tester.pumpWidget(
107+
MaterialApp(
108+
home: SyncErrorsScreen(
109+
rows: [row(7, 'X', ErrorCode.VALIDATION, state: OutboxState.paused)],
110+
onRetry: (id) async => retried = id,
111+
onRetryAll: () async {},
112+
onStop: () async {},
113+
onOpen: (_) {},
114+
onViewError: (_) {},
115+
retryAllRunning: false,
116+
),
117+
),
118+
);
119+
await tester.tap(find.widgetWithText(OutlinedButton, 'Retry'));
120+
await tester.pump();
121+
expect(retried, 7);
122+
},
123+
);
124+
62125
testWidgets('per-row Retry disabled while retry-all is running', (
63126
tester,
64127
) async {

0 commit comments

Comments
 (0)