import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/models/models.dart'; import '../../../core/network/api_exception.dart'; import '../../../data/repositories/nodes_repository.dart'; /// Add/edit dialog for a registered [RemoteNode]. This only edits the /// registry entry (name/url/description/tags) — reachability is never /// user-editable, it's always computed live via `/nodes/aggregated`. Future showNodeFormDialog(BuildContext context, {RemoteNode? existing}) { return showDialog( context: context, builder: (context) => _NodeFormDialog(existing: existing), ); } class _NodeFormDialog extends ConsumerStatefulWidget { const _NodeFormDialog({this.existing}); final RemoteNode? existing; @override ConsumerState<_NodeFormDialog> createState() => _NodeFormDialogState(); } class _NodeFormDialogState extends ConsumerState<_NodeFormDialog> { final _formKey = GlobalKey(); late final _nameController = TextEditingController(text: widget.existing?.name); late final _urlController = TextEditingController(text: widget.existing?.url); late final _descriptionController = TextEditingController(text: widget.existing?.description); late final _tagsController = TextEditingController(text: widget.existing?.tags.join(', ')); bool _saving = false; String? _error; @override void dispose() { _nameController.dispose(); _urlController.dispose(); _descriptionController.dispose(); _tagsController.dispose(); super.dispose(); } String? _validateUrl(String? value) { final trimmed = value?.trim() ?? ''; if (trimmed.isEmpty) return 'Required.'; final uri = Uri.tryParse(trimmed); if (uri == null || !uri.hasScheme || !uri.hasAuthority) { return 'Enter a full URL, e.g. https://edge-02.local:8080'; } return null; } Future _save() async { if (!(_formKey.currentState?.validate() ?? false)) return; setState(() { _saving = true; _error = null; }); final repo = ref.read(nodesRepositoryProvider); if (repo == null) { setState(() { _saving = false; _error = 'No active connection.'; }); return; } var url = _urlController.text.trim(); if (url.endsWith('/')) url = url.substring(0, url.length - 1); final tags = _tagsController.text .split(',') .map((t) => t.trim()) .where((t) => t.isNotEmpty) .toList(); final description = _descriptionController.text.trim(); final draft = (widget.existing ?? const RemoteNode(name: '', url: '')).copyWith( name: _nameController.text.trim(), url: url, description: description.isEmpty ? null : description, tags: tags, ); try { if (widget.existing == null) { await repo.add(draft); } else { await repo.update(widget.existing!.id, draft); } ref.invalidate(remoteNodesListProvider); if (mounted) Navigator.of(context).pop(); } catch (e) { setState(() { _saving = false; _error = e is ApiException ? e.userMessage : e.toString(); }); } } @override Widget build(BuildContext context) { return AlertDialog( title: Text(widget.existing == null ? 'Register node' : 'Edit node'), content: Form( key: _formKey, child: SizedBox( width: 420, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextFormField( controller: _nameController, decoration: const InputDecoration(labelText: 'Name'), validator: (v) => (v == null || v.trim().isEmpty) ? 'Required.' : null, ), const SizedBox(height: 14), TextFormField( controller: _urlController, decoration: const InputDecoration( labelText: 'Base URL', hintText: 'https://edge-02.local:8080', ), validator: _validateUrl, keyboardType: TextInputType.url, ), const SizedBox(height: 14), TextFormField( controller: _descriptionController, decoration: const InputDecoration(labelText: 'Description (optional)'), ), const SizedBox(height: 14), TextFormField( controller: _tagsController, decoration: const InputDecoration( labelText: 'Tags (optional, comma-separated)', hintText: 'prod, db', ), ), if (_error != null) ...[ const SizedBox(height: 12), Text(_error!, style: TextStyle(color: Theme.of(context).colorScheme.error)), ], ], ), ), ), actions: [ TextButton( onPressed: _saving ? null : () => Navigator.of(context).pop(), child: const Text('Cancel'), ), FilledButton( onPressed: _saving ? null : _save, child: _saving ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) : const Text('Save'), ), ], ); } }