-
Notifications
You must be signed in to change notification settings - Fork 177
feat: Unsaved changes indicator with review dialog on Edit Task screen #524
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
46fe9ac
6076f62
facf7d2
106bf08
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -119,122 +119,128 @@ class DetailRouteView extends GetView<DetailRouteController> { | |||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| child: Scaffold( | ||||||||||||||||||||||||||||||||||
| backgroundColor: tColors.primaryBackgroundColor, | ||||||||||||||||||||||||||||||||||
| appBar: AppBar( | ||||||||||||||||||||||||||||||||||
| leading: BackButton(color: TaskWarriorColors.white), | ||||||||||||||||||||||||||||||||||
| backgroundColor: Palette.kToDark, | ||||||||||||||||||||||||||||||||||
| title: Text( | ||||||||||||||||||||||||||||||||||
| '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageID}: ${(controller.modify.id == 0) ? '-' : controller.modify.id}', | ||||||||||||||||||||||||||||||||||
| style: TextStyle( | ||||||||||||||||||||||||||||||||||
| color: TaskWarriorColors.white, | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| )), | ||||||||||||||||||||||||||||||||||
| body: Padding( | ||||||||||||||||||||||||||||||||||
| padding: const EdgeInsets.only(left: 8.0, right: 8.0), | ||||||||||||||||||||||||||||||||||
| child: Obx( | ||||||||||||||||||||||||||||||||||
| () => ListView( | ||||||||||||||||||||||||||||||||||
| padding: | ||||||||||||||||||||||||||||||||||
| const EdgeInsets.symmetric(vertical: 4, horizontal: 2), | ||||||||||||||||||||||||||||||||||
| children: [ | ||||||||||||||||||||||||||||||||||
| for (var entry in { | ||||||||||||||||||||||||||||||||||
| 'description': controller.descriptionValue.value, | ||||||||||||||||||||||||||||||||||
| 'status': controller.statusValue.value, | ||||||||||||||||||||||||||||||||||
| 'entry': controller.entryValue.value, | ||||||||||||||||||||||||||||||||||
| 'modified': controller.modifiedValue.value, | ||||||||||||||||||||||||||||||||||
| 'start': controller.startValue.value, | ||||||||||||||||||||||||||||||||||
| 'end': controller.endValue.value, | ||||||||||||||||||||||||||||||||||
| 'due': controller.dueValue.value, | ||||||||||||||||||||||||||||||||||
| 'wait': controller.waitValue.value, | ||||||||||||||||||||||||||||||||||
| 'until': controller.untilValue.value, | ||||||||||||||||||||||||||||||||||
| 'priority': controller.priorityValue?.value, | ||||||||||||||||||||||||||||||||||
| 'project': controller.projectValue?.value, | ||||||||||||||||||||||||||||||||||
| 'tags': controller.tagsValue?.value, | ||||||||||||||||||||||||||||||||||
| 'urgency': controller.urgencyValue.value, | ||||||||||||||||||||||||||||||||||
| }.entries) | ||||||||||||||||||||||||||||||||||
| AttributeWidget( | ||||||||||||||||||||||||||||||||||
| name: entry.key, | ||||||||||||||||||||||||||||||||||
| value: entry.value, | ||||||||||||||||||||||||||||||||||
| callback: (newValue) => | ||||||||||||||||||||||||||||||||||
| controller.setAttribute(entry.key, newValue), | ||||||||||||||||||||||||||||||||||
| waitKey: controller.waitKey, | ||||||||||||||||||||||||||||||||||
| dueKey: controller.dueKey, | ||||||||||||||||||||||||||||||||||
| untilKey: controller.untilKey, | ||||||||||||||||||||||||||||||||||
| priorityKey: controller.priorityKey, | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| )), | ||||||||||||||||||||||||||||||||||
| floatingActionButton: controller.modify.changes.isEmpty | ||||||||||||||||||||||||||||||||||
| ? const SizedBox.shrink() | ||||||||||||||||||||||||||||||||||
| : FloatingActionButton( | ||||||||||||||||||||||||||||||||||
| backgroundColor: tColors.primaryTextColor, | ||||||||||||||||||||||||||||||||||
| foregroundColor: tColors.secondaryBackgroundColor, | ||||||||||||||||||||||||||||||||||
| splashColor: tColors.primaryTextColor, | ||||||||||||||||||||||||||||||||||
| heroTag: "btn1", | ||||||||||||||||||||||||||||||||||
| onPressed: () { | ||||||||||||||||||||||||||||||||||
| showDialog( | ||||||||||||||||||||||||||||||||||
| context: context, | ||||||||||||||||||||||||||||||||||
| builder: (context) { | ||||||||||||||||||||||||||||||||||
| return AlertDialog( | ||||||||||||||||||||||||||||||||||
| scrollable: true, | ||||||||||||||||||||||||||||||||||
| title: Text( | ||||||||||||||||||||||||||||||||||
| '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.reviewChanges}:', | ||||||||||||||||||||||||||||||||||
| style: TextStyle( | ||||||||||||||||||||||||||||||||||
| color: tColors.primaryTextColor, | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| content: SingleChildScrollView( | ||||||||||||||||||||||||||||||||||
| scrollDirection: Axis.horizontal, | ||||||||||||||||||||||||||||||||||
| child: Text( | ||||||||||||||||||||||||||||||||||
| controller.modify.changes.entries | ||||||||||||||||||||||||||||||||||
| .map((entry) => '${entry.key}:\n' | ||||||||||||||||||||||||||||||||||
| ' ${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.oldChanges}: ${entry.value['old']}\n' | ||||||||||||||||||||||||||||||||||
| ' ${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.newChanges}: ${entry.value['new']}') | ||||||||||||||||||||||||||||||||||
| .toList() | ||||||||||||||||||||||||||||||||||
| .join('\n'), | ||||||||||||||||||||||||||||||||||
| style: TextStyle( | ||||||||||||||||||||||||||||||||||
| color: tColors.primaryTextColor, | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| actions: [ | ||||||||||||||||||||||||||||||||||
| TextButton( | ||||||||||||||||||||||||||||||||||
| onPressed: () { | ||||||||||||||||||||||||||||||||||
| Get.back(); | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| child: Text( | ||||||||||||||||||||||||||||||||||
| SentenceManager( | ||||||||||||||||||||||||||||||||||
| currentLanguage: | ||||||||||||||||||||||||||||||||||
| AppSettings.selectedLanguage) | ||||||||||||||||||||||||||||||||||
| .sentences | ||||||||||||||||||||||||||||||||||
| .cancel, | ||||||||||||||||||||||||||||||||||
| style: TextStyle( | ||||||||||||||||||||||||||||||||||
| color: tColors.primaryTextColor, | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| TextButton( | ||||||||||||||||||||||||||||||||||
| onPressed: () { | ||||||||||||||||||||||||||||||||||
| controller.saveChanges(); | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| child: Text( | ||||||||||||||||||||||||||||||||||
| SentenceManager( | ||||||||||||||||||||||||||||||||||
| currentLanguage: | ||||||||||||||||||||||||||||||||||
| AppSettings.selectedLanguage) | ||||||||||||||||||||||||||||||||||
| .sentences | ||||||||||||||||||||||||||||||||||
| .submit, | ||||||||||||||||||||||||||||||||||
| style: TextStyle( | ||||||||||||||||||||||||||||||||||
| color: tColors.primaryBackgroundColor, | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| child: const Icon(Icons.save), | ||||||||||||||||||||||||||||||||||
| )), | ||||||||||||||||||||||||||||||||||
| backgroundColor: tColors.primaryBackgroundColor, | ||||||||||||||||||||||||||||||||||
| appBar: AppBar( | ||||||||||||||||||||||||||||||||||
| leading: BackButton(color: TaskWarriorColors.white), | ||||||||||||||||||||||||||||||||||
| backgroundColor: Palette.kToDark, | ||||||||||||||||||||||||||||||||||
| title: Text( | ||||||||||||||||||||||||||||||||||
| '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageID}: ${(controller.modify.id == 0) ? '-' : controller.modify.id}', | ||||||||||||||||||||||||||||||||||
| style: TextStyle( | ||||||||||||||||||||||||||||||||||
| color: TaskWarriorColors.white, | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| )), | ||||||||||||||||||||||||||||||||||
| body: Padding( | ||||||||||||||||||||||||||||||||||
| padding: const EdgeInsets.only(left: 8.0, right: 8.0), | ||||||||||||||||||||||||||||||||||
| child: Obx( | ||||||||||||||||||||||||||||||||||
| () => ListView( | ||||||||||||||||||||||||||||||||||
| padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 2), | ||||||||||||||||||||||||||||||||||
| children: [ | ||||||||||||||||||||||||||||||||||
| for (var entry in { | ||||||||||||||||||||||||||||||||||
| 'description': controller.descriptionValue.value, | ||||||||||||||||||||||||||||||||||
| 'status': controller.statusValue.value, | ||||||||||||||||||||||||||||||||||
| 'entry': controller.entryValue.value, | ||||||||||||||||||||||||||||||||||
| 'modified': controller.modifiedValue.value, | ||||||||||||||||||||||||||||||||||
| 'start': controller.startValue.value, | ||||||||||||||||||||||||||||||||||
| 'end': controller.endValue.value, | ||||||||||||||||||||||||||||||||||
| 'due': controller.dueValue.value, | ||||||||||||||||||||||||||||||||||
| 'wait': controller.waitValue.value, | ||||||||||||||||||||||||||||||||||
| 'until': controller.untilValue.value, | ||||||||||||||||||||||||||||||||||
| 'priority': controller.priorityValue?.value, | ||||||||||||||||||||||||||||||||||
| 'project': controller.projectValue?.value, | ||||||||||||||||||||||||||||||||||
| 'tags': controller.tagsValue?.value, | ||||||||||||||||||||||||||||||||||
| 'urgency': controller.urgencyValue.value, | ||||||||||||||||||||||||||||||||||
| }.entries) | ||||||||||||||||||||||||||||||||||
| AttributeWidget( | ||||||||||||||||||||||||||||||||||
| name: entry.key, | ||||||||||||||||||||||||||||||||||
| value: entry.value, | ||||||||||||||||||||||||||||||||||
| callback: (newValue) => | ||||||||||||||||||||||||||||||||||
| controller.setAttribute(entry.key, newValue), | ||||||||||||||||||||||||||||||||||
| waitKey: controller.waitKey, | ||||||||||||||||||||||||||||||||||
| dueKey: controller.dueKey, | ||||||||||||||||||||||||||||||||||
| untilKey: controller.untilKey, | ||||||||||||||||||||||||||||||||||
| priorityKey: controller.priorityKey, | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| )), | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // SAVE BUTTON — Bottom Right | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| floatingActionButton: Obx(() { | ||||||||||||||||||||||||||||||||||
| if (!controller.onEdit.value) { | ||||||||||||||||||||||||||||||||||
| return const SizedBox.shrink(); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return FloatingActionButton( | ||||||||||||||||||||||||||||||||||
| onPressed: () => _showReviewChangesDialog(context, tColors), | ||||||||||||||||||||||||||||||||||
| backgroundColor: tColors.primaryTextColor, | ||||||||||||||||||||||||||||||||||
| foregroundColor: tColors.secondaryBackgroundColor, | ||||||||||||||||||||||||||||||||||
| splashColor: tColors.primaryTextColor, | ||||||||||||||||||||||||||||||||||
| child: const Icon(Icons.save), | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||
| floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // REVIEW CHANGES DIALOG | ||||||||||||||||||||||||||||||||||
| void _showReviewChangesDialog( | ||||||||||||||||||||||||||||||||||
| BuildContext context, TaskwarriorColorTheme tColors) { | ||||||||||||||||||||||||||||||||||
| final sentences = | ||||||||||||||||||||||||||||||||||
| SentenceManager(currentLanguage: AppSettings.selectedLanguage) | ||||||||||||||||||||||||||||||||||
| .sentences; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| showDialog( | ||||||||||||||||||||||||||||||||||
| context: context, | ||||||||||||||||||||||||||||||||||
| builder: (context) { | ||||||||||||||||||||||||||||||||||
| return AlertDialog( | ||||||||||||||||||||||||||||||||||
| scrollable: true, | ||||||||||||||||||||||||||||||||||
| title: Text( | ||||||||||||||||||||||||||||||||||
| '${sentences.reviewChanges}:', | ||||||||||||||||||||||||||||||||||
| style: TextStyle(color: tColors.primaryTextColor), | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| content: SingleChildScrollView( | ||||||||||||||||||||||||||||||||||
| scrollDirection: Axis.horizontal, | ||||||||||||||||||||||||||||||||||
| child: Text( | ||||||||||||||||||||||||||||||||||
| controller.modify.changes.entries | ||||||||||||||||||||||||||||||||||
| .map((entry) => '${entry.key}:\n' | ||||||||||||||||||||||||||||||||||
| ' ${sentences.oldChanges}: ${entry.value['old']}\n' | ||||||||||||||||||||||||||||||||||
| ' ${sentences.newChanges}: ${entry.value['new']}') | ||||||||||||||||||||||||||||||||||
| .toList() | ||||||||||||||||||||||||||||||||||
| .join('\n'), | ||||||||||||||||||||||||||||||||||
| style: TextStyle(color: tColors.primaryTextColor), | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| actions: [ | ||||||||||||||||||||||||||||||||||
| TextButton( | ||||||||||||||||||||||||||||||||||
| onPressed: () => Get.back(), | ||||||||||||||||||||||||||||||||||
| child: Text( | ||||||||||||||||||||||||||||||||||
| sentences.cancel, | ||||||||||||||||||||||||||||||||||
| style: TextStyle(color: tColors.primaryTextColor), | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| TextButton( | ||||||||||||||||||||||||||||||||||
| onPressed: () { | ||||||||||||||||||||||||||||||||||
| Get.back(); | ||||||||||||||||||||||||||||||||||
| controller.saveChanges(); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| ScaffoldMessenger.of(context).showSnackBar( | ||||||||||||||||||||||||||||||||||
| SnackBar( | ||||||||||||||||||||||||||||||||||
| content: Text(sentences.taskUpdated), | ||||||||||||||||||||||||||||||||||
| behavior: SnackBarBehavior.floating, | ||||||||||||||||||||||||||||||||||
| duration: const Duration(seconds: 2), | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+224
to
+234
|
||||||||||||||||||||||||||||||||||
| ScaffoldMessenger.of(context).showSnackBar( | |
| SnackBar( | |
| content: Text(sentences.taskUpdated), | |
| behavior: SnackBarBehavior.floating, | |
| duration: const Duration(seconds: 2), | |
| ), | |
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove duplicate success notification from the dialog submit path.
controller.saveChanges() already handles success feedback and navigation. On Line 228, showing another ScaffoldMessenger snackbar after popping can duplicate messages and rely on a dialog context that may no longer be valid.
Proposed fix
TextButton(
onPressed: () {
Get.back();
controller.saveChanges();
-
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text(sentences.taskUpdated),
- behavior: SnackBarBehavior.floating,
- duration: const Duration(seconds: 2),
- ),
- );
},
child: Text(
sentences.submit,
style: const TextStyle(color: Colors.white),
),
),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| onPressed: () { | |
| Get.back(); | |
| controller.saveChanges(); | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| SnackBar( | |
| content: Text(sentences.taskUpdated), | |
| behavior: SnackBarBehavior.floating, | |
| duration: const Duration(seconds: 2), | |
| ), | |
| ); | |
| }, | |
| onPressed: () { | |
| Get.back(); | |
| controller.saveChanges(); | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/app/modules/detailRoute/views/detail_route_view.dart` around lines 224 -
235, The dialog handler is showing a duplicate snackbar and performing
navigation that controller.saveChanges() already handles; remove the explicit
Get.back() call and the ScaffoldMessenger.of(context).showSnackBar(...) block
(including the SnackBar creation and sentences.taskUpdated usage) and instead
just invoke controller.saveChanges() so feedback and navigation come solely from
controller.saveChanges().
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,11 +21,16 @@ class PriorityWidget extends StatelessWidget { | |
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| TaskwarriorColorTheme tColors = Theme.of(context).extension<TaskwarriorColorTheme>()!; | ||
| TaskwarriorColorTheme tColors = | ||
| Theme.of(context).extension<TaskwarriorColorTheme>()!; | ||
| final Color? textColor = isEditable | ||
| ? tColors.primaryTextColor | ||
| : tColors.primaryDisabledTextColor; | ||
|
|
||
| // Normalize value: null → X | ||
| final String priority = | ||
| (value == null || value == '') ? 'X' : value.toString(); | ||
|
|
||
|
Comment on lines
+31
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle non-canonical priority values to avoid silent no-op taps. At Line 31-32 and Line 70-83, unexpected values (e.g., lowercase or spaced strings) won’t match any case, so tapping does nothing even when editable. Canonicalize before switching or add a Proposed fix- final String priority =
- (value == null || value == '') ? 'X' : value.toString();
+ final String priority = (() {
+ final raw = value?.toString().trim().toUpperCase();
+ return (raw == null || raw.isEmpty) ? 'X' : raw;
+ })();
...
switch (priority) {
case 'X':
callback('H');
break;
case 'H':
callback('M');
break;
case 'M':
callback('L');
break;
case 'L':
callback(null);
break;
+ default:
+ callback('H');
+ break;
}Also applies to: 70-83 🤖 Prompt for AI Agents |
||
| return Card( | ||
| key: globalKey, | ||
| color: tColors.secondaryBackgroundColor, | ||
|
|
@@ -48,7 +53,7 @@ class PriorityWidget extends StatelessWidget { | |
| ), | ||
| ), | ||
| TextSpan( | ||
| text: value ?? "not selected", | ||
| text: priority, // Always show X / H / M / L | ||
| style: GoogleFonts.poppins( | ||
| fontSize: TaskWarriorFonts.fontSizeMedium, | ||
| color: textColor, | ||
|
|
@@ -60,18 +65,24 @@ class PriorityWidget extends StatelessWidget { | |
| ], | ||
| ), | ||
| ), | ||
| onTap: () { | ||
| switch (value) { | ||
| case 'H': | ||
| return callback('M'); | ||
| case 'M': | ||
| return callback('L'); | ||
| case 'L': | ||
| return callback(null); | ||
| default: | ||
| return callback('H'); | ||
| } | ||
| }, | ||
| onTap: isEditable | ||
| ? () { | ||
| switch (priority) { | ||
| case 'X': | ||
| callback('H'); | ||
| break; | ||
| case 'H': | ||
| callback('M'); | ||
| break; | ||
| case 'M': | ||
| callback('L'); | ||
| break; | ||
| case 'L': | ||
| callback(null); | ||
| break; | ||
| } | ||
| } | ||
| : null, | ||
| ), | ||
| ); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment should be properly formatted with consistent spacing. Comments should start with a space after the double-slash to follow Dart style conventions.