fix(flows): honest unknown-store state + snapshot lifecycle
Some checks are pending
Security / Security check (push) Waiting to run

Store snapshot is now nullable: until the first successful store
search (or after a sealed-area connection switch) the editor gets
null and claims neither 'installable' nor 'not in store'. The
snapshot reloads after installs and on connection switches — a
sealed switch previously kept the other hub's capability offers
alive on the mounted Flows page. Editor pin 2535c28.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-07-22 14:02:54 +02:00
parent ae46258ff7
commit 642d8feb1d
2 changed files with 44 additions and 7 deletions

View file

@ -10,6 +10,8 @@
// package so a swap is one pubspec change Studio doesn't
// need to know what's inside the editor any more.
import 'dart:async' show unawaited;
import 'package:chain_studio_flow_editor/chain_studio_flow_editor.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
@ -40,10 +42,17 @@ class _FlowsPageState extends State<FlowsPage> {
late final StudioFlowRunDriver _driver;
/// Snapshot of bare capability names the public store can
/// install. Refreshed once at open so the editor's analyzer
/// can decide between "Install" (in store) and "Add source"
/// (not in store) without a per-keystroke network call.
List<String> _storeCaps = const [];
/// install; null while the store state is UNKNOWN (not yet
/// loaded / search failed) so the editor claims neither
/// "installable" nor "not in store". Refreshed at open, after
/// an install and on a connection switch.
List<String>? _storeCaps;
/// Which hub connection the snapshots were loaded against
/// the sealed-area slug, or empty for the shared hub. A sealed
/// switch keeps this page mounted, so without the guard the
/// editor would keep offering the OTHER hub's capabilities.
String _connKey = Workspace.instance.activeSealed?.slug ?? '';
@override
void initState() {
@ -51,6 +60,28 @@ class _FlowsPageState extends State<FlowsPage> {
_driver = StudioFlowRunDriver();
_capabilities = _loadCapabilities();
_loadStoreCapabilities();
Workspace.instance.addListener(_onWorkspaceChanged);
}
@override
void dispose() {
Workspace.instance.removeListener(_onWorkspaceChanged);
super.dispose();
}
/// Reload both capability snapshots when the CONNECTION target
/// changes (entering/leaving a sealed area) a plain project
/// filter change stays cheap and reuses the loaded lists.
void _onWorkspaceChanged() {
final key = Workspace.instance.activeSealed?.slug ?? '';
if (key == _connKey) return;
_connKey = key;
if (!mounted) return;
setState(() {
_storeCaps = null; // other hub snapshot unknown again
_capabilities = _loadCapabilities();
});
_loadStoreCapabilities();
}
Future<void> _loadStoreCapabilities() async {
@ -66,8 +97,10 @@ class _FlowsPageState extends State<FlowsPage> {
() => _storeCaps = installableStoreCapabilities(items).toList()..sort(),
);
} catch (_) {
// Soft-fail: empty list disables the Install button on
// unknown-cap fixes, but Add-source remains available.
// Soft-fail: keep the last snapshot for THIS connection if
// one exists; otherwise stay in the honest "unknown" state
// (no install offers, no not-in-store claims). Add-source
// remains available either way.
}
}
@ -134,6 +167,10 @@ class _FlowsPageState extends State<FlowsPage> {
Future<List<String>?> _runInstall({required String source}) async {
try {
await HubService.instance.installModule(source: source);
// The store snapshot may have moved too (an install can pull
// in new entries / the operator refreshed a store) keep the
// badge's truth in step with the capability list.
unawaited(_loadStoreCapabilities());
final caps = await HubService.instance.allCapabilities();
final updated = caps
.map((c) => '${c.capability}@${c.version}')