Merge branch 'feat/issue-7-undo-log'

This commit is contained in:
Thomas SharedInbox
2026-05-10 14:45:11 +02:00
4 changed files with 52 additions and 19 deletions
+2
View File
@@ -91,3 +91,5 @@ snap/
*.log *.log
runner-data/ runner-data/
sharedinbox-runner/runner-data/ sharedinbox-runner/runner-data/
.gemini/
.rustup/
+13 -3
View File
@@ -21,11 +21,21 @@ class UndoService extends StateNotifier<List<UndoAction>> {
state = []; state = [];
} }
Future<void> undo() async { Future<void> undo({String? actionId}) async {
if (state.isEmpty) return; if (state.isEmpty) return;
final action = state.last; final UndoAction action;
state = state.sublist(0, state.length - 1); if (actionId == null) {
action = state.last;
state = state.sublist(0, state.length - 1);
} else {
try {
action = state.firstWhere((a) => a.id == actionId);
state = state.where((a) => a.id != actionId).toList();
} catch (e) {
return; // Action not found
}
}
final repo = _ref.read(emailRepositoryProvider); final repo = _ref.read(emailRepositoryProvider);
+7 -16
View File
@@ -56,22 +56,13 @@ class _UndoActionTile extends ConsumerWidget {
subtitle: Text(_timeFmt.format(action.timestamp.toLocal())), subtitle: Text(_timeFmt.format(action.timestamp.toLocal())),
trailing: TextButton( trailing: TextButton(
onPressed: () async { onPressed: () async {
// In a real log, "undoing" an old action might be complex await ref
// if subsequent actions conflict. For now, we only support .read(undoServiceProvider.notifier)
// undoing the LATEST action via the global UndoService. .undo(actionId: action.id);
// To keep it simple, we check if this is the latest action. if (context.mounted) {
final history = ref.read(undoServiceProvider); ScaffoldMessenger.of(context).showSnackBar(
final latest = history.isNotEmpty ? history.last : null; const SnackBar(content: Text('Action undone.')),
if (latest?.id == action.id) { );
await ref.read(undoServiceProvider.notifier).undo();
} else {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Only the latest action can be undone.'),
),
);
}
} }
}, },
child: const Text('Undo'), child: const Text('Undo'),
+30
View File
@@ -88,6 +88,36 @@ void main() {
verify(mockEmailRepo.moveEmail('e1', 'INBOX')).called(1); verify(mockEmailRepo.moveEmail('e1', 'INBOX')).called(1);
}); });
test('undo with actionId removes and undos specific action', () async {
final action1 = UndoAction(
id: '1',
accountId: 'acc1',
type: UndoType.move,
emailIds: ['e1'],
sourceMailboxPath: 'INBOX',
);
final action2 = UndoAction(
id: '2',
accountId: 'acc1',
type: UndoType.delete,
emailIds: ['e2'],
sourceMailboxPath: 'INBOX',
);
when(mockEmailRepo.moveEmail(any, any)).thenAnswer((_) async {});
when(mockEmailRepo.cancelPendingChange(any, any))
.thenAnswer((_) async => false);
final notifier = container.read(undoServiceProvider.notifier);
notifier.pushAction(action1);
notifier.pushAction(action2);
await notifier.undo(actionId: '1');
expect(container.read(undoServiceProvider), [action2]);
verify(mockEmailRepo.moveEmail('e1', 'INBOX')).called(1);
verifyNever(mockEmailRepo.moveEmail('e2', 'INBOX'));
});
test('undo performs cancelPendingChange optimization', () async { test('undo performs cancelPendingChange optimization', () async {
final action = UndoAction( final action = UndoAction(
id: '1', id: '1',