Add related-screen navigation for sync rejections
This commit is contained in:
+20
-2
@@ -2,6 +2,24 @@
|
||||
|
||||
Updated: 2026-02-15
|
||||
|
||||
## Latest Milestone (2026-02-15): Sync Rejection Repair Navigation
|
||||
|
||||
Added direct repair navigation from rejected sync items:
|
||||
|
||||
- Updated `lib/features/sync/sync_view.dart`:
|
||||
- each rejected item now includes `Open Related Screen`
|
||||
- routes user to a relevant maintenance screen based on entity type:
|
||||
- `person` -> People
|
||||
- `capture` -> Moments
|
||||
- `giftIdea`/`eventIdea` -> Ideas
|
||||
- `reminderRule` -> Reminders
|
||||
- keeps existing payload inspect + requeue/dismiss actions
|
||||
|
||||
Validation for this milestone:
|
||||
|
||||
- `flutter analyze` -> pass
|
||||
- `flutter test` -> pass
|
||||
|
||||
## Latest Milestone (2026-02-15): Notification Permission UX
|
||||
|
||||
Added runtime notification permission flow through the product UI:
|
||||
@@ -226,8 +244,8 @@ Validation for this milestone:
|
||||
- permission UX implemented in settings.
|
||||
- future enhancement: direct notification tap routing.
|
||||
2. Conflict resolution UX:
|
||||
- reject/requeue + payload inspect are implemented; future enhancement is
|
||||
direct deep-link navigation to entity edit screens from Sync view.
|
||||
- reject/requeue + payload inspect + related-screen navigation implemented.
|
||||
- future enhancement: deep-link directly to specific entity edit form.
|
||||
3. Sync trigger maturity:
|
||||
- implemented via reachability gating; future improvement is explicit OS
|
||||
connectivity event subscription.
|
||||
|
||||
@@ -3,6 +3,10 @@ import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:relationship_saver/core/config/app_theme.dart';
|
||||
import 'package:relationship_saver/features/ideas/ideas_view.dart';
|
||||
import 'package:relationship_saver/features/moments/moments_view.dart';
|
||||
import 'package:relationship_saver/features/people/people_view.dart';
|
||||
import 'package:relationship_saver/features/reminders/reminders_view.dart';
|
||||
import 'package:relationship_saver/features/shared/frosted_card.dart';
|
||||
import 'package:relationship_saver/features/sync/sync_coordinator.dart';
|
||||
import 'package:relationship_saver/features/sync/sync_queue_repository.dart';
|
||||
@@ -207,6 +211,11 @@ class _SyncViewState extends ConsumerState<SyncView> {
|
||||
: () => _toggleRejectionExpanded(
|
||||
rejection.clientMutationId,
|
||||
),
|
||||
onOpenEntity:
|
||||
matched != null &&
|
||||
_entityViewForType(matched.entityType) != null
|
||||
? () => _openEntityViewFor(matched)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}),
|
||||
@@ -273,6 +282,7 @@ class _SyncViewState extends ConsumerState<SyncView> {
|
||||
required ChangeEnvelope? matched,
|
||||
required bool expanded,
|
||||
required VoidCallback? onToggleExpanded,
|
||||
required VoidCallback? onOpenEntity,
|
||||
}) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
@@ -348,6 +358,19 @@ class _SyncViewState extends ConsumerState<SyncView> {
|
||||
),
|
||||
),
|
||||
],
|
||||
if (onOpenEntity != null) ...<Widget>[
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: <Widget>[
|
||||
TextButton.icon(
|
||||
onPressed: onOpenEntity,
|
||||
icon: const Icon(Icons.open_in_new_rounded),
|
||||
label: const Text('Open Related Screen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -486,4 +509,72 @@ class _SyncViewState extends ConsumerState<SyncView> {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _openEntityViewFor(ChangeEnvelope matched) async {
|
||||
final Widget? view = _entityViewForType(matched.entityType);
|
||||
if (view == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('No editor available for ${matched.entityType}.'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await Navigator.of(context).push<void>(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) => _EntityResolveScreen(
|
||||
title: _titleForEntityType(matched.entityType),
|
||||
child: view,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _entityViewForType(String entityType) {
|
||||
switch (entityType) {
|
||||
case 'person':
|
||||
return const PeopleView();
|
||||
case 'capture':
|
||||
return const MomentsView();
|
||||
case 'giftIdea':
|
||||
case 'eventIdea':
|
||||
return const IdeasView();
|
||||
case 'reminderRule':
|
||||
return const RemindersView();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String _titleForEntityType(String entityType) {
|
||||
switch (entityType) {
|
||||
case 'person':
|
||||
return 'Resolve Person';
|
||||
case 'capture':
|
||||
return 'Resolve Moment';
|
||||
case 'giftIdea':
|
||||
case 'eventIdea':
|
||||
return 'Resolve Idea';
|
||||
case 'reminderRule':
|
||||
return 'Resolve Reminder';
|
||||
default:
|
||||
return 'Resolve Item';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _EntityResolveScreen extends StatelessWidget {
|
||||
const _EntityResolveScreen({required this.title, required this.child});
|
||||
|
||||
final String title;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(title)),
|
||||
body: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user