mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-07-04 13:43:38 +02:00
Add priority/template wizard and raw-as-default for guided mode
Entering Guided on an empty draft now runs a two-step wizard (priority, then template) before showing the stepper. CaptureScreen defaults to Raw mode so a quick capture stays in the single-field flow; switching to Guided opens the wizard. Chrome (template/mode bar and priority/status row) is hidden while Guided or the wizard is active. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Cb7oE5Xsc8zSBHHtT6qwa
This commit is contained in:
parent
f91311f3f9
commit
29f94e76a5
@ -81,6 +81,10 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
SyncSettings? _settings;
|
SyncSettings? _settings;
|
||||||
bool _syncing = false;
|
bool _syncing = false;
|
||||||
|
|
||||||
|
/// Hides the Priority/Status row while the editor's own bare-guided chrome
|
||||||
|
/// (template/mode selectors) is also hidden, so the two stay in lockstep.
|
||||||
|
bool _chromeVisible = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -301,6 +305,7 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
_lastSavedAt = null;
|
_lastSavedAt = null;
|
||||||
_draftPriority = Priority.defaultValue;
|
_draftPriority = Priority.defaultValue;
|
||||||
_draftStatus = Status.todo;
|
_draftStatus = Status.todo;
|
||||||
|
_chromeVisible = true;
|
||||||
});
|
});
|
||||||
if (saved) {
|
if (saved) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@ -359,35 +364,44 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
// Pickers sit above the editor so the bottom-right Save FAB
|
// Pickers sit above the editor so the bottom-right Save FAB
|
||||||
// never overlaps them.
|
// never overlaps them. Hidden together with the editor's own
|
||||||
Row(
|
// chrome while the bare guided stepper or its entry wizard is up,
|
||||||
children: [
|
// so the top of the screen stays free of noise.
|
||||||
Expanded(
|
if (_chromeVisible) ...[
|
||||||
child: _MetaDropdown<Priority>(
|
Row(
|
||||||
label: 'Priority',
|
children: [
|
||||||
value: _draftPriority,
|
Expanded(
|
||||||
values: Priority.values,
|
child: _MetaDropdown<Priority>(
|
||||||
labelOf: (p) => p.label,
|
label: 'Priority',
|
||||||
onChanged: _setPriority,
|
value: _draftPriority,
|
||||||
|
values: Priority.values,
|
||||||
|
labelOf: (p) => p.label,
|
||||||
|
onChanged: _setPriority,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 12),
|
||||||
const SizedBox(width: 12),
|
Expanded(
|
||||||
Expanded(
|
child: _MetaDropdown<Status>(
|
||||||
child: _MetaDropdown<Status>(
|
label: 'Status',
|
||||||
label: 'Status',
|
value: _draftStatus,
|
||||||
value: _draftStatus,
|
values: Status.values,
|
||||||
values: Status.values,
|
labelOf: (s) => s.label,
|
||||||
labelOf: (s) => s.label,
|
onChanged: _setStatus,
|
||||||
onChanged: _setStatus,
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
const SizedBox(height: 12),
|
||||||
const SizedBox(height: 12),
|
],
|
||||||
Expanded(
|
Expanded(
|
||||||
child: NoteEditor(
|
child: NoteEditor(
|
||||||
key: ValueKey(_editorGeneration),
|
key: ValueKey(_editorGeneration),
|
||||||
initialTemplate: NoteTemplate.defaultTemplate,
|
initialTemplate: NoteTemplate.defaultTemplate,
|
||||||
|
initialMode: NoteEditorMode.raw,
|
||||||
|
priority: _draftPriority,
|
||||||
|
onPriorityChanged: _setPriority,
|
||||||
|
onChromeVisibleChanged: (visible) =>
|
||||||
|
setState(() => _chromeVisible = visible),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
onChanged: _onChanged,
|
onChanged: _onChanged,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -28,6 +28,10 @@ class NoteDetailScreen extends StatefulWidget {
|
|||||||
class _NoteDetailScreenState extends State<NoteDetailScreen> {
|
class _NoteDetailScreenState extends State<NoteDetailScreen> {
|
||||||
late Note _note = widget.note;
|
late Note _note = widget.note;
|
||||||
|
|
||||||
|
/// Hides the Priority/Status row while the editor's own bare-guided chrome
|
||||||
|
/// (template/mode selectors) is also hidden, so the two stay in lockstep.
|
||||||
|
bool _chromeVisible = true;
|
||||||
|
|
||||||
Future<void> _persist(Note next) async {
|
Future<void> _persist(Note next) async {
|
||||||
setState(() => _note = next);
|
setState(() => _note = next);
|
||||||
await widget.repository.upsert(next);
|
await widget.repository.upsert(next);
|
||||||
@ -60,38 +64,46 @@ class _NoteDetailScreenState extends State<NoteDetailScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
if (_chromeVisible) ...[
|
||||||
children: [
|
Row(
|
||||||
Expanded(
|
children: [
|
||||||
child: _MetaDropdown<Priority>(
|
Expanded(
|
||||||
label: 'Priority',
|
child: _MetaDropdown<Priority>(
|
||||||
value: _note.priority,
|
label: 'Priority',
|
||||||
values: Priority.values,
|
value: _note.priority,
|
||||||
labelOf: (p) => p.label,
|
values: Priority.values,
|
||||||
onChanged: (p) => _persist(
|
labelOf: (p) => p.label,
|
||||||
_note.copyWith(priority: p, updatedAt: DateTime.now()),
|
onChanged: (p) => _persist(
|
||||||
|
_note.copyWith(priority: p, updatedAt: DateTime.now()),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 12),
|
||||||
const SizedBox(width: 12),
|
Expanded(
|
||||||
Expanded(
|
child: _MetaDropdown<Status>(
|
||||||
child: _MetaDropdown<Status>(
|
label: 'Status',
|
||||||
label: 'Status',
|
value: _note.status,
|
||||||
value: _note.status,
|
values: Status.values,
|
||||||
values: Status.values,
|
labelOf: (s) => s.label,
|
||||||
labelOf: (s) => s.label,
|
onChanged: (s) => _persist(
|
||||||
onChanged: (s) => _persist(
|
_note.copyWith(status: s, updatedAt: DateTime.now()),
|
||||||
_note.copyWith(status: s, updatedAt: DateTime.now()),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
const SizedBox(height: 12),
|
||||||
const SizedBox(height: 12),
|
],
|
||||||
Expanded(
|
Expanded(
|
||||||
child: NoteEditor(
|
child: NoteEditor(
|
||||||
initialText: _note.text,
|
initialText: _note.text,
|
||||||
initialMode: NoteEditorMode.preview,
|
initialMode: NoteEditorMode.raw,
|
||||||
|
priority: _note.priority,
|
||||||
|
onPriorityChanged: (p) => _persist(
|
||||||
|
_note.copyWith(priority: p, updatedAt: DateTime.now()),
|
||||||
|
),
|
||||||
|
onChromeVisibleChanged: (visible) =>
|
||||||
|
setState(() => _chromeVisible = visible),
|
||||||
onChanged: _onTextChanged,
|
onChanged: _onTextChanged,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../data/note.dart';
|
||||||
import '../data/note_template.dart';
|
import '../data/note_template.dart';
|
||||||
import 'markdown_view.dart';
|
import 'markdown_view.dart';
|
||||||
|
|
||||||
@ -31,9 +32,19 @@ enum NoteEditorMode {
|
|||||||
/// Non-conforming or freeform text never enters the guided stepper (we never
|
/// Non-conforming or freeform text never enters the guided stepper (we never
|
||||||
/// force it into the template), so for such text Guided is unavailable and the
|
/// force it into the template), so for such text Guided is unavailable and the
|
||||||
/// editable source stays the raw body, preserving the user's content.
|
/// editable source stays the raw body, preserving the user's content.
|
||||||
|
///
|
||||||
|
/// Entering Guided on an empty draft first runs a two-step wizard (priority,
|
||||||
|
/// then template) via [onPriorityChanged], since those choices only make
|
||||||
|
/// sense once, before there's anything to guide. Guided itself — wizard or
|
||||||
|
/// bare stepper — hides the template/mode chrome entirely (just a back arrow
|
||||||
|
/// to return to Raw); [onChromeVisibleChanged] tells the parent screen to
|
||||||
|
/// hide its own priority/status row in sync.
|
||||||
class NoteEditor extends StatefulWidget {
|
class NoteEditor extends StatefulWidget {
|
||||||
const NoteEditor({
|
const NoteEditor({
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
|
required this.priority,
|
||||||
|
required this.onPriorityChanged,
|
||||||
|
required this.onChromeVisibleChanged,
|
||||||
this.initialText = '',
|
this.initialText = '',
|
||||||
this.initialTemplate,
|
this.initialTemplate,
|
||||||
this.initialMode = NoteEditorMode.guided,
|
this.initialMode = NoteEditorMode.guided,
|
||||||
@ -44,6 +55,18 @@ class NoteEditor extends StatefulWidget {
|
|||||||
/// Called with the freshly assembled note text on every edit.
|
/// Called with the freshly assembled note text on every edit.
|
||||||
final ValueChanged<String> onChanged;
|
final ValueChanged<String> onChanged;
|
||||||
|
|
||||||
|
/// The note's current priority, shown as the wizard's starting selection.
|
||||||
|
final Priority priority;
|
||||||
|
|
||||||
|
/// Called when the priority wizard step is confirmed (Guided "Start").
|
||||||
|
final ValueChanged<Priority> onPriorityChanged;
|
||||||
|
|
||||||
|
/// Called whenever the editor's own chrome (template dropdown, mode
|
||||||
|
/// selector) is shown/hidden, so the parent screen can hide its
|
||||||
|
/// priority/status row in sync while Guided (wizard or bare stepper) is
|
||||||
|
/// active.
|
||||||
|
final ValueChanged<bool> onChromeVisibleChanged;
|
||||||
|
|
||||||
/// Existing note text to load. Empty for a fresh draft.
|
/// Existing note text to load. Empty for a fresh draft.
|
||||||
final String initialText;
|
final String initialText;
|
||||||
|
|
||||||
@ -75,6 +98,18 @@ class _NoteEditorState extends State<NoteEditor> {
|
|||||||
|
|
||||||
int _currentStep = 0;
|
int _currentStep = 0;
|
||||||
|
|
||||||
|
/// Whether the priority+template entry wizard is showing in place of the
|
||||||
|
/// normal chrome. True only between tapping Guided on an empty draft and
|
||||||
|
/// either "Start" (which flips to bare Guided) or "Cancel" (back to Raw).
|
||||||
|
bool _enteringGuided = false;
|
||||||
|
|
||||||
|
/// Which wizard step (0: priority, 1: template) is showing.
|
||||||
|
int _wizardStep = 0;
|
||||||
|
|
||||||
|
/// Working copies of the wizard's two choices, committed on "Start".
|
||||||
|
Priority _wizardPriority = Priority.defaultValue;
|
||||||
|
NoteTemplate _wizardTemplate = NoteTemplate.defaultTemplate;
|
||||||
|
|
||||||
/// One controller per structured section (keyed by section key).
|
/// One controller per structured section (keyed by section key).
|
||||||
final Map<String, TextEditingController> _section = {};
|
final Map<String, TextEditingController> _section = {};
|
||||||
|
|
||||||
@ -94,14 +129,30 @@ class _NoteEditorState extends State<NoteEditor> {
|
|||||||
final initial = widget.initialTemplate;
|
final initial = widget.initialTemplate;
|
||||||
if (initial != null) {
|
if (initial != null) {
|
||||||
_template = initial;
|
_template = initial;
|
||||||
_loadSource(initial, widget.initialText, preferGuided: true);
|
// Only prefer the guided (sections) source for an empty draft when the
|
||||||
|
// caller actually wants to open in Guided — an empty Raw draft (the new
|
||||||
|
// default) must keep the raw body as its source, or typing into the Raw
|
||||||
|
// field would silently update the (hidden, unused) section controllers
|
||||||
|
// instead of what's emitted via onChanged.
|
||||||
|
_loadSource(
|
||||||
|
initial,
|
||||||
|
widget.initialText,
|
||||||
|
preferGuided: widget.initialMode != NoteEditorMode.raw,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Detect: does the text cleanly fit the design-spec template?
|
// Detect: does the text cleanly fit the design-spec template?
|
||||||
final parsed = parse(NoteTemplate.llmDesignSpec, widget.initialText);
|
final parsed = parse(NoteTemplate.llmDesignSpec, widget.initialText);
|
||||||
if (parsed.conforms) {
|
if (parsed.conforms) {
|
||||||
_template = NoteTemplate.llmDesignSpec;
|
_template = NoteTemplate.llmDesignSpec;
|
||||||
_rawSource = false;
|
// Same Raw/source consistency rule as above: an explicit Raw request
|
||||||
_fillSections(parsed.values);
|
// must keep the raw body as the source even for conforming text.
|
||||||
|
if (widget.initialMode == NoteEditorMode.raw) {
|
||||||
|
_rawSource = true;
|
||||||
|
_body.text = widget.initialText;
|
||||||
|
} else {
|
||||||
|
_rawSource = false;
|
||||||
|
_fillSections(parsed.values);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Freeform / legacy / hand-mangled — keep it as a raw body, untouched.
|
// Freeform / legacy / hand-mangled — keep it as a raw body, untouched.
|
||||||
_template = NoteTemplate.blank;
|
_template = NoteTemplate.blank;
|
||||||
@ -244,7 +295,18 @@ class _NoteEditorState extends State<NoteEditor> {
|
|||||||
_rawSource = false;
|
_rawSource = false;
|
||||||
}
|
}
|
||||||
_currentStep = 0;
|
_currentStep = 0;
|
||||||
_mode = NoteEditorMode.guided;
|
if (_currentText().trim().isEmpty) {
|
||||||
|
// Fresh, empty draft: priority and template are meaningful
|
||||||
|
// choices only once, so ask for them before showing the stepper.
|
||||||
|
_enteringGuided = true;
|
||||||
|
_wizardStep = 0;
|
||||||
|
_wizardPriority = widget.priority;
|
||||||
|
_wizardTemplate = NoteTemplate.defaultTemplate;
|
||||||
|
} else {
|
||||||
|
// Existing content: priority/template are already settled, so
|
||||||
|
// skip straight to the bare stepper rather than re-asking.
|
||||||
|
_mode = NoteEditorMode.guided;
|
||||||
|
}
|
||||||
case NoteEditorMode.raw:
|
case NoteEditorMode.raw:
|
||||||
if (!_rawSource) {
|
if (!_rawSource) {
|
||||||
// guided -> raw: materialise the assembled text into the body.
|
// guided -> raw: materialise the assembled text into the body.
|
||||||
@ -257,67 +319,225 @@ class _NoteEditorState extends State<NoteEditor> {
|
|||||||
_mode = NoteEditorMode.preview;
|
_mode = NoteEditorMode.preview;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
widget.onChromeVisibleChanged(
|
||||||
|
!_enteringGuided && _mode != NoteEditorMode.guided,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Commits the wizard's template choice and enters the bare stepper.
|
||||||
|
/// Distinct from [_switchTemplate]: that short-circuits when the template
|
||||||
|
/// is unchanged, which would skip flipping out of the wizard here.
|
||||||
|
void _enterGuidedWithTemplate(NoteTemplate template) {
|
||||||
|
final text = _currentText();
|
||||||
|
setState(() {
|
||||||
|
_template = template;
|
||||||
|
_currentStep = 0;
|
||||||
|
_loadSource(template, text, preferGuided: true);
|
||||||
|
_mode = NoteEditorMode.guided;
|
||||||
|
_enteringGuided = false;
|
||||||
|
});
|
||||||
|
_emit();
|
||||||
|
widget.onChromeVisibleChanged(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Aborts the wizard, returning to Raw with the chrome restored.
|
||||||
|
void _cancelWizard() {
|
||||||
|
setState(() => _enteringGuided = false);
|
||||||
|
widget.onChromeVisibleChanged(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exits the bare stepper back to Raw, restoring the chrome.
|
||||||
|
void _exitGuided() {
|
||||||
|
setState(() {
|
||||||
|
if (!_rawSource) {
|
||||||
|
_body.text = _currentText();
|
||||||
|
_rawSource = true;
|
||||||
|
}
|
||||||
|
_mode = NoteEditorMode.raw;
|
||||||
|
});
|
||||||
|
widget.onChromeVisibleChanged(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
if (_enteringGuided) return _buildGuidedEntryWizard(theme);
|
||||||
|
|
||||||
|
// Guided (the bare stepper) hides the template/mode chrome entirely —
|
||||||
|
// that's the point of "Guided": just the stepper, no top-bar noise. A
|
||||||
|
// single back arrow is the only way out, restoring the full chrome.
|
||||||
|
final bareGuided = _mode == NoteEditorMode.guided;
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
DropdownButtonFormField<String>(
|
if (bareGuided)
|
||||||
initialValue: _template.id,
|
Align(
|
||||||
isDense: true,
|
alignment: Alignment.centerLeft,
|
||||||
decoration: const InputDecoration(
|
child: IconButton(
|
||||||
labelText: 'Template',
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
tooltip: 'Exit guided',
|
||||||
|
onPressed: _exitGuided,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else ...[
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
initialValue: _template.id,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
border: OutlineInputBorder(),
|
decoration: const InputDecoration(
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
labelText: 'Template',
|
||||||
),
|
isDense: true,
|
||||||
items: [
|
border: OutlineInputBorder(),
|
||||||
for (final t in NoteTemplate.all)
|
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
DropdownMenuItem(value: t.id, child: Text(t.label)),
|
),
|
||||||
],
|
items: [
|
||||||
onChanged: (id) {
|
for (final t in NoteTemplate.all)
|
||||||
if (id == null) return;
|
DropdownMenuItem(value: t.id, child: Text(t.label)),
|
||||||
_switchTemplate(NoteTemplate.all.firstWhere((t) => t.id == id));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: SegmentedButton<NoteEditorMode>(
|
|
||||||
showSelectedIcon: false,
|
|
||||||
segments: [
|
|
||||||
const ButtonSegment(
|
|
||||||
value: NoteEditorMode.preview,
|
|
||||||
icon: Icon(Icons.visibility_outlined),
|
|
||||||
label: Text('View'),
|
|
||||||
),
|
|
||||||
// Guided is offered for any structured template; switching to it
|
|
||||||
// is blocked at switch time if the raw text no longer conforms.
|
|
||||||
if (!_template.isFreeform)
|
|
||||||
const ButtonSegment(
|
|
||||||
value: NoteEditorMode.guided,
|
|
||||||
icon: Icon(Icons.checklist),
|
|
||||||
label: Text('Guided'),
|
|
||||||
),
|
|
||||||
const ButtonSegment(
|
|
||||||
value: NoteEditorMode.raw,
|
|
||||||
icon: Icon(Icons.notes),
|
|
||||||
label: Text('Raw'),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
selected: {_mode},
|
onChanged: (id) {
|
||||||
onSelectionChanged: (s) => _setMode(s.first),
|
if (id == null) return;
|
||||||
|
_switchTemplate(NoteTemplate.all.firstWhere((t) => t.id == id));
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 8),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: SegmentedButton<NoteEditorMode>(
|
||||||
|
showSelectedIcon: false,
|
||||||
|
segments: [
|
||||||
|
const ButtonSegment(
|
||||||
|
value: NoteEditorMode.preview,
|
||||||
|
icon: Icon(Icons.visibility_outlined),
|
||||||
|
label: Text('View'),
|
||||||
|
),
|
||||||
|
// Guided is offered for any structured template; tapping it
|
||||||
|
// on an empty draft opens the wizard (see _setMode), and is
|
||||||
|
// blocked at switch time if the raw text no longer conforms.
|
||||||
|
if (!_template.isFreeform)
|
||||||
|
const ButtonSegment(
|
||||||
|
value: NoteEditorMode.guided,
|
||||||
|
icon: Icon(Icons.checklist),
|
||||||
|
label: Text('Guided'),
|
||||||
|
),
|
||||||
|
const ButtonSegment(
|
||||||
|
value: NoteEditorMode.raw,
|
||||||
|
icon: Icon(Icons.notes),
|
||||||
|
label: Text('Raw'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
selected: {_mode},
|
||||||
|
onSelectionChanged: (s) => _setMode(s.first),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Expanded(child: _buildBody(theme)),
|
Expanded(child: _buildBody(theme)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The two-step priority -> template wizard shown before a fresh draft
|
||||||
|
/// enters Guided. Replaces the normal chrome entirely (see [build]).
|
||||||
|
Widget _buildGuidedEntryWizard(ThemeData theme) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
tooltip: 'Cancel',
|
||||||
|
onPressed: _cancelWizard,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Step ${_wizardStep + 1} of 2',
|
||||||
|
style: theme.textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (_wizardStep == 0)
|
||||||
|
..._buildWizardPriorityStep()
|
||||||
|
else
|
||||||
|
..._buildWizardTemplateStep(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildWizardPriorityStep() {
|
||||||
|
return [
|
||||||
|
DropdownButtonFormField<Priority>(
|
||||||
|
initialValue: _wizardPriority,
|
||||||
|
isDense: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Priority',
|
||||||
|
isDense: true,
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
for (final p in Priority.values)
|
||||||
|
DropdownMenuItem(value: p, child: Text(p.label)),
|
||||||
|
],
|
||||||
|
onChanged: (p) {
|
||||||
|
if (p != null) setState(() => _wizardPriority = p);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: () => setState(() => _wizardStep = 1),
|
||||||
|
child: const Text('Next'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildWizardTemplateStep() {
|
||||||
|
final templates = NoteTemplate.all.where((t) => !t.isFreeform).toList();
|
||||||
|
return [
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
initialValue: _wizardTemplate.id,
|
||||||
|
isDense: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Template',
|
||||||
|
isDense: true,
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
for (final t in templates)
|
||||||
|
DropdownMenuItem(value: t.id, child: Text(t.label)),
|
||||||
|
],
|
||||||
|
onChanged: (id) {
|
||||||
|
if (id == null) return;
|
||||||
|
setState(
|
||||||
|
() => _wizardTemplate = templates.firstWhere((t) => t.id == id),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => setState(() => _wizardStep = 0),
|
||||||
|
child: const Text('Back'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
widget.onPriorityChanged(_wizardPriority);
|
||||||
|
_enterGuidedWithTemplate(_wizardTemplate);
|
||||||
|
},
|
||||||
|
child: const Text('Start'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildBody(ThemeData theme) {
|
Widget _buildBody(ThemeData theme) {
|
||||||
switch (_mode) {
|
switch (_mode) {
|
||||||
case NoteEditorMode.preview:
|
case NoteEditorMode.preview:
|
||||||
|
|||||||
@ -63,16 +63,31 @@ void main() {
|
|||||||
'sync.token': 'tok',
|
'sync.token': 'tok',
|
||||||
};
|
};
|
||||||
|
|
||||||
testWidgets('opens the guided editor with section guidance', (tester) async {
|
testWidgets('defaults to Raw, with Guided available via the entry wizard', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
await pumpCapture(tester);
|
await pumpCapture(tester);
|
||||||
|
|
||||||
// The guided stepper shows the design-spec sections and the title step's
|
// Defaults to Raw: a single text field, no note persisted yet.
|
||||||
// guidance, with no note persisted yet.
|
expect(find.byType(TextField), findsOneWidget);
|
||||||
expect(find.text('Guided'), findsOneWidget);
|
expect(find.text('Guided'), findsOneWidget);
|
||||||
|
expect(find.text('0 saved'), findsOneWidget);
|
||||||
|
|
||||||
|
// Tapping Guided on the empty draft opens the priority+template wizard
|
||||||
|
// rather than jumping straight into the stepper.
|
||||||
|
await tester.tap(find.text('Guided'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Step 1 of 2'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Next'));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.tap(find.text('Start'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Now in the bare guided stepper, showing the design-spec sections.
|
||||||
expect(find.textContaining('imperative'), findsOneWidget); // title helper
|
expect(find.textContaining('imperative'), findsOneWidget); // title helper
|
||||||
expect(find.text('what'), findsOneWidget); // a section step header
|
expect(find.text('what'), findsOneWidget); // a section step header
|
||||||
expect(find.text('done'), findsOneWidget);
|
expect(find.text('done'), findsOneWidget);
|
||||||
expect(find.text('0 saved'), findsOneWidget);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('saving the untouched template creates no note', (tester) async {
|
testWidgets('saving the untouched template creates no note', (tester) async {
|
||||||
@ -115,8 +130,10 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(await repo.listNotes(), hasLength(1));
|
expect(await repo.listNotes(), hasLength(1));
|
||||||
// The editor reset to a fresh guided template (title guidance shown again).
|
// The editor reset to a fresh, empty Raw draft.
|
||||||
expect(find.textContaining('imperative'), findsOneWidget);
|
expect(find.byType(TextField), findsOneWidget);
|
||||||
|
final raw = tester.widget<TextField>(find.byType(TextField));
|
||||||
|
expect(raw.controller!.text, isEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('tapping Sync while unconfigured prompts for a token', (
|
testWidgets('tapping Sync while unconfigured prompts for a token', (
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:todo/data/note.dart';
|
import 'package:todo/data/note.dart';
|
||||||
import 'package:todo/ui/markdown_view.dart';
|
|
||||||
import 'package:todo/ui/note_detail_screen.dart';
|
import 'package:todo/ui/note_detail_screen.dart';
|
||||||
|
|
||||||
import 'fake_note_repository.dart';
|
import 'fake_note_repository.dart';
|
||||||
@ -33,14 +32,12 @@ void main() {
|
|||||||
return repo;
|
return repo;
|
||||||
}
|
}
|
||||||
|
|
||||||
testWidgets('opens in the rendered Markdown view with the title in the bar', (
|
testWidgets('opens in Raw with the title in the app bar', (tester) async {
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await pumpDetail(tester, seedNote('# My note\n\n## what\n_why_\n\nbody'));
|
await pumpDetail(tester, seedNote('# My note\n\n## what\n_why_\n\nbody'));
|
||||||
|
|
||||||
expect(find.byType(MarkdownView), findsOneWidget);
|
final raw = tester.widget<TextField>(find.byType(TextField));
|
||||||
// Title appears both in the app bar and the rendered body.
|
expect(raw.controller!.text, contains('My note'));
|
||||||
expect(find.text('My note'), findsWidgets);
|
expect(find.text('My note'), findsOneWidget); // app bar title
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('changing the priority dropdown persists the note', (
|
testWidgets('changing the priority dropdown persists the note', (
|
||||||
@ -84,6 +81,34 @@ void main() {
|
|||||||
expect((await repo.listNotes()).single.text, contains('new body'));
|
expect((await repo.listNotes()).single.text, contains('new body'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'clearing the body then tapping Guided runs the wizard and persists the chosen priority',
|
||||||
|
(tester) async {
|
||||||
|
final repo = await pumpDetail(tester, seedNote('# T\n\n## what\nold'));
|
||||||
|
|
||||||
|
await tester.enterText(find.byType(TextField), '');
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Guided'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Step 1 of 2'), findsOneWidget);
|
||||||
|
// Wizard hides the Status dropdown (onChromeVisibleChanged(false)).
|
||||||
|
expect(find.text('Status'), findsNothing);
|
||||||
|
|
||||||
|
await tester.tap(find.byType(DropdownButtonFormField<Priority>));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('High').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Next'));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.tap(find.text('Start'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect((await repo.listNotes()).single.priority, Priority.high);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets('the delete action removes the note and pops', (tester) async {
|
testWidgets('the delete action removes the note and pops', (tester) async {
|
||||||
final repo = await pumpDetail(tester, seedNote('# Bye'));
|
final repo = await pumpDetail(tester, seedNote('# Bye'));
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:todo/data/note.dart';
|
||||||
import 'package:todo/data/note_template.dart';
|
import 'package:todo/data/note_template.dart';
|
||||||
import 'package:todo/ui/markdown_view.dart';
|
import 'package:todo/ui/markdown_view.dart';
|
||||||
import 'package:todo/ui/note_editor.dart';
|
import 'package:todo/ui/note_editor.dart';
|
||||||
@ -14,6 +15,9 @@ void main() {
|
|||||||
String initialText = '',
|
String initialText = '',
|
||||||
NoteTemplate? initialTemplate,
|
NoteTemplate? initialTemplate,
|
||||||
NoteEditorMode initialMode = NoteEditorMode.guided,
|
NoteEditorMode initialMode = NoteEditorMode.guided,
|
||||||
|
Priority priority = Priority.defaultValue,
|
||||||
|
ValueChanged<Priority>? onPriorityChanged,
|
||||||
|
ValueChanged<bool>? onChromeVisibleChanged,
|
||||||
}) async {
|
}) async {
|
||||||
final emitted = <String>[];
|
final emitted = <String>[];
|
||||||
tester.view.physicalSize = const Size(1200, 2400);
|
tester.view.physicalSize = const Size(1200, 2400);
|
||||||
@ -27,6 +31,9 @@ void main() {
|
|||||||
initialText: initialText,
|
initialText: initialText,
|
||||||
initialTemplate: initialTemplate,
|
initialTemplate: initialTemplate,
|
||||||
initialMode: initialMode,
|
initialMode: initialMode,
|
||||||
|
priority: priority,
|
||||||
|
onPriorityChanged: onPriorityChanged ?? (_) {},
|
||||||
|
onChromeVisibleChanged: onChromeVisibleChanged ?? (_) {},
|
||||||
onChanged: emitted.add,
|
onChanged: emitted.add,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -41,7 +48,7 @@ void main() {
|
|||||||
) async {
|
) async {
|
||||||
final emitted = await pumpEditor(tester, initialTemplate: spec);
|
final emitted = await pumpEditor(tester, initialTemplate: spec);
|
||||||
|
|
||||||
expect(find.text('Guided'), findsOneWidget);
|
expect(find.byTooltip('Exit guided'), findsOneWidget);
|
||||||
await tester.enterText(find.byType(TextField).first, 'Dark mode');
|
await tester.enterText(find.byType(TextField).first, 'Dark mode');
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
@ -73,8 +80,12 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('switching to View renders the note as Markdown', (tester) async {
|
testWidgets('switching to View renders the note as Markdown', (tester) async {
|
||||||
await pumpEditor(tester, initialTemplate: spec);
|
await pumpEditor(
|
||||||
await tester.enterText(find.byType(TextField).first, 'Render me');
|
tester,
|
||||||
|
initialTemplate: spec,
|
||||||
|
initialMode: NoteEditorMode.raw,
|
||||||
|
);
|
||||||
|
await tester.enterText(find.byType(TextField).first, '# Render me');
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
await tester.tap(find.text('View'));
|
await tester.tap(find.text('View'));
|
||||||
@ -84,23 +95,24 @@ void main() {
|
|||||||
expect(find.text('Render me'), findsOneWidget); // rendered heading text
|
expect(find.text('Render me'), findsOneWidget); // rendered heading text
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('guided → Raw materialises the assembled text and edits emit', (
|
testWidgets(
|
||||||
tester,
|
'bare Guided back arrow returns to Raw, materialising the assembled text',
|
||||||
) async {
|
(tester) async {
|
||||||
final emitted = await pumpEditor(tester, initialTemplate: spec);
|
final emitted = await pumpEditor(tester, initialTemplate: spec);
|
||||||
await tester.enterText(find.byType(TextField).first, 'T');
|
await tester.enterText(find.byType(TextField).first, 'T');
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
await tester.tap(find.text('Raw'));
|
await tester.tap(find.byTooltip('Exit guided'));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
final raw = tester.widget<TextField>(find.byType(TextField));
|
final raw = tester.widget<TextField>(find.byType(TextField));
|
||||||
expect(raw.controller!.text, startsWith('# T'));
|
expect(raw.controller!.text, startsWith('# T'));
|
||||||
|
|
||||||
await tester.enterText(find.byType(TextField), '# T\n\n## what\nedited');
|
await tester.enterText(find.byType(TextField), '# T\n\n## what\nedited');
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(emitted.last, contains('edited'));
|
expect(emitted.last, contains('edited'));
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets('Raw → Guided is blocked with a snackbar when non-conforming', (
|
testWidgets('Raw → Guided is blocked with a snackbar when non-conforming', (
|
||||||
tester,
|
tester,
|
||||||
@ -120,25 +132,29 @@ void main() {
|
|||||||
expect(find.byType(Stepper), findsNothing); // still raw
|
expect(find.byType(Stepper), findsNothing); // still raw
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('guided → Raw → Guided round-trips when text still conforms', (
|
testWidgets(
|
||||||
tester,
|
'guided → Raw → Guided round-trips (skipping the wizard) when text still conforms',
|
||||||
) async {
|
(tester) async {
|
||||||
// Open guided from a conforming note (pre-fills the section controllers
|
// Open guided from a conforming note (pre-fills the section controllers
|
||||||
// without typing into collapsed steps), so the assembled body conforms.
|
// without typing into collapsed steps), so the assembled body conforms.
|
||||||
final conforming = assemble(spec, {'title': 'Keep', 'what': 'a body'});
|
final conforming = assemble(spec, {'title': 'Keep', 'what': 'a body'});
|
||||||
await pumpEditor(tester, initialTemplate: spec, initialText: conforming);
|
await pumpEditor(tester, initialTemplate: spec, initialText: conforming);
|
||||||
expect(find.byType(Stepper), findsOneWidget);
|
expect(find.byType(Stepper), findsOneWidget);
|
||||||
|
|
||||||
// guided → raw makes the (still conforming) body the source…
|
// Bare guided -> raw via the back arrow makes the (still conforming)
|
||||||
await tester.tap(find.text('Raw'));
|
// body the source…
|
||||||
await tester.pump();
|
await tester.tap(find.byTooltip('Exit guided'));
|
||||||
expect(find.byType(Stepper), findsNothing);
|
await tester.pump();
|
||||||
|
expect(find.byType(Stepper), findsNothing);
|
||||||
|
|
||||||
// …then raw → guided re-parses the conforming body back into sections.
|
// …then raw -> guided re-parses the conforming body straight back into
|
||||||
await tester.tap(find.text('Guided'));
|
// the bare stepper — no wizard, since the content isn't empty.
|
||||||
await tester.pump();
|
await tester.tap(find.text('Guided'));
|
||||||
expect(find.byType(Stepper), findsOneWidget);
|
await tester.pump();
|
||||||
});
|
expect(find.byType(Stepper), findsOneWidget);
|
||||||
|
expect(find.text('Step 1 of 2'), findsNothing); // wizard was skipped
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets('freeform template offers only View and Raw', (tester) async {
|
testWidgets('freeform template offers only View and Raw', (tester) async {
|
||||||
final emitted = await pumpEditor(tester, initialTemplate: blank);
|
final emitted = await pumpEditor(tester, initialTemplate: blank);
|
||||||
@ -155,16 +171,19 @@ void main() {
|
|||||||
testWidgets('switching template via the dropdown reloads the source', (
|
testWidgets('switching template via the dropdown reloads the source', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
await pumpEditor(tester, initialTemplate: spec);
|
await pumpEditor(
|
||||||
expect(find.byType(Stepper), findsOneWidget);
|
tester,
|
||||||
|
initialTemplate: spec,
|
||||||
|
initialMode: NoteEditorMode.raw,
|
||||||
|
);
|
||||||
|
expect(find.text('Guided'), findsOneWidget); // structured: Guided offered
|
||||||
|
|
||||||
await tester.tap(find.text('LLM design spec').first); // open the menu
|
await tester.tap(find.text('LLM design spec').first); // open the menu
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.text('Blank').last);
|
await tester.tap(find.text('Blank').last);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// Freeform now: the stepper is gone and Guided is no longer offered.
|
// Freeform now: Guided is no longer offered.
|
||||||
expect(find.byType(Stepper), findsNothing);
|
|
||||||
expect(find.text('Guided'), findsNothing);
|
expect(find.text('Guided'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -175,7 +194,7 @@ void main() {
|
|||||||
await pumpEditor(tester, initialText: conforming);
|
await pumpEditor(tester, initialText: conforming);
|
||||||
|
|
||||||
expect(find.byType(Stepper), findsOneWidget); // guided by default
|
expect(find.byType(Stepper), findsOneWidget); // guided by default
|
||||||
expect(find.text('Guided'), findsOneWidget);
|
expect(find.byTooltip('Exit guided'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('detects a legacy note (no template given) as freeform raw', (
|
testWidgets('detects a legacy note (no template given) as freeform raw', (
|
||||||
@ -202,6 +221,23 @@ void main() {
|
|||||||
expect(find.text('Preview me'), findsOneWidget);
|
expect(find.text('Preview me'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Preview → Raw materialises the still-guided source', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final conforming = assemble(spec, {'title': 'Preview src', 'what': 'x'});
|
||||||
|
await pumpEditor(
|
||||||
|
tester,
|
||||||
|
initialText: conforming,
|
||||||
|
initialMode: NoteEditorMode.preview,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Raw'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final raw = tester.widget<TextField>(find.byType(TextField));
|
||||||
|
expect(raw.controller!.text, contains('Preview src'));
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('initialMode guided falls back to Raw for non-conforming text', (
|
testWidgets('initialMode guided falls back to Raw for non-conforming text', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
@ -217,4 +253,138 @@ void main() {
|
|||||||
final raw = tester.widget<TextField>(find.byType(TextField));
|
final raw = tester.widget<TextField>(find.byType(TextField));
|
||||||
expect(raw.controller!.text, 'cannot be guided');
|
expect(raw.controller!.text, 'cannot be guided');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'tapping Guided on an empty draft opens the priority+template wizard',
|
||||||
|
(tester) async {
|
||||||
|
await pumpEditor(
|
||||||
|
tester,
|
||||||
|
initialTemplate: spec,
|
||||||
|
initialMode: NoteEditorMode.raw,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Guided'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('Step 1 of 2'), findsOneWidget);
|
||||||
|
expect(find.byType(DropdownButtonFormField<Priority>), findsOneWidget);
|
||||||
|
expect(find.byType(Stepper), findsNothing);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'the wizard Next/Back moves between priority and template steps',
|
||||||
|
(tester) async {
|
||||||
|
await pumpEditor(
|
||||||
|
tester,
|
||||||
|
initialTemplate: spec,
|
||||||
|
initialMode: NoteEditorMode.raw,
|
||||||
|
);
|
||||||
|
await tester.tap(find.text('Guided'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Next'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Step 2 of 2'), findsOneWidget);
|
||||||
|
expect(find.byType(DropdownButtonFormField<String>), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Back'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Step 1 of 2'), findsOneWidget);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('the wizard template step only offers structured templates', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await pumpEditor(
|
||||||
|
tester,
|
||||||
|
initialTemplate: spec,
|
||||||
|
initialMode: NoteEditorMode.raw,
|
||||||
|
);
|
||||||
|
await tester.tap(find.text('Guided'));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.tap(find.text('Next'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.tap(find.text('LLM design spec').first); // open the menu
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Blank'), findsNothing);
|
||||||
|
|
||||||
|
// Select the (only) offered template, exercising the dropdown's onChanged.
|
||||||
|
await tester.tap(find.text('LLM design spec').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Step 2 of 2'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'wizard Start commits the chosen priority and enters the bare stepper',
|
||||||
|
(tester) async {
|
||||||
|
Priority? committed;
|
||||||
|
await pumpEditor(
|
||||||
|
tester,
|
||||||
|
initialTemplate: spec,
|
||||||
|
initialMode: NoteEditorMode.raw,
|
||||||
|
onPriorityChanged: (p) => committed = p,
|
||||||
|
);
|
||||||
|
await tester.tap(find.text('Guided'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.tap(find.byType(DropdownButtonFormField<Priority>));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('High').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Next'));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.tap(find.text('Start'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(committed, Priority.high);
|
||||||
|
expect(find.byType(Stepper), findsOneWidget);
|
||||||
|
expect(find.byTooltip('Exit guided'), findsOneWidget);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('wizard Cancel returns to Raw with the chrome restored', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final chromeVisible = <bool>[];
|
||||||
|
await pumpEditor(
|
||||||
|
tester,
|
||||||
|
initialTemplate: spec,
|
||||||
|
initialMode: NoteEditorMode.raw,
|
||||||
|
onChromeVisibleChanged: chromeVisible.add,
|
||||||
|
);
|
||||||
|
await tester.tap(find.text('Guided'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(chromeVisible.last, false);
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Cancel'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(chromeVisible.last, true);
|
||||||
|
expect(find.byType(Stepper), findsNothing);
|
||||||
|
expect(find.text('Raw'), findsOneWidget); // chrome's mode selector is back
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('exiting bare Guided via the back arrow restores the chrome', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final chromeVisible = <bool>[];
|
||||||
|
final conforming = assemble(spec, {'title': 'X', 'what': 'a body'});
|
||||||
|
await pumpEditor(
|
||||||
|
tester,
|
||||||
|
initialTemplate: spec,
|
||||||
|
initialText: conforming,
|
||||||
|
onChromeVisibleChanged: chromeVisible.add,
|
||||||
|
);
|
||||||
|
expect(find.byType(Stepper), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Exit guided'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(chromeVisible, contains(true));
|
||||||
|
expect(find.text('Raw'), findsOneWidget);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user