feat(run): host-injected native file picker for flow file inputs

The Run tab asked the operator to type an absolute path into a text
field — with no file dialog anywhere. FlowEditorPage now accepts an
onPickFile callback (PickedFileData: name + bytes) that hosts wire
to their native picker; the path-entry dialog remains only as the
no-host fallback. 0.23.0.

Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
flemming-it 2026-07-17 01:04:40 +02:00
parent 9e73036d0e
commit ae09a55146
6 changed files with 52 additions and 6 deletions

View file

@ -40,6 +40,8 @@ export 'src/run_driver.dart'
show
FlowRunDriver,
FlowRunEvent,
PickedFileData,
PickFileCallback,
StepStarted,
StepCompleted,
StepFailed,

View file

@ -101,6 +101,11 @@ class FlowEditorPage extends StatefulWidget {
/// silent no install button offered.
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
/// path-entry fallback.
final PickFileCallback? onPickFile;
/// The host's active workspace/project slug (empty = "all
/// projects", no active workspace). Used only to flag a
/// mismatch when the open flow file declares a *different*
@ -126,6 +131,7 @@ class FlowEditorPage extends StatefulWidget {
this.storeCapabilities = const [],
this.activeProject = '',
this.onSwitchToFileProject,
this.onPickFile,
});
@override
@ -658,6 +664,7 @@ outputs:
controller: _controller,
strings: _l,
driver: widget.runDriver,
onPickFile: widget.onPickFile,
),
],
),

View file

@ -19,6 +19,19 @@
import 'dart:typed_data';
/// A file chosen by the host's native picker for a flow file input.
class PickedFileData {
final String fileName;
final Uint8List bytes;
const PickedFileData({required this.fileName, required this.bytes});
}
/// Host-side native file picker. Returns null when the operator
/// cancels. When a host does not supply one, the Run tab falls back
/// to its manual path-entry dialog.
typedef PickFileCallback = Future<PickedFileData?> Function();
abstract class FlowRunDriver {
/// Trigger a run of [flowName] with the supplied inputs.
/// Text + file inputs come in separate maps so the host

View file

@ -36,11 +36,17 @@ class RunTab extends StatefulWidget {
final FlowEditorController controller;
final FlowEditorStrings strings;
final FlowRunDriver? driver;
/// Host-side native file picker; null keeps the manual
/// path-entry fallback dialog.
final PickFileCallback? onPickFile;
const RunTab({
super.key,
required this.controller,
required this.strings,
this.driver,
this.onPickFile,
});
@override
@ -469,11 +475,21 @@ class _RunTabState extends State<RunTab> {
}
Future<void> _pickFile(String name) async {
// For 0.2.0 the editor relies on the host to inject a
// file-picker via callback; minimal version uses
// dart:io directly for desktop. Studio uses file_picker;
// we fall back to a manual path entry dialog if no host
// picker is available.
// Host-injected native picker first typing an absolute path
// by hand is a last resort, not an input method.
final hostPick = widget.onPickFile;
if (hostPick != null) {
final picked = await hostPick();
if (picked == null || !mounted) return;
setState(() {
_fileInputs[name] = _FilePick(
fileName: picked.fileName,
bytes: picked.bytes,
);
});
return;
}
// Fallback without a host picker: manual path entry.
final controller = TextEditingController();
final ctx = context;
final result = await showDialog<String>(