fix: never claim 'not in store' while the store state is unknown
A failed/unloaded store snapshot used to be indistinguishable from a known-empty store, so every missing capability was labelled 'not in store' the moment the hub or store endpoint was unreachable — a wrong claim. storeCapabilities is now nullable (null = unknown): missing caps then get the plain missing chip with an honest tooltip, no install offer and no not-in-store claim; the analyzer message says the store cannot be checked right now (EN+DE). Split and badge covered by new unit + widget tests. Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
parent
c4a39a3779
commit
2535c28fce
6 changed files with 131 additions and 34 deletions
|
|
@ -38,13 +38,14 @@ class FlowAnalyzer extends AbstractAnalyzer {
|
|||
/// rebuild.
|
||||
final List<String> Function() availableCapabilities;
|
||||
|
||||
/// Returns the names of capabilities the public store knows
|
||||
/// how to install. Used to decide whether an unknown-cap
|
||||
/// issue should carry an Install button — clicking the
|
||||
/// button on a capability the hub can't actually fetch would
|
||||
/// just fail. Null = "no store available" → install offered
|
||||
/// for every unknown cap (legacy behaviour).
|
||||
final List<String> Function()? storeCapabilities;
|
||||
/// Returns the names of capabilities the store can actually
|
||||
/// install, or null when the store state is UNKNOWN (snapshot
|
||||
/// not loaded / store unreachable). Drives whether an
|
||||
/// unknown-cap issue carries an Install button (in store), the
|
||||
/// "not in store" recovery message (known, absent) or the
|
||||
/// neutral store-unknown wording (null) — the analyzer never
|
||||
/// claims "no store provides it" without a loaded snapshot.
|
||||
final List<String>? Function()? storeCapabilities;
|
||||
|
||||
/// Quick fixes attached to the most-recent analyze() pass.
|
||||
/// Keyed by the same `Issue` instances that landed in
|
||||
|
|
@ -111,9 +112,10 @@ class FlowAnalyzer extends AbstractAnalyzer {
|
|||
// as Did-you-mean candidates so the suggestion can preserve
|
||||
// the version constraint when the user already typed one.
|
||||
final installedFull = caps.toSet();
|
||||
final storeCaps =
|
||||
storeCapabilities?.call() ?? const <String>[];
|
||||
final storeBare = storeCaps.map(_bareCap).toSet();
|
||||
final storeCaps = storeCapabilities?.call();
|
||||
final storeKnown = storeCaps != null;
|
||||
final storeBare =
|
||||
(storeCaps ?? const <String>[]).map(_bareCap).toSet();
|
||||
|
||||
YamlNode? doc;
|
||||
try {
|
||||
|
|
@ -175,7 +177,9 @@ class FlowAnalyzer extends AbstractAnalyzer {
|
|||
? strings.unknownCapInStore(useValue)
|
||||
: didYouMean != null
|
||||
? strings.unknownCapTypo(useValue, didYouMean)
|
||||
: strings.unknownCapNotInStore(useValue);
|
||||
: storeKnown
|
||||
? strings.unknownCapNotInStore(useValue)
|
||||
: strings.unknownCapStoreUnknown(useValue);
|
||||
final issue = Issue(
|
||||
line: issueLine,
|
||||
message: message,
|
||||
|
|
|
|||
|
|
@ -95,12 +95,12 @@ class FlowEditorPage extends StatefulWidget {
|
|||
/// then call the Hub install API.
|
||||
final AddModuleSourceCallback? onAddModuleSource;
|
||||
|
||||
/// Capabilities the public store knows how to install. The
|
||||
/// analyzer uses this to decide whether to show "Install …"
|
||||
/// (in store) or "Add source for …" (not in store) as the
|
||||
/// quick-fix on an unknown `use:` line. Empty list = store
|
||||
/// silent — no install button offered.
|
||||
final List<String> storeCapabilities;
|
||||
/// Capabilities the store can actually install, or null when
|
||||
/// the store state is UNKNOWN (snapshot not loaded / store
|
||||
/// unreachable). Drives the analyzer's quick-fix choice and
|
||||
/// the flow list's badge: in store → Install; known-absent →
|
||||
/// "not in store" + recovery paths; unknown → neither claim.
|
||||
final List<String>? storeCapabilities;
|
||||
|
||||
/// Host-side native file picker for the Run tab's file inputs.
|
||||
/// Studio passes a real file dialog; null keeps the manual
|
||||
|
|
@ -141,7 +141,7 @@ class FlowEditorPage extends StatefulWidget {
|
|||
this.style,
|
||||
this.onInstallCapability,
|
||||
this.onAddModuleSource,
|
||||
this.storeCapabilities = const [],
|
||||
this.storeCapabilities,
|
||||
this.activeProject = '',
|
||||
this.onSwitchToFileProject,
|
||||
this.onPickFile,
|
||||
|
|
@ -616,7 +616,9 @@ outputs:
|
|||
installedNames: _installedNames(
|
||||
widget.availableCapabilities,
|
||||
),
|
||||
storeNames: _installedNames(widget.storeCapabilities),
|
||||
storeNames: widget.storeCapabilities == null
|
||||
? null
|
||||
: _installedNames(widget.storeCapabilities!),
|
||||
activeProject: widget.activeProject,
|
||||
onOpen: _openFile,
|
||||
onRefresh: _refreshFiles,
|
||||
|
|
@ -1399,10 +1401,12 @@ class _FileList extends StatefulWidget {
|
|||
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;
|
||||
/// (host-filtered to installable entries), or null when the
|
||||
/// store state is unknown. Missing caps outside this set
|
||||
/// render the "not in store" state instead of an install
|
||||
/// action that the hub would refuse; with null neither claim
|
||||
/// is made.
|
||||
final Set<String>? storeNames;
|
||||
|
||||
/// Active workspace project slug; empty = all projects. Files
|
||||
/// without a `project:` key count as `general`.
|
||||
|
|
@ -1670,6 +1674,7 @@ class _FileListState extends State<_FileList> {
|
|||
MissingModulesBadge(
|
||||
installable: split.installable,
|
||||
notInStore: split.notInStore,
|
||||
unclassified: split.unclassified,
|
||||
strings: strings,
|
||||
// Install only what the store
|
||||
// resolves — the not-in-store
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import 'wire_colors.dart';
|
|||
class FlowYamlCodeController extends CodeController {
|
||||
FlowYamlCodeController({
|
||||
List<String> Function()? availableCapabilities,
|
||||
List<String> Function()? storeCapabilities,
|
||||
List<String>? Function()? storeCapabilities,
|
||||
AnalyzerStrings analyzerStrings = AnalyzerStrings.english,
|
||||
}) : super(
|
||||
text: '',
|
||||
|
|
@ -49,7 +49,7 @@ class FlowYamlCodeController extends CodeController {
|
|||
/// closure or stale strings inside an old FlowAnalyzer.
|
||||
void setCapabilityProviders({
|
||||
required List<String> Function() available,
|
||||
List<String> Function()? store,
|
||||
List<String>? Function()? store,
|
||||
AnalyzerStrings? strings,
|
||||
}) {
|
||||
analyzer = FlowAnalyzer(
|
||||
|
|
|
|||
|
|
@ -268,6 +268,16 @@ class FlowEditorStrings {
|
|||
'passenden Store hinzufügen oder die Anbindung (MCP/n8n) '
|
||||
'einrichten, die sie bereitstellt.',
|
||||
);
|
||||
String analyzerUnknownCapStoreUnknown(String cap) => _t(
|
||||
'Unknown capability "$cap". '
|
||||
'The store is not reachable right now, so it may or may not '
|
||||
'be installable — check the spelling, or add a local source '
|
||||
'(`chain install --link <path>`).',
|
||||
'Unbekannte Capability "$cap". '
|
||||
'Der Store ist gerade nicht erreichbar — ob sie installierbar '
|
||||
'ist, lässt sich nicht sagen. Tippfehler prüfen oder lokale '
|
||||
'Quelle hinzufügen (`chain install --link <pfad>`).',
|
||||
);
|
||||
String analyzerInputKind() => _t('input', 'Eingabe');
|
||||
String analyzerOutputKind() => _t('output', 'Ausgabe');
|
||||
String analyzerUnknownType(String kind, String value, String validList) =>
|
||||
|
|
@ -295,6 +305,14 @@ class FlowEditorStrings {
|
|||
'Klicken, um die fehlenden zu installieren.',
|
||||
);
|
||||
String get flowListInstallMissing => _t('Install', 'Installieren');
|
||||
String flowListNeedsModulesTooltipNoAction(String caps) => _t(
|
||||
'This flow needs capabilities that are not installed:\n$caps\n'
|
||||
'The store is not reachable right now — no install offer '
|
||||
'until Studio can check it.',
|
||||
'Dieser Flow braucht nicht installierte Capabilities:\n$caps\n'
|
||||
'Der Store ist gerade nicht erreichbar — kein Install-Angebot, '
|
||||
'bis Studio das prüfen kann.',
|
||||
);
|
||||
String flowListNotInStore(int n) => _t(
|
||||
n == 1 ? 'not in store' : '$n not in store',
|
||||
n == 1 ? 'nicht im Store' : '$n nicht im Store',
|
||||
|
|
@ -352,6 +370,7 @@ class AnalyzerStrings {
|
|||
final String Function(String cap) unknownCapInStore;
|
||||
final String Function(String cap, String suggestion) unknownCapTypo;
|
||||
final String Function(String cap) unknownCapNotInStore;
|
||||
final String Function(String cap) unknownCapStoreUnknown;
|
||||
final String Function(String kind, String value, String validList)
|
||||
unknownType;
|
||||
final String Function() inputKind;
|
||||
|
|
@ -367,6 +386,7 @@ class AnalyzerStrings {
|
|||
required this.unknownCapInStore,
|
||||
required this.unknownCapTypo,
|
||||
required this.unknownCapNotInStore,
|
||||
required this.unknownCapStoreUnknown,
|
||||
required this.unknownType,
|
||||
required this.inputKind,
|
||||
required this.outputKind,
|
||||
|
|
@ -385,6 +405,7 @@ class AnalyzerStrings {
|
|||
unknownCapInStore: s.analyzerUnknownCapInStore,
|
||||
unknownCapTypo: s.analyzerUnknownCapTypo,
|
||||
unknownCapNotInStore: s.analyzerUnknownCapNotInStore,
|
||||
unknownCapStoreUnknown: s.analyzerUnknownCapStoreUnknown,
|
||||
unknownType: s.analyzerUnknownType,
|
||||
inputKind: s.analyzerInputKind,
|
||||
outputKind: s.analyzerOutputKind,
|
||||
|
|
@ -405,6 +426,7 @@ class AnalyzerStrings {
|
|||
: unknownCapInStore = _enUnknownCapInStore,
|
||||
unknownCapTypo = _enUnknownCapTypo,
|
||||
unknownCapNotInStore = _enUnknownCapNotInStore,
|
||||
unknownCapStoreUnknown = _enUnknownCapStoreUnknown,
|
||||
unknownType = _enUnknownType,
|
||||
inputKind = _enInputKind,
|
||||
outputKind = _enOutputKind,
|
||||
|
|
@ -424,6 +446,11 @@ class AnalyzerStrings {
|
|||
'No configured store can install it — install a local module '
|
||||
'(`chain install --link <path>`), add the store that provides '
|
||||
'it, or configure the integration (MCP/n8n) that supplies it.';
|
||||
static String _enUnknownCapStoreUnknown(String cap) =>
|
||||
'Unknown capability "$cap". '
|
||||
'The store is not reachable right now, so it may or may not '
|
||||
'be installable — check the spelling, or add a local source '
|
||||
'(`chain install --link <path>`).';
|
||||
static String _enUnknownType(String kind, String value, String validList) =>
|
||||
'Unknown $kind type "$value". Use one of: $validList.';
|
||||
static String _enInputKind() => 'input';
|
||||
|
|
|
|||
|
|
@ -13,22 +13,39 @@ import '../tokens.dart';
|
|||
///
|
||||
/// [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(
|
||||
/// — published/alpha, native). An empty set means "nothing is
|
||||
/// store-installable" (known state); `null` means the store state
|
||||
/// is UNKNOWN (snapshot not loaded / unreachable) — then every
|
||||
/// missing cap lands in `unclassified` and the UI claims neither
|
||||
/// "installable" nor "not in store".
|
||||
({
|
||||
List<String> installable,
|
||||
List<String> notInStore,
|
||||
List<String> unclassified,
|
||||
}) splitMissingCaps(
|
||||
List<String> missing,
|
||||
Set<String> storeNames,
|
||||
Set<String>? storeNames,
|
||||
) {
|
||||
if (storeNames == null) {
|
||||
// Store state unknown (snapshot not loaded / unreachable):
|
||||
// claim neither "installable" nor "not in store".
|
||||
return (
|
||||
installable: const <String>[],
|
||||
notInStore: const <String>[],
|
||||
unclassified: missing,
|
||||
);
|
||||
}
|
||||
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);
|
||||
return (
|
||||
installable: installable,
|
||||
notInStore: notInStore,
|
||||
unclassified: const <String>[],
|
||||
);
|
||||
}
|
||||
|
||||
/// "N modules missing" status on flows whose steps reference
|
||||
|
|
@ -48,10 +65,13 @@ import '../tokens.dart';
|
|||
/// 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.
|
||||
/// hub's "no store entry" error. [unclassified] capabilities
|
||||
/// (store state unknown) get the missing chip with neither an
|
||||
/// install offer nor a not-in-store claim.
|
||||
class MissingModulesBadge extends StatelessWidget {
|
||||
final List<String> installable;
|
||||
final List<String> notInStore;
|
||||
final List<String> unclassified;
|
||||
final FlowEditorStrings strings;
|
||||
final VoidCallback? onInstall;
|
||||
const MissingModulesBadge({
|
||||
|
|
@ -60,6 +80,7 @@ class MissingModulesBadge extends StatelessWidget {
|
|||
required this.notInStore,
|
||||
required this.strings,
|
||||
required this.onInstall,
|
||||
this.unclassified = const [],
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -92,6 +113,15 @@ class MissingModulesBadge extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (unclassified.isNotEmpty)
|
||||
_chip(
|
||||
theme,
|
||||
dot: const Color(0xFFEF6C00),
|
||||
label: strings.flowListNeedsModules(unclassified.length),
|
||||
tooltip: strings.flowListNeedsModulesTooltipNoAction(
|
||||
unclassified.join('\n'),
|
||||
),
|
||||
),
|
||||
if (notInStore.isNotEmpty)
|
||||
_chip(
|
||||
theme,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue