feat: honest install badge — classify store resolvability before the click
Split each flow's missing capabilities into store-resolvable ones (amber chip + Install link, which now installs only those) and a quiet 'not in store' chip whose tooltip explains the three recovery paths in place: local install via chain install --link, adding the providing store, or configuring the MCP/n8n integration. The analyzer's not-in-store message names the same three paths (EN+DE). Previously the list's Install action covered every missing capability and could end in the hub's 'no store entry' error. Also replace a private-looking capability example name in a doc comment and test with a neutral placeholder. (0.25.0) Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
parent
7d6a575cae
commit
ab97e5e834
8 changed files with 340 additions and 105 deletions
154
lib/src/widgets/missing_modules_badge.dart
Normal file
154
lib/src/widgets/missing_modules_badge.dart
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
// Flow-list badge for flows whose steps reference capabilities
|
||||
// the hub doesn't provide. Split out of flow_editor_page.dart so
|
||||
// the resolvability states (installable from a store vs. not in
|
||||
// any store) stay unit-testable.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../l10n.dart';
|
||||
import '../tokens.dart';
|
||||
|
||||
/// Split a flow's missing capabilities into the ones a configured
|
||||
/// store can actually install and the ones no store resolves.
|
||||
///
|
||||
/// [storeNames] is the host-supplied set of bare capability names
|
||||
/// the store can install (already filtered to installable entries
|
||||
/// — published/alpha, native). An empty set therefore means
|
||||
/// "nothing is store-installable", not "unknown": the honest
|
||||
/// state without store data is the not-in-store explanation, and
|
||||
/// the local-install / add-store / configure-integration paths
|
||||
/// remain available.
|
||||
({List<String> installable, List<String> notInStore}) splitMissingCaps(
|
||||
List<String> missing,
|
||||
Set<String> storeNames,
|
||||
) {
|
||||
final installable = <String>[];
|
||||
final notInStore = <String>[];
|
||||
for (final cap in missing) {
|
||||
final bare = cap.split('@').first;
|
||||
(storeNames.contains(bare) ? installable : notInStore).add(cap);
|
||||
}
|
||||
return (installable: installable, notInStore: notInStore);
|
||||
}
|
||||
|
||||
/// "N modules missing" status on flows whose steps reference
|
||||
/// capabilities the hub doesn't provide. Status and action are
|
||||
/// SEPARATE elements (usertest: the combined orange chip made
|
||||
/// the whole list read like an error wall and ellipsized the
|
||||
/// action word):
|
||||
/// - status = quiet neutral chip with a small amber dot — a
|
||||
/// note, not an alarm; warning orange stays reserved for real
|
||||
/// failures.
|
||||
/// - action = its own "Install" link that is never truncated;
|
||||
/// both live in the row's Wrap, so tight widths wrap to a
|
||||
/// second line instead of cutting text mid-word.
|
||||
///
|
||||
/// The install action only covers [installable] — capabilities a
|
||||
/// configured store resolves. [notInStore] capabilities get their
|
||||
/// own quiet chip whose tooltip explains the three recovery paths
|
||||
/// (local install / add store / configure integration) BEFORE any
|
||||
/// click, instead of an install button that would end in the
|
||||
/// hub's "no store entry" error.
|
||||
class MissingModulesBadge extends StatelessWidget {
|
||||
final List<String> installable;
|
||||
final List<String> notInStore;
|
||||
final FlowEditorStrings strings;
|
||||
final VoidCallback? onInstall;
|
||||
const MissingModulesBadge({
|
||||
super.key,
|
||||
required this.installable,
|
||||
required this.notInStore,
|
||||
required this.strings,
|
||||
required this.onInstall,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final children = <Widget>[
|
||||
if (installable.isNotEmpty)
|
||||
_chip(
|
||||
theme,
|
||||
dot: const Color(0xFFEF6C00),
|
||||
label: strings.flowListNeedsModules(installable.length),
|
||||
tooltip: strings.flowListNeedsModulesTooltip(
|
||||
installable.join('\n'),
|
||||
),
|
||||
),
|
||||
if (installable.isNotEmpty && onInstall != null)
|
||||
InkWell(
|
||||
onTap: onInstall,
|
||||
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
child: Text(
|
||||
strings.flowListInstallMissing,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (notInStore.isNotEmpty)
|
||||
_chip(
|
||||
theme,
|
||||
// Muted dot: this state has no one-click fix, so it
|
||||
// must not borrow the actionable chip's amber.
|
||||
dot: theme.colorScheme.outline,
|
||||
label: strings.flowListNotInStore(notInStore.length),
|
||||
tooltip: strings.flowListNotInStoreTooltip(notInStore.join('\n')),
|
||||
),
|
||||
];
|
||||
if (children.length == 1) return children.single;
|
||||
return Wrap(
|
||||
spacing: FaiSpace.xs,
|
||||
runSpacing: FaiSpace.xs,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _chip(
|
||||
ThemeData theme, {
|
||||
required Color dot,
|
||||
required String label,
|
||||
required String tooltip,
|
||||
}) {
|
||||
final fg = theme.colorScheme.onSurfaceVariant;
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.outlineVariant.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(color: dot, shape: BoxShape.circle),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: fg,
|
||||
fontSize: 10,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue