Parcours : confirmation avant d'abandonner du travail non enregistré
Les trois dialogues imbriqués (Parcours, Étape, Question) fermaient sur
« Annuler » par un Navigator.pop sans rien demander, chacun jetant tout ce
qui était saisi sous lui — le plus coûteux étant une question de quiz avec
ses réponses.
Confirmation ajoutée aux trois, plus interception du retour arrière du
navigateur par PopScope : le clic hors fenêtre et la touche Échap étaient
déjà neutralisés par barrierDismissible: false, le retour navigateur était
le seul chemin de perte encore ouvert.
Deux points non évidents :
- l'instantané de référence est pris après la première frame, parce que
ensureSimpleResponse écrit dans la question pendant la construction et
ferait passer un dialog intact pour modifié ;
- les deux dialogues partageant le même Navigator, popper depuis onYes
fermerait la confirmation et non l'éditeur — la fermeture est différée
d'une frame pour être déterministe.
Réutilise showConfirmationDialog plutôt que d'ajouter un second composant.
3 clés i18n FR/EN/NL. flutter build web ✅, analyse du dossier propre.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
9d9f4bc395
commit
ac4927bdef
@ -5,6 +5,7 @@ import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:manager_app/constants.dart';
|
||||
import 'package:manager_app/l10n/app_localizations.dart';
|
||||
import 'package:manager_app/Components/confirmation_dialog.dart';
|
||||
import 'package:manager_app/Components/rounded_button.dart';
|
||||
import 'package:manager_app/Components/multi_string_input_container.dart';
|
||||
import 'package:manager_app/Components/check_input_container.dart';
|
||||
@ -41,12 +42,42 @@ void showNewOrUpdateGuidedPath(
|
||||
bool isSaving = false;
|
||||
int stepsRevision = 0;
|
||||
|
||||
// Instantané de référence pour détecter les modifications non enregistrées.
|
||||
// Pris après la première frame : le corps du dialog normalise certains champs
|
||||
// pendant sa construction, et ces écritures ne sont pas des saisies.
|
||||
String? baseline;
|
||||
bool hasUnsavedChanges() =>
|
||||
baseline != null && jsonEncode(workingPath) != baseline;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
if (baseline == null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => baseline = jsonEncode(workingPath));
|
||||
}
|
||||
|
||||
void closeWithConfirmation() {
|
||||
if (!hasUnsavedChanges()) {
|
||||
Navigator.pop(context);
|
||||
return;
|
||||
}
|
||||
showConfirmationDialog(
|
||||
AppLocalizations.of(context)!.discardPathChangesConfirm,
|
||||
() {},
|
||||
// Les deux dialogs partagent le même Navigator : popper ici
|
||||
// fermerait la confirmation, pas l'éditeur. On attend donc que
|
||||
// showConfirmationDialog ait retiré sa propre route.
|
||||
() => WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
}),
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
final double screenWidth = MediaQuery.of(context).size.width;
|
||||
final double screenHeight = MediaQuery.of(context).size.height;
|
||||
final double dialogWidth = screenWidth * 0.82;
|
||||
@ -54,7 +85,15 @@ void showNewOrUpdateGuidedPath(
|
||||
final double contentWidth = dialogWidth - 48;
|
||||
final double halfWidth = (contentWidth - 20) / 2;
|
||||
|
||||
return Dialog(
|
||||
return PopScope(
|
||||
// `barrierDismissible: false` neutralise déjà le clic hors dialog et
|
||||
// la touche Échap, mais pas le retour arrière du navigateur — sur une
|
||||
// app web c'est le vrai chemin par lequel la saisie disparaît.
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (!didPop) closeWithConfirmation();
|
||||
},
|
||||
child: Dialog(
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Container(
|
||||
@ -436,7 +475,7 @@ void showNewOrUpdateGuidedPath(
|
||||
height: 46,
|
||||
child: RoundedButton(
|
||||
text: "Annuler",
|
||||
press: () => Navigator.pop(context),
|
||||
press: closeWithConfirmation,
|
||||
color: kSecond,
|
||||
fontSize: 15,
|
||||
horizontal: 24,
|
||||
@ -476,6 +515,7 @@ void showNewOrUpdateGuidedPath(
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@ -44,6 +44,11 @@ void showNewOrUpdateGuidedStep(
|
||||
bool isSaving = false;
|
||||
int questionsRevision = 0;
|
||||
|
||||
// Voir showNewOrUpdateGuidedPath : instantané pris après la première frame.
|
||||
String? baseline;
|
||||
bool hasUnsavedChanges() =>
|
||||
baseline != null && jsonEncode(workingStep) != baseline;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
@ -57,7 +62,34 @@ void showNewOrUpdateGuidedStep(
|
||||
final double halfWidth = (contentWidth - 20) / 2;
|
||||
final appCtx = Provider.of<AppContext>(context, listen: false);
|
||||
|
||||
return Dialog(
|
||||
if (baseline == null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => baseline = jsonEncode(workingStep));
|
||||
}
|
||||
|
||||
void closeWithConfirmation() {
|
||||
if (!hasUnsavedChanges()) {
|
||||
Navigator.pop(context);
|
||||
return;
|
||||
}
|
||||
showConfirmationDialog(
|
||||
AppLocalizations.of(context)!.discardStepChangesConfirm,
|
||||
() {},
|
||||
// Voir showNewOrUpdateGuidedPath : on laisse la confirmation
|
||||
// retirer sa route avant de fermer l'éditeur.
|
||||
() => WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
}),
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (!didPop) closeWithConfirmation();
|
||||
},
|
||||
child: Dialog(
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Container(
|
||||
@ -464,7 +496,7 @@ void showNewOrUpdateGuidedStep(
|
||||
height: 46,
|
||||
child: RoundedButton(
|
||||
text: "Annuler",
|
||||
press: () => Navigator.pop(context),
|
||||
press: closeWithConfirmation,
|
||||
color: kSecond,
|
||||
fontSize: 15,
|
||||
horizontal: 24,
|
||||
@ -502,6 +534,7 @@ void showNewOrUpdateGuidedStep(
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:manager_app/constants.dart';
|
||||
import 'package:manager_app/l10n/app_localizations.dart';
|
||||
import 'package:manager_app/Components/confirmation_dialog.dart';
|
||||
import 'package:manager_app/Components/rounded_button.dart';
|
||||
import 'package:manager_app/Components/multi_string_input_container.dart';
|
||||
import 'package:manager_app/Components/resource_input_container.dart';
|
||||
@ -58,8 +59,16 @@ void showNewOrUpdateQuizQuestion(
|
||||
guidedStepId: question?.guidedStepId ?? stepId,
|
||||
);
|
||||
|
||||
// Voir showNewOrUpdateGuidedPath. L'instantané est d'autant plus nécessaire ici
|
||||
// que `ensureSimpleResponse` écrit dans workingQuestion pendant la construction :
|
||||
// le prendre avant la première frame déclarerait « modifié » un dialog intact.
|
||||
String? baseline;
|
||||
bool hasUnsavedChanges() =>
|
||||
baseline != null && jsonEncode(workingQuestion) != baseline;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
@ -69,6 +78,28 @@ void showNewOrUpdateQuizQuestion(
|
||||
final double contentWidth = dialogWidth - 48;
|
||||
final double halfWidth = (contentWidth - 20) / 2;
|
||||
|
||||
if (baseline == null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => baseline = jsonEncode(workingQuestion));
|
||||
}
|
||||
|
||||
void closeWithConfirmation() {
|
||||
if (!hasUnsavedChanges()) {
|
||||
Navigator.pop(context);
|
||||
return;
|
||||
}
|
||||
showConfirmationDialog(
|
||||
AppLocalizations.of(context)!.discardQuestionChangesConfirm,
|
||||
() {},
|
||||
// Voir showNewOrUpdateGuidedPath : on laisse la confirmation
|
||||
// retirer sa route avant de fermer l'éditeur.
|
||||
() => WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
}),
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
void ensureSimpleResponse() {
|
||||
if (workingQuestion.responses.isEmpty) {
|
||||
workingQuestion.responses
|
||||
@ -77,7 +108,12 @@ void showNewOrUpdateQuizQuestion(
|
||||
workingQuestion.responses[0].isGood = true;
|
||||
}
|
||||
|
||||
return Dialog(
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (!didPop) closeWithConfirmation();
|
||||
},
|
||||
child: Dialog(
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Container(
|
||||
@ -378,7 +414,7 @@ void showNewOrUpdateQuizQuestion(
|
||||
height: 46,
|
||||
child: RoundedButton(
|
||||
text: "Annuler",
|
||||
press: () => Navigator.pop(context),
|
||||
press: closeWithConfirmation,
|
||||
color: kSecond,
|
||||
fontSize: 15,
|
||||
horizontal: 24,
|
||||
@ -408,6 +444,7 @@ void showNewOrUpdateQuizQuestion(
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@ -681,5 +681,8 @@
|
||||
"resourceDeleteConfirm": "Are you sure you want to delete this resource?",
|
||||
"categoriesTitle": "Categories",
|
||||
"endTimeLabel": "End time",
|
||||
"annotationsLabel": "Annotations"
|
||||
"annotationsLabel": "Annotations",
|
||||
"discardPathChangesConfirm": "This path has unsaved changes, along with its steps and their questions. Discard these changes?",
|
||||
"discardStepChangesConfirm": "This step has unsaved changes, along with its questions. Discard these changes?",
|
||||
"discardQuestionChangesConfirm": "This question has unsaved changes, along with its answers. Discard these changes?"
|
||||
}
|
||||
|
||||
@ -681,5 +681,8 @@
|
||||
"resourceDeleteConfirm": "Êtes-vous sûr de vouloir supprimer cette ressource ?",
|
||||
"categoriesTitle": "Catégories",
|
||||
"endTimeLabel": "Heure de fin",
|
||||
"annotationsLabel": "Annotations"
|
||||
"annotationsLabel": "Annotations",
|
||||
"discardPathChangesConfirm": "Ce parcours contient des modifications non enregistrées, ainsi que ses étapes et leurs questions. Abandonner ces modifications ?",
|
||||
"discardStepChangesConfirm": "Cette étape contient des modifications non enregistrées, ainsi que ses questions. Abandonner ces modifications ?",
|
||||
"discardQuestionChangesConfirm": "Cette question contient des modifications non enregistrées, ainsi que ses réponses. Abandonner ces modifications ?"
|
||||
}
|
||||
|
||||
@ -3297,6 +3297,24 @@ abstract class AppLocalizations {
|
||||
/// In fr, this message translates to:
|
||||
/// **'Annotations'**
|
||||
String get annotationsLabel;
|
||||
|
||||
/// No description provided for @discardPathChangesConfirm.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
/// **'Ce parcours contient des modifications non enregistrées, ainsi que ses étapes et leurs questions. Abandonner ces modifications ?'**
|
||||
String get discardPathChangesConfirm;
|
||||
|
||||
/// No description provided for @discardStepChangesConfirm.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
/// **'Cette étape contient des modifications non enregistrées, ainsi que ses questions. Abandonner ces modifications ?'**
|
||||
String get discardStepChangesConfirm;
|
||||
|
||||
/// No description provided for @discardQuestionChangesConfirm.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
/// **'Cette question contient des modifications non enregistrées, ainsi que ses réponses. Abandonner ces modifications ?'**
|
||||
String get discardQuestionChangesConfirm;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@ -1744,4 +1744,16 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get annotationsLabel => 'Annotations';
|
||||
|
||||
@override
|
||||
String get discardPathChangesConfirm =>
|
||||
'This path has unsaved changes, along with its steps and their questions. Discard these changes?';
|
||||
|
||||
@override
|
||||
String get discardStepChangesConfirm =>
|
||||
'This step has unsaved changes, along with its questions. Discard these changes?';
|
||||
|
||||
@override
|
||||
String get discardQuestionChangesConfirm =>
|
||||
'This question has unsaved changes, along with its answers. Discard these changes?';
|
||||
}
|
||||
|
||||
@ -1782,4 +1782,16 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get annotationsLabel => 'Annotations';
|
||||
|
||||
@override
|
||||
String get discardPathChangesConfirm =>
|
||||
'Ce parcours contient des modifications non enregistrées, ainsi que ses étapes et leurs questions. Abandonner ces modifications ?';
|
||||
|
||||
@override
|
||||
String get discardStepChangesConfirm =>
|
||||
'Cette étape contient des modifications non enregistrées, ainsi que ses questions. Abandonner ces modifications ?';
|
||||
|
||||
@override
|
||||
String get discardQuestionChangesConfirm =>
|
||||
'Cette question contient des modifications non enregistrées, ainsi que ses réponses. Abandonner ces modifications ?';
|
||||
}
|
||||
|
||||
@ -1761,4 +1761,16 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get annotationsLabel => 'Annotaties';
|
||||
|
||||
@override
|
||||
String get discardPathChangesConfirm =>
|
||||
'Deze route heeft niet-opgeslagen wijzigingen, samen met de stappen en hun vragen. Deze wijzigingen weggooien?';
|
||||
|
||||
@override
|
||||
String get discardStepChangesConfirm =>
|
||||
'Deze stap heeft niet-opgeslagen wijzigingen, samen met de vragen. Deze wijzigingen weggooien?';
|
||||
|
||||
@override
|
||||
String get discardQuestionChangesConfirm =>
|
||||
'Deze vraag heeft niet-opgeslagen wijzigingen, samen met de antwoorden. Deze wijzigingen weggooien?';
|
||||
}
|
||||
|
||||
@ -681,5 +681,8 @@
|
||||
"resourceDeleteConfirm": "Weet u zeker dat u deze resource wilt verwijderen?",
|
||||
"categoriesTitle": "Categorieën",
|
||||
"endTimeLabel": "Eindtijd",
|
||||
"annotationsLabel": "Annotaties"
|
||||
"annotationsLabel": "Annotaties",
|
||||
"discardPathChangesConfirm": "Deze route heeft niet-opgeslagen wijzigingen, samen met de stappen en hun vragen. Deze wijzigingen weggooien?",
|
||||
"discardStepChangesConfirm": "Deze stap heeft niet-opgeslagen wijzigingen, samen met de vragen. Deze wijzigingen weggooien?",
|
||||
"discardQuestionChangesConfirm": "Deze vraag heeft niet-opgeslagen wijzigingen, samen met de antwoorden. Deze wijzigingen weggooien?"
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user