Initial commit
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user