feat(account-menu): move force full sync button from edit screen to account menu (#99)

Add "Force full sync" popup menu item below "Verify sync health" in the
per-account menu on the account list screen, with a confirmation dialog.
Remove the button and handler from the edit account screen.
This commit is contained in:
Thomas SharedInbox
2026-05-15 21:29:43 +02:00
parent a38691a760
commit 1fd37cc966
4 changed files with 83 additions and 48 deletions
+33
View File
@@ -154,6 +154,10 @@ class _AccountTile extends ConsumerWidget {
value: _AccountAction.verifySync,
child: Text('Verify sync health'),
),
const PopupMenuItem(
value: _AccountAction.forceSync,
child: Text('Force full sync'),
),
const PopupMenuItem(
value: _AccountAction.edit,
child: Text('Edit'),
@@ -204,6 +208,34 @@ class _AccountTile extends ConsumerWidget {
);
}
break;
case _AccountAction.forceSync:
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Force full sync?'),
content: const Text(
'This clears all locally-cached emails and mailboxes for this '
'account and immediately re-downloads everything from the server. '
'Previously viewed email content will not need to be re-downloaded.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('Force sync'),
),
],
),
);
if (confirmed == true && context.mounted) {
await ProviderScope.containerOf(
context,
).read(syncManagerProvider).forceResync(account.id);
}
break;
case _AccountAction.edit:
await context.push('/accounts/${account.id}/edit');
break;
@@ -354,6 +386,7 @@ class _Step extends StatelessWidget {
enum _AccountAction {
syncLog,
verifySync,
forceSync,
edit,
emailFiltersRemote,
emailFiltersLocal,
+1 -48
View File
@@ -43,7 +43,6 @@ class _EditAccountScreenState extends ConsumerState<EditAccountScreen> {
bool _tryTesting = false;
String? _tryOk;
String? _tryErr;
bool _resyncing = false;
@override
void initState() {
@@ -171,43 +170,6 @@ class _EditAccountScreenState extends ConsumerState<EditAccountScreen> {
}
}
Future<void> _forceResync() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Force full sync?'),
content: const Text(
'This clears all locally-cached emails and mailboxes for this '
'account and immediately re-downloads everything from the server. '
'Previously viewed email content will not need to be re-downloaded.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('Force sync'),
),
],
),
);
if (confirmed != true || !mounted) return;
setState(() => _resyncing = true);
try {
await ref.read(syncManagerProvider).forceResync(widget.accountId);
if (mounted) context.pop();
} catch (e) {
if (mounted) {
setState(() {
_resyncing = false;
_errorMessage = 'Force sync failed: $e';
});
}
}
}
Future<void> _save() async {
if (!_formKey.currentState!.validate()) return;
final password = _passwordCtrl.text.isNotEmpty ? _passwordCtrl.text : null;
@@ -268,7 +230,7 @@ class _EditAccountScreenState extends ConsumerState<EditAccountScreen> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Edit account')),
body: _loading || _saving || _resyncing
body: _loading || _saving
? const Center(child: CircularProgressIndicator())
: _buildForm(),
);
@@ -387,15 +349,6 @@ class _EditAccountScreenState extends ConsumerState<EditAccountScreen> {
),
const SizedBox(height: 8),
FilledButton(onPressed: _save, child: const Text('Save')),
const SizedBox(height: 8),
OutlinedButton.icon(
icon: const Icon(Icons.sync_problem),
label: const Text('Force full sync'),
style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
),
onPressed: _forceResync,
),
],
),
),
+37
View File
@@ -153,6 +153,43 @@ void main() {
expect(find.text('Export account'), findsOneWidget);
});
testWidgets('account popup menu contains Force full sync item', (
tester,
) async {
await tester.pumpWidget(
buildApp(
initialLocation: '/accounts',
overrides: baseOverrides(accounts: [kTestAccount]),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.more_vert));
await tester.pumpAndSettle();
expect(find.text('Force full sync'), findsOneWidget);
});
testWidgets(
'Force full sync appears below Verify sync health in popup menu',
(tester) async {
await tester.pumpWidget(
buildApp(
initialLocation: '/accounts',
overrides: baseOverrides(accounts: [kTestAccount]),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.more_vert));
await tester.pumpAndSettle();
final verifyPos = tester.getTopLeft(find.text('Verify sync health')).dy;
final forcePos = tester.getTopLeft(find.text('Force full sync')).dy;
expect(forcePos, greaterThan(verifyPos));
},
);
testWidgets('AppBar does not overflow at minimum supported window size', (
tester,
) async {
+12
View File
@@ -45,6 +45,18 @@ void main() {
expect(find.text('Save'), findsOneWidget);
});
testWidgets('does not show Force full sync button', (tester) async {
await tester.pumpWidget(
buildApp(
initialLocation: '/accounts/acc-1/edit',
overrides: baseOverrides(accounts: [kTestAccount]),
),
);
await tester.pumpAndSettle();
expect(find.text('Force full sync'), findsNothing);
});
testWidgets('saving without password change pops back', (tester) async {
tester.view.physicalSize = const Size(800, 1400);
tester.view.devicePixelRatio = 1.0;