237 lines
7.3 KiB
Dart
237 lines
7.3 KiB
Dart
|
|
import 'package:flutter/material.dart';
|
||
|
|
|
||
|
|
import '../../core/contracts/script_widget.dart';
|
||
|
|
import '../../shared/theme/app_theme.dart';
|
||
|
|
import '../../shared/widgets/setting_select.dart';
|
||
|
|
|
||
|
|
class ScriptWidgetParameterForm extends StatefulWidget {
|
||
|
|
final List<ScriptWidgetParam> parameters;
|
||
|
|
final Map<String, Object?> initialValues;
|
||
|
|
final ValueChanged<Map<String, Object?>> onChanged;
|
||
|
|
final bool hidePagingParameters;
|
||
|
|
|
||
|
|
const ScriptWidgetParameterForm({
|
||
|
|
super.key,
|
||
|
|
required this.parameters,
|
||
|
|
required this.initialValues,
|
||
|
|
required this.onChanged,
|
||
|
|
this.hidePagingParameters = false,
|
||
|
|
});
|
||
|
|
|
||
|
|
@override
|
||
|
|
State<ScriptWidgetParameterForm> createState() =>
|
||
|
|
_ScriptWidgetParameterFormState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _ScriptWidgetParameterFormState extends State<ScriptWidgetParameterForm> {
|
||
|
|
final Map<String, TextEditingController> _textControllers =
|
||
|
|
<String, TextEditingController>{};
|
||
|
|
late Map<String, Object?> _values;
|
||
|
|
|
||
|
|
@override
|
||
|
|
void initState() {
|
||
|
|
super.initState();
|
||
|
|
_values = _buildInitialValues();
|
||
|
|
_synchronizeTextControllers();
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
void didUpdateWidget(covariant ScriptWidgetParameterForm oldWidget) {
|
||
|
|
super.didUpdateWidget(oldWidget);
|
||
|
|
if (oldWidget.parameters != widget.parameters ||
|
||
|
|
oldWidget.initialValues != widget.initialValues) {
|
||
|
|
_values = _buildInitialValues();
|
||
|
|
_synchronizeTextControllers();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
void dispose() {
|
||
|
|
for (final controller in _textControllers.values) {
|
||
|
|
controller.dispose();
|
||
|
|
}
|
||
|
|
super.dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
Map<String, Object?> _buildInitialValues() {
|
||
|
|
final defaultValues = buildScriptWidgetParameterDefaults(widget.parameters);
|
||
|
|
return <String, Object?>{
|
||
|
|
for (final parameter in widget.parameters)
|
||
|
|
parameter.name:
|
||
|
|
widget.initialValues[parameter.name] ??
|
||
|
|
defaultValues[parameter.name],
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
void _synchronizeTextControllers() {
|
||
|
|
final activeNames = widget.parameters
|
||
|
|
.where((parameter) => !_usesOptionSelector(parameter))
|
||
|
|
.map((parameter) => parameter.name)
|
||
|
|
.toSet();
|
||
|
|
final staleNames = _textControllers.keys
|
||
|
|
.where((name) => !activeNames.contains(name))
|
||
|
|
.toList(growable: false);
|
||
|
|
for (final name in staleNames) {
|
||
|
|
_textControllers.remove(name)?.dispose();
|
||
|
|
}
|
||
|
|
for (final parameter in widget.parameters) {
|
||
|
|
if (_usesOptionSelector(parameter)) continue;
|
||
|
|
final value = _values[parameter.name]?.toString() ?? '';
|
||
|
|
final controller = _textControllers.putIfAbsent(
|
||
|
|
parameter.name,
|
||
|
|
TextEditingController.new,
|
||
|
|
);
|
||
|
|
if (controller.text != value) {
|
||
|
|
controller.value = TextEditingValue(
|
||
|
|
text: value,
|
||
|
|
selection: TextSelection.collapsed(offset: value.length),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
bool _usesOptionSelector(ScriptWidgetParam parameter) {
|
||
|
|
return parameter.type == 'enumeration' ||
|
||
|
|
parameter.enumOptions.isNotEmpty ||
|
||
|
|
parameter.placeholders.isNotEmpty;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool _isVisible(ScriptWidgetParam parameter) {
|
||
|
|
if (parameter.type == 'constant') return false;
|
||
|
|
if (widget.hidePagingParameters &&
|
||
|
|
const <String>{'page', 'offset'}.contains(parameter.type)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
final condition = parameter.belongTo;
|
||
|
|
if (condition == null || condition.paramName.isEmpty) return true;
|
||
|
|
final parentValue = _values[condition.paramName]?.toString() ?? '';
|
||
|
|
return condition.value.contains(parentValue);
|
||
|
|
}
|
||
|
|
|
||
|
|
void _updateValue(String name, Object? value) {
|
||
|
|
setState(() {
|
||
|
|
_values[name] = value;
|
||
|
|
});
|
||
|
|
widget.onChanged(Map<String, Object?>.unmodifiable(_values));
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
final visibleParameters = widget.parameters
|
||
|
|
.where(_isVisible)
|
||
|
|
.toList(growable: false);
|
||
|
|
if (visibleParameters.isEmpty) return const SizedBox.shrink();
|
||
|
|
|
||
|
|
return Wrap(
|
||
|
|
spacing: 12,
|
||
|
|
runSpacing: 12,
|
||
|
|
children: [
|
||
|
|
for (final parameter in visibleParameters)
|
||
|
|
SizedBox(
|
||
|
|
width: _fieldWidth(parameter),
|
||
|
|
child: _buildField(parameter),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
double _fieldWidth(ScriptWidgetParam parameter) {
|
||
|
|
if (const <String>{'count', 'page', 'offset'}.contains(parameter.type)) {
|
||
|
|
return 132;
|
||
|
|
}
|
||
|
|
return 240;
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildField(ScriptWidgetParam parameter) {
|
||
|
|
final options = parameter.enumOptions.isNotEmpty
|
||
|
|
? parameter.enumOptions
|
||
|
|
: parameter.placeholders;
|
||
|
|
if (_usesOptionSelector(parameter)) {
|
||
|
|
final selectedValue = _values[parameter.name]?.toString() ?? '';
|
||
|
|
final optionValues = options.map((option) => option.value).toSet();
|
||
|
|
final effectiveValue = optionValues.contains(selectedValue)
|
||
|
|
? selectedValue
|
||
|
|
: null;
|
||
|
|
final labelsByValue = <String, String>{
|
||
|
|
for (final option in options)
|
||
|
|
option.value: option.title.isEmpty ? option.value : option.title,
|
||
|
|
};
|
||
|
|
final theme = Theme.of(context);
|
||
|
|
final tokens = context.appTokens;
|
||
|
|
final helper = _helperFor(parameter);
|
||
|
|
return Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
Text(
|
||
|
|
_labelFor(parameter),
|
||
|
|
style: theme.textTheme.labelMedium?.copyWith(
|
||
|
|
fontWeight: FontWeight.w600,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 6),
|
||
|
|
SettingSelect<String>(
|
||
|
|
value: effectiveValue,
|
||
|
|
options: options
|
||
|
|
.map((option) => option.value)
|
||
|
|
.toList(growable: false),
|
||
|
|
labelOf: (value) => labelsByValue[value] ?? value,
|
||
|
|
onChanged: (value) => _updateValue(parameter.name, value ?? ''),
|
||
|
|
),
|
||
|
|
if (helper != null) ...[
|
||
|
|
const SizedBox(height: 4),
|
||
|
|
Text(
|
||
|
|
helper,
|
||
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
||
|
|
color: tokens.mutedForeground,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
],
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
final isNumeric = const <String>{
|
||
|
|
'count',
|
||
|
|
'page',
|
||
|
|
'offset',
|
||
|
|
}.contains(parameter.type);
|
||
|
|
final theme = Theme.of(context);
|
||
|
|
final tokens = context.appTokens;
|
||
|
|
final inputBorder = OutlineInputBorder(
|
||
|
|
borderRadius: BorderRadius.circular(8),
|
||
|
|
borderSide: BorderSide(
|
||
|
|
color: tokens.mutedForeground.withValues(alpha: 0.28),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
return TextField(
|
||
|
|
controller: _textControllers[parameter.name],
|
||
|
|
keyboardType: isNumeric ? TextInputType.number : TextInputType.text,
|
||
|
|
decoration: InputDecoration(
|
||
|
|
labelText: _labelFor(parameter),
|
||
|
|
helperText: _helperFor(parameter),
|
||
|
|
filled: true,
|
||
|
|
fillColor: theme.colorScheme.surface.withValues(alpha: 0.42),
|
||
|
|
border: inputBorder,
|
||
|
|
enabledBorder: inputBorder,
|
||
|
|
focusedBorder: inputBorder.copyWith(
|
||
|
|
borderSide: BorderSide(color: theme.colorScheme.primary),
|
||
|
|
),
|
||
|
|
isDense: true,
|
||
|
|
),
|
||
|
|
onChanged: (value) {
|
||
|
|
final parsedValue = isNumeric ? int.tryParse(value) ?? value : value;
|
||
|
|
_updateValue(parameter.name, parsedValue);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
String _labelFor(ScriptWidgetParam parameter) {
|
||
|
|
return parameter.title.isEmpty ? parameter.name : parameter.title;
|
||
|
|
}
|
||
|
|
|
||
|
|
String? _helperFor(ScriptWidgetParam parameter) {
|
||
|
|
final description = parameter.description.trim();
|
||
|
|
return description.isEmpty ? null : description;
|
||
|
|
}
|
||
|
|
}
|