Add rejection payload inspection in sync view

This commit is contained in:
Rijad Zuzo
2026-02-15 23:34:55 +01:00
parent 04d269ae10
commit 960a891eba
2 changed files with 76 additions and 9 deletions
+17 -1
View File
@@ -2,6 +2,21 @@
Updated: 2026-02-15 Updated: 2026-02-15
## Latest Milestone (2026-02-15): Sync Rejection Payload Inspection
Improved conflict handling UX in Sync view:
- Updated `lib/features/sync/sync_view.dart`:
- each rejected mutation now supports payload inspect/expand
- shows formatted local envelope payload JSON for failed mutations when
available
- keeps requeue/dismiss actions for operator control
Validation for this milestone:
- `flutter analyze` -> pass
- `flutter test` -> pass
## Latest Milestone (2026-02-15): Reachability-Gated Auto Sync ## Latest Milestone (2026-02-15): Reachability-Gated Auto Sync
Added connectivity-aware gating to background sync auto-triggers: Added connectivity-aware gating to background sync auto-triggers:
@@ -157,7 +172,8 @@ Validation for this milestone:
- replace no-op scheduler with platform local notifications implementation - replace no-op scheduler with platform local notifications implementation
for Android/iOS/macOS/Windows/Linux/Web-safe fallback. for Android/iOS/macOS/Windows/Linux/Web-safe fallback.
2. Conflict resolution UX: 2. Conflict resolution UX:
- reject/requeue is implemented; still missing guided per-entity repair UI. - reject/requeue + payload inspect are implemented; future enhancement is
direct deep-link navigation to entity edit screens from Sync view.
3. Sync trigger maturity: 3. Sync trigger maturity:
- implemented via reachability gating; future improvement is explicit OS - implemented via reachability gating; future improvement is explicit OS
connectivity event subscription. connectivity event subscription.
+59 -8
View File
@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:relationship_saver/core/config/app_theme.dart'; import 'package:relationship_saver/core/config/app_theme.dart';
@@ -17,6 +19,7 @@ class SyncView extends ConsumerStatefulWidget {
class _SyncViewState extends ConsumerState<SyncView> { class _SyncViewState extends ConsumerState<SyncView> {
bool _busy = false; bool _busy = false;
String _status = 'Ready. Local-first mode keeps app usable without sync.'; String _status = 'Ready. Local-first mode keeps app usable without sync.';
final Set<String> _expandedRejections = <String>{};
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -185,18 +188,28 @@ class _SyncViewState extends ConsumerState<SyncView> {
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
...state.lastRejected.map( ...state.lastRejected.map((MutationRejection rejection) {
(MutationRejection rejection) => Padding( final ChangeEnvelope? matched = _findRejectedChange(
state.lastRejectedChanges,
rejection.clientMutationId,
);
final bool expanded = _expandedRejections.contains(
rejection.clientMutationId,
);
return Padding(
padding: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.only(bottom: 10),
child: _rejectionTile( child: _rejectionTile(
rejection, rejection,
matched: _findRejectedChange( matched: matched,
state.lastRejectedChanges, expanded: expanded,
rejection.clientMutationId, onToggleExpanded: matched?.payload == null
), ? null
: () => _toggleRejectionExpanded(
rejection.clientMutationId,
),
), ),
), );
), }),
], ],
), ),
), ),
@@ -258,6 +271,8 @@ class _SyncViewState extends ConsumerState<SyncView> {
Widget _rejectionTile( Widget _rejectionTile(
MutationRejection rejection, { MutationRejection rejection, {
required ChangeEnvelope? matched, required ChangeEnvelope? matched,
required bool expanded,
required VoidCallback? onToggleExpanded,
}) { }) {
return Container( return Container(
width: double.infinity, width: double.infinity,
@@ -300,6 +315,17 @@ class _SyncViewState extends ConsumerState<SyncView> {
color: AppTheme.textSecondary, color: AppTheme.textSecondary,
), ),
), ),
if (onToggleExpanded != null)
IconButton(
onPressed: onToggleExpanded,
tooltip: expanded ? 'Hide payload' : 'Show payload',
icon: Icon(
expanded
? Icons.keyboard_arrow_up_rounded
: Icons.keyboard_arrow_down_rounded,
color: AppTheme.textSecondary,
),
),
], ],
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
@@ -307,6 +333,21 @@ class _SyncViewState extends ConsumerState<SyncView> {
rejection.message, rejection.message,
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
), ),
if (expanded && matched?.payload != null) ...<Widget>[
const SizedBox(height: 10),
Container(
width: double.infinity,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: const Color(0xFFF3F8FB),
borderRadius: BorderRadius.circular(10),
),
child: SelectableText(
const JsonEncoder.withIndent(' ').convert(matched!.payload),
style: Theme.of(context).textTheme.bodySmall,
),
),
],
], ],
), ),
); );
@@ -435,4 +476,14 @@ class _SyncViewState extends ConsumerState<SyncView> {
} }
} }
} }
void _toggleRejectionExpanded(String clientMutationId) {
setState(() {
if (_expandedRejections.contains(clientMutationId)) {
_expandedRejections.remove(clientMutationId);
} else {
_expandedRejections.add(clientMutationId);
}
});
}
} }