feat(editor): Fix-dialog from diagnostic strip + inline approval in Run tab
Two big operator-UX wins that the previous batch missed:
1. Diagnostic-strip header gets a 'Beheben' (Fix) button next
to 'Copy all'. Clicking opens a focused modal listing every
issue with its quick-fix actions plus an 'apply every quick
fix' master button. Replaces the cramped expand-strip flow
for operators who want a deliberate dialog instead of
scrolling in a 200-px footer.
2. Run tab inlines the Approve / Reject form right under any
step that the hub paused on system.approval@^0. The
FlowRunDriver gains three new methods (host-implements
them; defaults throw with a clear message):
- pendingApprovalIdForStep(flowName, stepId) → String?
- approveApproval(approvalId, reviewer) → Future<void>
- rejectApproval(approvalId, reviewer, reason)
The _InlineApprovalCard polls the driver for ~3s waiting for
the hub to materialise the approval row (event-stream race
against the hub's create), then renders the same form the
Approvals page does — reviewer + optional reason + Approve /
Reject buttons. After submit the hub picks up the decision
on its next poll and emits step.approved / step.rejected,
which the existing event stream already maps to the right
step status.
Closes the operator question 'wie soll das gehen dass da
Freigaben landen?': they land in the Approvals page AND
inline in the Run tab; the inline path is now the natural
workflow.
All 36 editor tests + 24 Studio tests green. Bumped to 0.21.0.
Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
parent
8b38584555
commit
303e318ba8
5 changed files with 593 additions and 1 deletions
|
|
@ -1427,6 +1427,24 @@ class _DiagnosticStripState extends State<_DiagnosticStrip> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Opens the "Fix flow issues" modal. Each issue gets a row
|
||||
/// with its quick fixes; rows without a fix surface the
|
||||
/// no-fix hint so the operator knows to edit YAML manually.
|
||||
/// A master "apply all" runs every available fix sequentially,
|
||||
/// pausing on first failure.
|
||||
Future<void> _showFixDialog(List<Issue> issues, ThemeData theme) async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => _FixDialog(
|
||||
issues: issues,
|
||||
controller: widget.controller,
|
||||
strings: widget.strings,
|
||||
toneForIssue: _toneForIssue,
|
||||
onApplyFix: _runFix,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
|
@ -1507,6 +1525,33 @@ class _DiagnosticStripState extends State<_DiagnosticStrip> {
|
|||
),
|
||||
),
|
||||
),
|
||||
// "Beheben" — opens a modal enumerating each
|
||||
// issue with its fixes plus an "apply all"
|
||||
// master button. Only shows when at least one
|
||||
// issue carries a quick fix; otherwise the
|
||||
// strip stays minimal.
|
||||
if (issues.any(
|
||||
(i) => widget.controller.fixesFor(i).isNotEmpty,
|
||||
))
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: FilledButton.tonalIcon(
|
||||
onPressed: () => _showFixDialog(issues, theme),
|
||||
icon: const Icon(Icons.auto_fix_high, size: 14),
|
||||
label: Text(
|
||||
widget.strings.diagnosticFixButton,
|
||||
style: _monoTextStyle(size: 11),
|
||||
),
|
||||
style: FilledButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 4,
|
||||
),
|
||||
minimumSize: const Size(0, 28),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: widget.strings.diagnosticCopyAll,
|
||||
icon: const Icon(Icons.content_copy, size: 14),
|
||||
|
|
@ -1818,3 +1863,198 @@ class _IssueHoverCard extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// "Fix flow issues" modal — opens from the diagnostic strip's
|
||||
/// header "Beheben" button. Lists each issue with its
|
||||
/// available quick fixes and an "apply every quick fix"
|
||||
/// master action. Replaces the inline expand-strip workflow
|
||||
/// for operators who prefer a deliberate dialog over scrolling
|
||||
/// in a 200-px-tall footer.
|
||||
class _FixDialog extends StatefulWidget {
|
||||
final List<Issue> issues;
|
||||
final FlowYamlCodeController controller;
|
||||
final FlowEditorStrings strings;
|
||||
final Color Function(IssueType, ThemeData) toneForIssue;
|
||||
final Future<void> Function(QuickFix) onApplyFix;
|
||||
|
||||
const _FixDialog({
|
||||
required this.issues,
|
||||
required this.controller,
|
||||
required this.strings,
|
||||
required this.toneForIssue,
|
||||
required this.onApplyFix,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_FixDialog> createState() => _FixDialogState();
|
||||
}
|
||||
|
||||
class _FixDialogState extends State<_FixDialog> {
|
||||
bool _applyingAll = false;
|
||||
|
||||
Future<void> _applyAll() async {
|
||||
setState(() => _applyingAll = true);
|
||||
try {
|
||||
for (final issue in widget.issues) {
|
||||
final fixes = widget.controller.fixesFor(issue);
|
||||
if (fixes.isEmpty) continue;
|
||||
// Pick the first fix per issue — the analyzer orders
|
||||
// Replace-fixes before Install/AddSource, so a typo
|
||||
// suggestion wins over a remote install round-trip.
|
||||
await widget.onApplyFix(fixes.first);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _applyingAll = false);
|
||||
}
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final strings = widget.strings;
|
||||
final fixCount = widget.issues
|
||||
.where((i) => widget.controller.fixesFor(i).isNotEmpty)
|
||||
.length;
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.auto_fix_high,
|
||||
size: 20,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(strings.diagnosticFixDialogTitle),
|
||||
],
|
||||
),
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 640, maxHeight: 480),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
strings.diagnosticFixDialogBody(widget.issues.length),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
for (final issue in widget.issues)
|
||||
_FixDialogRow(
|
||||
issue: issue,
|
||||
fixes: widget.controller.fixesFor(issue),
|
||||
tone: widget.toneForIssue(issue.type, theme),
|
||||
strings: strings,
|
||||
onApplyFix: (fix) async {
|
||||
await widget.onApplyFix(fix);
|
||||
if (mounted) setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (fixCount > 1)
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: _applyingAll ? null : _applyAll,
|
||||
icon: _applyingAll
|
||||
? const SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.auto_fix_high, size: 16),
|
||||
label: Text(strings.diagnosticFixApplyAll(fixCount)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(strings.diagnosticFixDialogClose),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FixDialogRow extends StatelessWidget {
|
||||
final Issue issue;
|
||||
final List<QuickFix> fixes;
|
||||
final Color tone;
|
||||
final FlowEditorStrings strings;
|
||||
final Future<void> Function(QuickFix) onApplyFix;
|
||||
|
||||
const _FixDialogRow({
|
||||
required this.issue,
|
||||
required this.fixes,
|
||||
required this.tone,
|
||||
required this.strings,
|
||||
required this.onApplyFix,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border(left: BorderSide(color: tone, width: 3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
strings.diagnosticLinePrefix(issue.line + 1),
|
||||
style: _monoTextStyle(
|
||||
size: 11,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: SelectableText(
|
||||
issue.message,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (fixes.isEmpty)
|
||||
Text(
|
||||
strings.diagnosticNoFixesAvailable,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
fontStyle: FontStyle.italic,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
else
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: [
|
||||
for (final fix in fixes)
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () => onApplyFix(fix),
|
||||
icon: const Icon(Icons.auto_fix_high, size: 13),
|
||||
label: Text(
|
||||
fix.label,
|
||||
style: _monoTextStyle(size: 11),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue