Add rejection payload inspection in sync view
This commit is contained in:
+17
-1
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
onToggleExpanded: matched?.payload == null
|
||||||
|
? null
|
||||||
|
: () => _toggleRejectionExpanded(
|
||||||
rejection.clientMutationId,
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user