From 04f98559fc1d418cf9b91b97979ec808506e6344 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 27 Feb 2026 13:55:08 +1100 Subject: [PATCH] Fix bool fields (#778) * Improved UX for boolean fields - Use segmented button - Allow tristate - Improved filtering options * Bug fix for null filter values * Prevent null filters from being sent to the server * Update release notes --- assets/release_notes.md | 3 ++ lib/api_form.dart | 95 ++++++++++++++++++++++++++++++++------- lib/labels.dart | 2 + lib/widget/paginator.dart | 16 +++++-- 4 files changed, 97 insertions(+), 19 deletions(-) diff --git a/assets/release_notes.md b/assets/release_notes.md index 1ecdbe5c..d69806a3 100644 --- a/assets/release_notes.md +++ b/assets/release_notes.md @@ -2,6 +2,9 @@ --- - Auto-fill location data when receiving item via barcode scan +- Visual improvements for boolean form fields +- Add support for tri-state boolean form fields +- Bug fixes for refreshing list view data ## 0.22.2 - February 2026 --- diff --git a/lib/api_form.dart b/lib/api_form.dart index 9df134cc..091af6de 100644 --- a/lib/api_form.dart +++ b/lib/api_form.dart @@ -874,25 +874,86 @@ class APIFormField { // Construct a boolean input element Widget _constructBoolean() { - bool? initial_value; + String initial_value = "null"; - if (value is bool || value == null) { - initial_value = value as bool?; + bool allow_null = (getParameter("tristate") ?? false) as bool; + + if (value is bool) { + initial_value = value.toString().toLowerCase(); + } else if (value == null) { + if (allow_null) { + initial_value = "null"; + } else { + initial_value = "false"; + } } else { - String vs = value.toString().toLowerCase(); - initial_value = ["1", "true", "yes"].contains(vs); + // Not a boolean value - may be a string + if (["1", "true", "yes"].contains(value.toString().toLowerCase())) { + initial_value = "true"; + } else if ([ + "0", + "false", + "no", + ].contains(value.toString().toLowerCase())) { + initial_value = "false"; + } else if (allow_null) { + initial_value = "null"; + } else { + initial_value = "false"; + } } - return CheckBoxField( - label: label, - labelStyle: _labelStyle(), - helperText: helpText, - helperStyle: _helperStyle(), - initial: initial_value, - tristate: (getParameter("tristate") ?? false) as bool, - onSaved: (val) { - setFieldValue(val); - }, + List> buttons = []; + + if ((getParameter("tristate") ?? false) as bool) { + buttons.add( + ButtonSegment( + value: "null", + icon: Icon(TablerIcons.minus, color: COLOR_GRAY_LIGHT), + ), + ); + } + + buttons.add( + ButtonSegment( + value: "false", + icon: Icon(TablerIcons.x, color: COLOR_DANGER), + ), + ); + + buttons.add( + ButtonSegment( + value: "true", + icon: Icon(TablerIcons.check, color: COLOR_SUCCESS), + ), + ); + + return ListTile( + title: Text(label), + contentPadding: EdgeInsets.zero, + subtitle: Text(helpText), + trailing: SegmentedButton( + segments: buttons, + selected: {initial_value}, + showSelectedIcon: false, + multiSelectionEnabled: false, + style: SegmentedButton.styleFrom( + padding: EdgeInsets.all(0), + // minimumSize: MaterialStateProperty.all(Size(0, 0)), + // tapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + ), + onSelectionChanged: (Set selection) { + String element = selection.first; + if (element == "null" && allow_null) { + setFieldValue(null); + } else if (element == "true") { + setFieldValue(true); + } else { + setFieldValue(false); + } + }, + ), ); } @@ -1168,7 +1229,9 @@ class APIFormWidgetState extends State { // Callback for when a field value is changed // Default implementation does nothing, // but custom form implementations may override this function - void onValueChanged(String field, dynamic value) {} + void onValueChanged(String field, dynamic value) { + setState(() {}); + } Future handleSuccess( Map submittedData, diff --git a/lib/labels.dart b/lib/labels.dart index fe226be0..8743a4a7 100644 --- a/lib/labels.dart +++ b/lib/labels.dart @@ -45,6 +45,8 @@ class LabelFormWidgetState extends APIFormWidgetState { if (field == "plugin") { onPluginChanged(value.toString()); } + + super.onValueChanged(field, value); } @override diff --git a/lib/widget/paginator.dart b/lib/widget/paginator.dart index 6bbf3553..38725931 100644 --- a/lib/widget/paginator.dart +++ b/lib/widget/paginator.dart @@ -92,9 +92,10 @@ abstract class PaginatedSearchState // Skip null values if (value == null) { - continue; + f[k] = "null"; + } else { + f[k] = value.toString(); } - f[k] = value.toString(); } return f; @@ -341,7 +342,16 @@ abstract class PaginatedSearchState Map f = await constructFilters(); if (f.isNotEmpty) { - params.addAll(f); + for (String k in f.keys) { + // Remove any existing filter keys + dynamic value = f[k]; + + if (value == null || value == "null") { + params.remove(k); + } else { + params[k] = value.toString(); + } + } } final page = await requestPage(_pageSize, pageKey, params);