feat(studio): operator QoL — flow runnability, audit grouping, batch approvals, welcome celebration (v0.42.0)
Four UX threads stitched into one commit. Each pulls Studio toward Stefan's "zero-learning-curve" goal — the feedback that earned its own memory entry. 1. Flow-Runnability-Indikator ───────────────────────── The Flows tab now fetches `listFlows` and `listModules` in parallel. Each card compares the flow's `requiredCapabilities` against the installed-modules' capability set; rows with missing modules show a "Needs: text.extract@^0" red pill row beneath the path and have their Run button greyed out + tooltip "Install the missing modules first." Operators stop hitting Run → cryptic hub error → frustration. 2. Welcome-Checklist Celebration ───────────────────────────── Once all four checklist signals flip to done, an `_AllDoneCelebration` card replaces the bare "All four steps complete" + Hide button. Three concrete next-threads with action buttons: "Read the audit log", "Set up the daily Today story" (opens the Flows / Today doc inline via `_DocReaderSheet`), and "Build your own module" (opens the architecture doc). Operator who just got set up sees what to do next instead of an empty "what now?" feeling. 3. Audit-Page Time-Bucket Headers + Flow-Run Detail ──────────────────────────────────────────────── The flat event list grows tiny "TODAY / YESTERDAY / EARLIER THIS WEEK / OLDER" section headers — bucket is computed in the operator's local timezone so an event at 23:55 yesterday in Berlin doesn't end up in "today" because UTC happened to spill into a new day. Plus: the event-detail dialog gains a "View flow run" action when the picked event has a `flow_execution`. It opens a drill-down that lists every event in the already-fetched 100-event window sharing the same execution id, sorted ascending — the operator reads the run from step.started top to flow.completed bottom. 4. Approvals-Batch-Aktionen ──────────────────────── Each pending approval card grows a checkbox. When ≥1 selected, a floating action bar appears at the bottom with "N selected · Select all · Clear · Reject all · Approve all". The parent loops sequentially through the per-record SDK calls so a partial failure produces "X done, Y failed" instead of a confusing all-or-nothing rollback. Reject prompts for a reason once and applies to the whole picked set. 13 new ARB keys cover the strings the four features needed. Studio's tests stay green. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
f90d8cc7a7
commit
2002486828
13 changed files with 1215 additions and 64 deletions
|
|
@ -535,28 +535,151 @@ class _OnboardingChecklistState extends State<_OnboardingChecklist> {
|
|||
),
|
||||
if (allDone) ...[
|
||||
const SizedBox(height: FaiSpace.md),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
size: 16,
|
||||
color: FaiColors.success,
|
||||
_AllDoneCelebration(onDismiss: _dismiss),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Shown beneath the four-step checklist once every signal
|
||||
/// flips to done. Three concrete next-threads the operator
|
||||
/// can pull on, plus the dismiss button. Each suggestion
|
||||
/// links somewhere meaningful inside Studio (Audit page,
|
||||
/// Today doc, Flows doc) — no external browser calls.
|
||||
class _AllDoneCelebration extends StatelessWidget {
|
||||
final VoidCallback onDismiss;
|
||||
const _AllDoneCelebration({required this.onDismiss});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(FaiSpace.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: FaiColors.success.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(FaiRadius.md),
|
||||
border: Border.all(
|
||||
color: FaiColors.success.withValues(alpha: 0.35),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.celebration_outlined, color: FaiColors.success),
|
||||
const SizedBox(width: FaiSpace.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l.welcomeChecklistAllSetTitle,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: FaiSpace.xs),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: onDismiss,
|
||||
child: Text(l.welcomeChecklistDismiss),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
l.welcomeChecklistAllSetBody,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: FaiSpace.md),
|
||||
_NextStepRow(
|
||||
icon: Icons.timeline_outlined,
|
||||
title: l.welcomeChecklistNextAuditTitle,
|
||||
body: l.welcomeChecklistNextAuditBody,
|
||||
buttonLabel: l.welcomeChecklistNextAuditButton,
|
||||
doc: null,
|
||||
onOpenDoc: null,
|
||||
),
|
||||
_NextStepRow(
|
||||
icon: Icons.auto_awesome,
|
||||
title: l.welcomeChecklistNextTodayTitle,
|
||||
body: l.welcomeChecklistNextTodayBody,
|
||||
buttonLabel: l.welcomeChecklistNextTodayButton,
|
||||
doc: _kDocs.firstWhere(
|
||||
(d) => d.slug == 'flows',
|
||||
orElse: () => _kDocs.first,
|
||||
),
|
||||
onOpenDoc: (entry) => _DocReaderSheet.show(context, entry),
|
||||
),
|
||||
_NextStepRow(
|
||||
icon: Icons.extension_outlined,
|
||||
title: l.welcomeChecklistNextModuleTitle,
|
||||
body: l.welcomeChecklistNextModuleBody,
|
||||
buttonLabel: l.welcomeChecklistNextModuleButton,
|
||||
doc: _kDocs.first,
|
||||
onOpenDoc: (entry) => _DocReaderSheet.show(context, entry),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NextStepRow extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String body;
|
||||
final String buttonLabel;
|
||||
final _DocEntry? doc;
|
||||
final void Function(_DocEntry)? onOpenDoc;
|
||||
|
||||
const _NextStepRow({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.body,
|
||||
required this.buttonLabel,
|
||||
required this.doc,
|
||||
required this.onOpenDoc,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final canActivate = doc != null && onOpenDoc != null;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: FaiSpace.sm),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(icon, size: 18, color: theme.colorScheme.primary),
|
||||
const SizedBox(width: FaiSpace.md),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l.welcomeChecklistAllDone,
|
||||
title,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
body,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
FilledButton.tonal(
|
||||
onPressed: _dismiss,
|
||||
child: Text(l.welcomeChecklistDismiss),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: FaiSpace.md),
|
||||
OutlinedButton(
|
||||
onPressed: canActivate ? () => onOpenDoc!(doc!) : null,
|
||||
child: Text(buttonLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue