feat(settings): multi-version uninstall picker + default_scope editor
Some checks failed
Security / Security check (push) Failing after 2s
Some checks failed
Security / Security check (push) Failing after 2s
Two operator surfaces shipped together:
**Multi-version uninstall picker** — when more than one version
of a `(provider, name)` is installed side-by-side, both the
module-sheet "Uninstall" affordance and the store-detail
"Uninstall" affordance now ask the operator which version to
remove before calling the RPC. The hub's wire-level support for
this (UninstallModuleRequest.version) was already there; Studio
just wasn't using it. Picker pre-selects the highest version so
single-version flows still take one click.
- `HubService.installedVersions(name)` enumerates the installed
versions via the capabilities list.
- `HubService.uninstallModule(name, version: ...)` forwards
the version into the RPC.
- `_UninstallVersionPickerDialog` (module sheet) and
`_StoreUninstallVersionPickerDialog` (store) host the
picker — separate widgets so each surface can evolve copy
independently. Uses `RadioGroup<String>` for Flutter
3.32+ deprecation compliance.
**Default scope editor** — new DEFAULT SCOPE panel in Settings
that calls the freshly-added HubAdmin RPCs
`GetDefaultScope` / `SetDefaultScope`. Operators can:
- reorder publisher segments with up/down buttons
(first match wins in the bare-form resolver),
- delete entries (hub still rejects empty list — Studio
surfaces the constraint inline),
- add arbitrary entries via the text field,
- add catalog-known publishers via suggestion chips
(sorted alphabetically, populated from the catalog).
Every change persists to `~/.fai/config.yaml` via the hub
and hot-swaps the in-memory copy without a daemon restart.
Bumped pubspec to 0.48.0. dart analyze clean (No issues
found!); flutter test green (11 tests).
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
327bc0fea2
commit
1903babc5a
10 changed files with 750 additions and 7 deletions
|
|
@ -50,11 +50,32 @@ class _FaiModuleSheetState extends State<FaiModuleSheet> {
|
|||
|
||||
Future<void> _confirmAndUninstall(ModuleDetail detail) async {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
// When more than one version of the same module is
|
||||
// installed side-by-side, ask the operator which one to
|
||||
// remove. The hub's uninstall RPC otherwise falls back to
|
||||
// "remove the highest version", which is rarely what an
|
||||
// operator means when they've explicitly kept multiple
|
||||
// versions around.
|
||||
final versions =
|
||||
await HubService.instance.installedVersions(detail.name);
|
||||
if (!mounted) return;
|
||||
String? targetVersion;
|
||||
if (versions.length > 1) {
|
||||
targetVersion = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (ctx) => _UninstallVersionPickerDialog(
|
||||
moduleName: detail.name,
|
||||
versions: versions,
|
||||
),
|
||||
);
|
||||
if (targetVersion == null || !mounted) return;
|
||||
}
|
||||
final displayVersion = targetVersion ?? detail.version;
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(l.modulesUninstallTitle),
|
||||
content: Text(l.modulesUninstallBody(detail.name, detail.version)),
|
||||
content: Text(l.modulesUninstallBody(detail.name, displayVersion)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
|
|
@ -74,7 +95,10 @@ class _FaiModuleSheetState extends State<FaiModuleSheet> {
|
|||
if (ok != true || !mounted) return;
|
||||
setState(() => _uninstalling = true);
|
||||
try {
|
||||
final r = await HubService.instance.uninstallModule(detail.name);
|
||||
final r = await HubService.instance.uninstallModule(
|
||||
detail.name,
|
||||
version: targetVersion ?? '',
|
||||
);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
|
|
@ -299,6 +323,85 @@ class _Body extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// Modal that asks the operator to pick which installed
|
||||
/// version to uninstall when more than one version of the same
|
||||
/// `(provider, name)` is on disk side-by-side. Pops the picked
|
||||
/// version string, or `null` on cancel.
|
||||
class _UninstallVersionPickerDialog extends StatefulWidget {
|
||||
final String moduleName;
|
||||
final List<String> versions;
|
||||
|
||||
const _UninstallVersionPickerDialog({
|
||||
required this.moduleName,
|
||||
required this.versions,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_UninstallVersionPickerDialog> createState() =>
|
||||
_UninstallVersionPickerDialogState();
|
||||
}
|
||||
|
||||
class _UninstallVersionPickerDialogState
|
||||
extends State<_UninstallVersionPickerDialog> {
|
||||
late String _picked;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Default to the highest version — same fallback the hub
|
||||
// would pick without an explicit version, so the operator
|
||||
// can confirm with one click in the common case.
|
||||
_picked = widget.versions.last;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
return AlertDialog(
|
||||
title: Text(l.uninstallVersionPickerTitle),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(l.uninstallVersionPickerBody(widget.moduleName)),
|
||||
const SizedBox(height: FaiSpace.md),
|
||||
RadioGroup<String>(
|
||||
groupValue: _picked,
|
||||
onChanged: (val) {
|
||||
if (val != null) setState(() => _picked = val);
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final v in widget.versions)
|
||||
RadioListTile<String>(
|
||||
value: v,
|
||||
title: Text(
|
||||
'v$v',
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
),
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(l.buttonCancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, _picked),
|
||||
child: Text(l.uninstallVersionPickerContinue),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionHeader extends StatelessWidget {
|
||||
final String text;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue