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
|
|
@ -41,6 +41,7 @@ import 'tokens.dart';
|
|||
import 'widgets.dart';
|
||||
import 'widgets/capability_picker.dart';
|
||||
import 'widgets/flow_canvas.dart';
|
||||
import 'widgets/missing_modules_badge.dart';
|
||||
import 'widgets/properties_panel.dart';
|
||||
import 'widgets/run_tab.dart';
|
||||
|
||||
|
|
@ -591,6 +592,7 @@ outputs:
|
|||
installedNames: _installedNames(
|
||||
widget.availableCapabilities,
|
||||
),
|
||||
storeNames: _installedNames(widget.storeCapabilities),
|
||||
onOpen: _openFile,
|
||||
onRefresh: _refreshFiles,
|
||||
onStart: _startFile,
|
||||
|
|
@ -1347,6 +1349,12 @@ class _FileList extends StatefulWidget {
|
|||
/// Bare capability NAMES the host reports as installed. Used
|
||||
/// to compute each row's missing-module count.
|
||||
final Set<String> installedNames;
|
||||
|
||||
/// Bare capability NAMES a configured store can install
|
||||
/// (host-filtered to installable entries). Missing caps
|
||||
/// outside this set render the "not in store" state instead
|
||||
/// of an install action that the hub would refuse.
|
||||
final Set<String> storeNames;
|
||||
final void Function(_FlowFile) onOpen;
|
||||
final VoidCallback onRefresh;
|
||||
|
||||
|
|
@ -1365,6 +1373,7 @@ class _FileList extends StatefulWidget {
|
|||
required this.activeName,
|
||||
required this.strings,
|
||||
required this.installedNames,
|
||||
required this.storeNames,
|
||||
required this.onOpen,
|
||||
required this.onRefresh,
|
||||
required this.onStart,
|
||||
|
|
@ -1502,6 +1511,7 @@ class _FileListState extends State<_FileList> {
|
|||
final f = files[i];
|
||||
final isActive = f.name == widget.activeName;
|
||||
final missing = f.meta.missingCaps(widget.installedNames);
|
||||
final split = splitMissingCaps(missing, widget.storeNames);
|
||||
return InkWell(
|
||||
onTap: () => widget.onOpen(f),
|
||||
child: Container(
|
||||
|
|
@ -1579,13 +1589,20 @@ class _FileListState extends State<_FileList> {
|
|||
if (f.meta.isExample)
|
||||
_ExampleBadge(strings: strings),
|
||||
if (missing.isNotEmpty)
|
||||
_MissingModulesBadge(
|
||||
missing: missing,
|
||||
MissingModulesBadge(
|
||||
installable: split.installable,
|
||||
notInStore: split.notInStore,
|
||||
strings: strings,
|
||||
onInstall: widget.onInstallMissing == null
|
||||
// Install only what the store
|
||||
// resolves — the not-in-store
|
||||
// chip explains the rest.
|
||||
onInstall:
|
||||
widget.onInstallMissing == null ||
|
||||
split.installable.isEmpty
|
||||
? null
|
||||
: () =>
|
||||
widget.onInstallMissing!(missing),
|
||||
: () => widget.onInstallMissing!(
|
||||
split.installable,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -1665,95 +1682,6 @@ class _ExampleBadge extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// "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.
|
||||
class _MissingModulesBadge extends StatelessWidget {
|
||||
final List<String> missing;
|
||||
final FlowEditorStrings strings;
|
||||
final VoidCallback? onInstall;
|
||||
const _MissingModulesBadge({
|
||||
required this.missing,
|
||||
required this.strings,
|
||||
required this.onInstall,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
const dot = Color(0xFFEF6C00);
|
||||
final fg = theme.colorScheme.onSurfaceVariant;
|
||||
final chip = Tooltip(
|
||||
message: strings.flowListNeedsModulesTooltip(missing.join('\n')),
|
||||
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: const BoxDecoration(
|
||||
color: dot,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
strings.flowListNeedsModules(missing.length),
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: fg,
|
||||
fontSize: 10,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
if (onInstall == null) return chip;
|
||||
return Wrap(
|
||||
spacing: FaiSpace.xs,
|
||||
runSpacing: FaiSpace.xs,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
chip,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- empty state ---
|
||||
|
||||
class _EmptyState extends StatelessWidget {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue