2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-04-28 05:26:47 +00:00

List refactor (#179)

* Catch paginator bug if widget is disposed before request returns

* Refactoring paginated query widget

- Add option to enable / disable search filters

* Major refactor of paginated search widget

- Learned something new.. a state can access widget.<attribute>
- THIS CHANGES EVERTHING

* Preferences: Add code for tri-state values

- Also improve unit testing for preferences code

* Allow boolean form fields to be optionally tristate

* paginator: Allow custom boolean filters

* Remove outdated filtering preferences

* Refactor filter options

- Allow specification of more detailed options

* Add custom filters for "part" list

* filter tweaks

* Remove legacy "SublocationList" widget

* Add filtering option for locationlist

* Updates for stock location widget

* Refactor category display widget

* More widget refactoring

* Update main search widget

* Fix unit tests

* Improve filtering on BOM display page
This commit is contained in:
Oliver 2022-07-19 23:29:01 +10:00 committed by GitHub
parent e03a8561b9
commit 13ebaf43e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 667 additions and 403 deletions

View File

@ -704,7 +704,8 @@ class APIFormField {
labelStyle: _labelStyle(), labelStyle: _labelStyle(),
helperText: helpText, helperText: helpText,
helperStyle: _helperStyle(), helperStyle: _helperStyle(),
initial: value as bool, initial: value as bool?,
tristate: (getParameter("tristate") ?? false) as bool,
onSaved: (val) { onSaved: (val) {
data["value"] = val; data["value"] = val;
}, },

View File

@ -32,15 +32,6 @@ class InvenTreePartCategory extends InvenTreeModel {
}; };
} }
@override
Map<String, String> defaultListFilters() {
return {
"active": "true",
"cascade": "false"
};
}
String get pathstring => (jsondata["pathstring"] ?? "") as String; String get pathstring => (jsondata["pathstring"] ?? "") as String;
String get parentPathString { String get parentPathString {
@ -171,8 +162,7 @@ class InvenTreePart extends InvenTreeModel {
@override @override
Map<String, String> defaultListFilters() { Map<String, String> defaultListFilters() {
return { return {
"cascade": "false", "location_detail": "true",
"active": "true",
}; };
} }

View File

@ -224,7 +224,6 @@ class InvenTreeStockItem extends InvenTreeModel {
"part_detail": "true", "part_detail": "true",
"location_detail": "true", "location_detail": "true",
"supplier_detail": "true", "supplier_detail": "true",
"cascade": "false",
"in_stock": "true", "in_stock": "true",
}; };
} }

View File

@ -297,6 +297,48 @@
"feedbackSuccess": "Feedback submitted", "feedbackSuccess": "Feedback submitted",
"@feedbackSuccess": {}, "@feedbackSuccess": {},
"filterActive": "Active",
"@filterActive": {},
"filterActiveDetail": "Show active parts",
"@filterActiveDetail": {},
"filterAssembly": "Assembled",
"@filterAssembly": {},
"filterAssemblyDetail": "Show assembled parts",
"@filterAssemblyDetail": {},
"filterComponent": "Component",
"@filterComponent": {},
"filterComponentDetail": "Show component parts",
"@filterComponentDetail": {},
"filterInStock": "In Stock",
"@filterInStock": {},
"filterInStockDetail": "Show parts which have stock",
"@filterInStockDetail": {},
"filterSerialized": "Serialized",
"@filterSerialized": {},
"filterSerializedDetail": "Show serialized stock items",
"@filterSerializedDetail": {},
"filterTemplate": "Template",
"@filterTemplate": {},
"filterTemplateDetail": "Show template parts",
"@filterTemplateDetail": {},
"filterVirtual": "Virtual",
"@filterVirtual": {},
"filterVirtualDetail": "Show virtual parts",
"@filterVirtualDetail": {},
"filteringOptions": "Filtering Options", "filteringOptions": "Filtering Options",
"@filteringOptions": {}, "@filteringOptions": {},
@ -368,13 +410,13 @@
"includeSubcategories": "Include Subcategories", "includeSubcategories": "Include Subcategories",
"@includeSubcategories": {}, "@includeSubcategories": {},
"includeSubcategoriesDetail": "Display subcategory parts in list view", "includeSubcategoriesDetail": "Show results from subcategories",
"@includeSubcategoriesDetail": {}, "@includeSubcategoriesDetail": {},
"includeSublocations": "Include Sublocations", "includeSublocations": "Include Sublocations",
"@includeSublocations": {}, "@includeSublocations": {},
"includeSublocationsDetail": "Display sublocation items in list view", "includeSublocationsDetail": "Show results from sublocations",
"@includeSublocationsDetail": {}, "@includeSublocationsDetail": {},
"incompleteDetails": "Incomplete profile details", "incompleteDetails": "Incomplete profile details",

View File

@ -16,9 +16,6 @@ const String INV_HOME_SHOW_SUPPLIERS = "homeShowSuppliers";
const String INV_SOUNDS_BARCODE = "barcodeSounds"; const String INV_SOUNDS_BARCODE = "barcodeSounds";
const String INV_SOUNDS_SERVER = "serverSounds"; const String INV_SOUNDS_SERVER = "serverSounds";
const String INV_PART_SUBCATEGORY = "partSubcategory";
const String INV_STOCK_SUBLOCATION = "stockSublocation";
const String INV_STOCK_SHOW_HISTORY = "stockShowHistory"; const String INV_STOCK_SHOW_HISTORY = "stockShowHistory";
const String INV_REPORT_ERRORS = "reportErrors"; const String INV_REPORT_ERRORS = "reportErrors";
@ -86,6 +83,11 @@ class InvenTreeSettingsManager {
Future<Database> get _db async => InvenTreePreferencesDB.instance.database; Future<Database> get _db async => InvenTreePreferencesDB.instance.database;
Future<void> removeValue(String key) async {
await store.record(key).delete(await _db);
}
Future<dynamic> getValue(String key, dynamic backup) async { Future<dynamic> getValue(String key, dynamic backup) async {
final value = await store.record(key).get(await _db); final value = await store.record(key).get(await _db);
@ -103,13 +105,40 @@ class InvenTreeSettingsManager {
if (value is bool) { if (value is bool) {
return value; return value;
} else if (value is String) {
return value.toLowerCase().contains("t");
} else { } else {
return backup; return false;
} }
} }
// Load a tristate (true / false / null) setting
Future<bool?> getTriState(String key, dynamic backup) async {
final dynamic value = await getValue(key, backup);
if (value == null) {
return null;
} else if (value is bool) {
return value;
} else {
String s = value.toString().toLowerCase();
if (s.contains("t")) {
return true;
} else if (s.contains("f")) {
return false;
} else {
return null;
}
}
}
// Store a key:value pair in the database
Future<void> setValue(String key, dynamic value) async { Future<void> setValue(String key, dynamic value) async {
// Encode null values as strings
value ??= "null";
await store.record(key).put(await _db, value); await store.record(key).put(await _db, value);
} }

View File

@ -21,11 +21,7 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
bool barcodeSounds = true; bool barcodeSounds = true;
bool serverSounds = true; bool serverSounds = true;
// Part settings
bool partSubcategory = false;
// Stock settings // Stock settings
bool stockSublocation = false;
bool stockShowHistory = false; bool stockShowHistory = false;
bool reportErrors = true; bool reportErrors = true;
@ -45,9 +41,6 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
barcodeSounds = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool; barcodeSounds = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool;
serverSounds = await InvenTreeSettingsManager().getValue(INV_SOUNDS_SERVER, true) as bool; serverSounds = await InvenTreeSettingsManager().getValue(INV_SOUNDS_SERVER, true) as bool;
partSubcategory = await InvenTreeSettingsManager().getValue(INV_PART_SUBCATEGORY, true) as bool;
stockSublocation = await InvenTreeSettingsManager().getValue(INV_STOCK_SUBLOCATION, true) as bool;
stockShowHistory = await InvenTreeSettingsManager().getValue(INV_STOCK_SHOW_HISTORY, false) as bool; stockShowHistory = await InvenTreeSettingsManager().getValue(INV_STOCK_SHOW_HISTORY, false) as bool;
reportErrors = await InvenTreeSettingsManager().getValue(INV_REPORT_ERRORS, true) as bool; reportErrors = await InvenTreeSettingsManager().getValue(INV_REPORT_ERRORS, true) as bool;
@ -68,49 +61,13 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
body: Container( body: Container(
child: ListView( child: ListView(
children: [ children: [
ListTile(
title: Text(
L10().parts,
style: TextStyle(fontWeight: FontWeight.bold),
),
leading: FaIcon(FontAwesomeIcons.shapes),
),
ListTile(
title: Text(L10().includeSubcategories),
subtitle: Text(L10().includeSubcategoriesDetail),
leading: FaIcon(FontAwesomeIcons.sitemap),
trailing: Switch(
value: partSubcategory,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(INV_PART_SUBCATEGORY, value);
setState(() {
partSubcategory = value;
});
},
),
),
/* Stock Settings */ /* Stock Settings */
Divider(height: 3),
ListTile( ListTile(
title: Text(L10().stock, title: Text(L10().stock,
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
leading: FaIcon(FontAwesomeIcons.boxes), leading: FaIcon(FontAwesomeIcons.boxes),
), ),
ListTile(
title: Text(L10().includeSublocations),
subtitle: Text(L10().includeSublocationsDetail),
leading: FaIcon(FontAwesomeIcons.sitemap),
trailing: Switch(
value: stockSublocation,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(INV_STOCK_SUBLOCATION, value);
setState(() {
stockSublocation = value;
});
},
),
),
ListTile( ListTile(
title: Text(L10().stockItemHistory), title: Text(L10().stockItemHistory),
subtitle: Text(L10().stockItemHistoryDetail), subtitle: Text(L10().stockItemHistoryDetail),

View File

@ -1,5 +1,6 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/api.dart"; import "package:inventree/api.dart";
import "package:inventree/helpers.dart"; import "package:inventree/helpers.dart";
@ -32,14 +33,31 @@ class _BillOfMaterialsState extends RefreshableState<BillOfMaterialsWidget> {
final InvenTreePart part; final InvenTreePart part;
bool showFilterOptions = false;
@override @override
String getAppBarTitle(BuildContext context) => L10().billOfMaterials; String getAppBarTitle(BuildContext context) => L10().billOfMaterials;
@override @override
Widget getBody(BuildContext context) { List<Widget> getAppBarActions(BuildContext context) => [
return PaginatedBomList({ IconButton(
"part": part.pk.toString(), icon: FaIcon(FontAwesomeIcons.filter),
onPressed: () async {
setState(() {
showFilterOptions = !showFilterOptions;
}); });
},
)
];
@override
Widget getBody(BuildContext context) {
return PaginatedBomList(
{
"part": part.pk.toString(),
},
showFilterOptions,
);
} }
} }
@ -47,26 +65,18 @@ class _BillOfMaterialsState extends RefreshableState<BillOfMaterialsWidget> {
/* /*
* Create a paginated widget displaying a list of BomItem objects * Create a paginated widget displaying a list of BomItem objects
*/ */
class PaginatedBomList extends StatefulWidget { class PaginatedBomList extends PaginatedSearchWidget {
const PaginatedBomList(this.filters, {this.onTotalChanged}); const PaginatedBomList(Map<String, String> filters, bool showSearch) : super(filters: filters, showSearch: showSearch);
final Map<String, String> filters;
final Function(int)? onTotalChanged;
@override @override
_PaginatedBomListState createState() => _PaginatedBomListState(filters, onTotalChanged); _PaginatedBomListState createState() => _PaginatedBomListState();
} }
class _PaginatedBomListState extends PaginatedSearchState<PaginatedBomList> { class _PaginatedBomListState extends PaginatedSearchState<PaginatedBomList> {
_PaginatedBomListState(Map<String, String> filters, this.onTotalChanged) : super(filters); _PaginatedBomListState() : super();
Function(int)? onTotalChanged;
@override @override
String get prefix => "bom_"; String get prefix => "bom_";
@ -77,6 +87,14 @@ class _PaginatedBomListState extends PaginatedSearchState<PaginatedBomList> {
"sub_part": L10().part, "sub_part": L10().part,
}; };
@override
Map<String, Map<String, dynamic>> get filterOptions => {
"sub_part_assembly": {
"label": L10().filterAssembly,
"help_text": L10().filterAssemblyDetail,
}
};
@override @override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async { Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {

View File

@ -1,14 +1,15 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/api.dart"; import "package:inventree/api.dart";
import "package:inventree/app_colors.dart"; import "package:inventree/app_colors.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/widget/part_list.dart";
import "package:inventree/widget/progress.dart";
import "package:inventree/widget/snacks.dart";
import "package:inventree/l10.dart"; import "package:inventree/l10.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/widget/category_list.dart";
import "package:inventree/widget/part_list.dart";
import "package:inventree/widget/snacks.dart";
import "package:inventree/widget/part_detail.dart"; import "package:inventree/widget/part_detail.dart";
import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/widget/refreshable_state.dart";
@ -28,6 +29,8 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
_CategoryDisplayState(this.category); _CategoryDisplayState(this.category);
bool showFilterOptions = false;
@override @override
String getAppBarTitle(BuildContext context) => L10().partCategory; String getAppBarTitle(BuildContext context) => L10().partCategory;
@ -73,8 +76,6 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
// The local InvenTreePartCategory object // The local InvenTreePartCategory object
final InvenTreePartCategory? category; final InvenTreePartCategory? category;
List<InvenTreePartCategory> _subcategories = [];
@override @override
Future<void> onBuild(BuildContext context) async { Future<void> onBuild(BuildContext context) async {
refresh(context); refresh(context);
@ -83,8 +84,6 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
@override @override
Future<void> request(BuildContext context) async { Future<void> request(BuildContext context) async {
int pk = category?.pk ?? -1;
// Update the category // Update the category
if (category != null) { if (category != null) {
final bool result = await category?.reload() ?? false; final bool result = await category?.reload() ?? false;
@ -93,27 +92,17 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
} }
// Request a list of sub-categories under this one
await InvenTreePartCategory().list(filters: {"parent": "$pk"}).then((var cats) {
_subcategories.clear();
for (var cat in cats) {
if (cat is InvenTreePartCategory) {
_subcategories.add(cat);
}
}
// Update state
setState(() {});
});
} }
Widget getCategoryDescriptionCard({bool extra = true}) { Widget getCategoryDescriptionCard({bool extra = true}) {
if (category == null) { if (category == null) {
return Card( return Card(
child: ListTile( child: ListTile(
title: Text(L10().partCategoryTopLevel) leading: FaIcon(FontAwesomeIcons.shapes),
title: Text(
L10().partCategoryTopLevel,
style: TextStyle(fontStyle: FontStyle.italic),
)
) )
); );
} else { } else {
@ -182,7 +171,9 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
); );
} }
// Construct the "details" panel
List<Widget> detailTiles() { List<Widget> detailTiles() {
List<Widget> tiles = <Widget>[ List<Widget> tiles = <Widget>[
getCategoryDescriptionCard(), getCategoryDescriptionCard(),
ListTile( ListTile(
@ -190,25 +181,60 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
L10().subcategories, L10().subcategories,
style: TextStyle(fontWeight: FontWeight.bold) style: TextStyle(fontWeight: FontWeight.bold)
), ),
trailing: _subcategories.isNotEmpty ? Text("${_subcategories.length}") : null, trailing: GestureDetector(
child: FaIcon(FontAwesomeIcons.filter),
onTap: () async {
setState(() {
showFilterOptions = !showFilterOptions;
});
},
)
), ),
Expanded(
child: PaginatedPartCategoryList(
{
"parent": category?.pk.toString() ?? "null"
},
showFilterOptions,
),
flex: 10,
)
]; ];
if (loading) { return tiles;
tiles.add(progressIndicator());
} else if (_subcategories.isEmpty) {
tiles.add(ListTile(
title: Text(L10().noSubcategories),
subtitle: Text(
L10().noSubcategoriesAvailable,
style: TextStyle(fontStyle: FontStyle.italic)
)
));
} else {
tiles.add(SubcategoryList(_subcategories));
} }
return tiles; // Construct the "parts" panel
List<Widget> partsTiles() {
Map<String, String> filters = {
"category": category?.pk.toString() ?? "null",
};
return [
getCategoryDescriptionCard(extra: false),
ListTile(
title: Text(
L10().parts,
style: TextStyle(fontWeight: FontWeight.bold),
),
trailing: GestureDetector(
child: FaIcon(FontAwesomeIcons.filter),
onTap: () async {
setState(() {
showFilterOptions = !showFilterOptions;
});
},
),
),
Expanded(
child: PaginatedPartList(
filters,
showFilterOptions,
),
flex: 10,
)
];
} }
Future<void> _newCategory(BuildContext context) async { Future<void> _newCategory(BuildContext context) async {
@ -323,14 +349,12 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
switch (tabIndex) { switch (tabIndex) {
case 0: case 0:
return ListView( return Column(
children: detailTiles() children: detailTiles()
); );
case 1: case 1:
return PaginatedPartList( return Column(
{ children: partsTiles()
"category": "${category?.pk ?? 'null'}"
},
); );
case 2: case 2:
return ListView( return ListView(
@ -341,47 +365,3 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
} }
} }
} }
/*
* Builder for displaying a list of PartCategory objects
*/
class SubcategoryList extends StatelessWidget {
const SubcategoryList(this._categories);
final List<InvenTreePartCategory> _categories;
void _openCategory(BuildContext context, int pk) {
// Attempt to load the sub-category.
InvenTreePartCategory().get(pk).then((var cat) {
if (cat is InvenTreePartCategory) {
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat)));
}
});
}
Widget _build(BuildContext context, int index) {
InvenTreePartCategory cat = _categories[index];
return ListTile(
title: Text("${cat.name}"),
subtitle: Text("${cat.description}"),
trailing: Text("${cat.partcount}"),
onTap: () {
_openCategory(context, cat.pk);
}
);
}
@override
Widget build(BuildContext context) {
return ListView.separated(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
separatorBuilder: (_, __) => const Divider(height: 3),
itemBuilder: _build, itemCount: _categories.length);
}
}

View File

@ -1,4 +1,5 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/inventree/model.dart"; import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/part.dart"; import "package:inventree/inventree/part.dart";
@ -25,34 +26,55 @@ class _PartCategoryListState extends RefreshableState<PartCategoryList> {
final Map<String, String> filters; final Map<String, String> filters;
bool showFilterOptions = false;
@override
List<Widget> getAppBarActions(BuildContext context) => [
IconButton(
icon: FaIcon(FontAwesomeIcons.filter),
onPressed: () async {
setState(() {
showFilterOptions = !showFilterOptions;
});
},
)
];
@override @override
String getAppBarTitle(BuildContext context) => L10().partCategories; String getAppBarTitle(BuildContext context) => L10().partCategories;
@override @override
Widget getBody(BuildContext context) { Widget getBody(BuildContext context) {
return PaginatedPartCategoryList(filters); return PaginatedPartCategoryList(filters, showFilterOptions);
} }
} }
class PaginatedPartCategoryList extends PaginatedSearchWidget {
class PaginatedPartCategoryList extends StatefulWidget { const PaginatedPartCategoryList(Map<String, String> filters, bool showSearch) : super(filters: filters, showSearch: showSearch);
const PaginatedPartCategoryList(this.filters);
final Map<String, String> filters;
@override @override
_PaginatedPartCategoryListState createState() => _PaginatedPartCategoryListState(filters); _PaginatedPartCategoryListState createState() => _PaginatedPartCategoryListState();
} }
class _PaginatedPartCategoryListState extends PaginatedSearchState<PaginatedPartCategoryList> { class _PaginatedPartCategoryListState extends PaginatedSearchState<PaginatedPartCategoryList> {
_PaginatedPartCategoryListState(Map<String, String> filters) : super(filters); // _PaginatedPartCategoryListState(Map<String, String> filters, bool searchEnabled) : super(filters, searchEnabled);
@override @override
String get prefix => "category_"; String get prefix => "category_";
@override
Map<String, Map<String, dynamic>> get filterOptions => {
"cascade": {
"default": false,
"label": L10().includeSubcategories,
"help_text": L10().includeSubcategoriesDetail,
"tristate": false,
}
};
@override @override
Map<String, String> get orderingOptions => { Map<String, String> get orderingOptions => {
"name": L10().name, "name": L10().name,

View File

@ -35,29 +35,22 @@ class _CompanyListWidgetState extends RefreshableState<CompanyListWidget> {
@override @override
Widget getBody(BuildContext context) { Widget getBody(BuildContext context) {
return PaginatedCompanyList(filters, true);
return PaginatedCompanyList(filters);
} }
} }
class PaginatedCompanyList extends PaginatedSearchWidget {
class PaginatedCompanyList extends StatefulWidget { const PaginatedCompanyList(Map<String, String> filters, bool showSearch) : super(filters: filters, showSearch: showSearch);
const PaginatedCompanyList(this.filters, {this.onTotalChanged});
final Map<String, String> filters;
final Function(int)? onTotalChanged;
@override @override
_CompanyListState createState() => _CompanyListState(filters); _CompanyListState createState() => _CompanyListState();
} }
class _CompanyListState extends PaginatedSearchState<PaginatedCompanyList> { class _CompanyListState extends PaginatedSearchState<PaginatedCompanyList> {
_CompanyListState(Map<String, String> filters) : super(filters); _CompanyListState() : super();
@override @override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async { Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {

View File

@ -142,7 +142,10 @@ class FilePickerDialog {
class CheckBoxField extends FormField<bool> { class CheckBoxField extends FormField<bool> {
CheckBoxField({ CheckBoxField({
String? label, bool initial = false, Function(bool?)? onSaved, String? label,
bool? initial = false,
bool tristate = false,
Function(bool?)? onSaved,
TextStyle? labelStyle, TextStyle? labelStyle,
String? helperText, String? helperText,
TextStyle? helperStyle, TextStyle? helperStyle,
@ -155,6 +158,7 @@ class CheckBoxField extends FormField<bool> {
//dense: state.hasError, //dense: state.hasError,
title: label != null ? Text(label, style: labelStyle) : null, title: label != null ? Text(label, style: labelStyle) : null,
value: state.value, value: state.value,
tristate: tristate,
onChanged: state.didChange, onChanged: state.didChange,
subtitle: helperText != null ? Text(helperText, style: helperStyle) : null, subtitle: helperText != null ? Text(helperText, style: helperStyle) : null,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,

View File

@ -5,15 +5,20 @@ import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/api.dart"; import "package:inventree/api.dart";
import "package:inventree/app_colors.dart"; import "package:inventree/app_colors.dart";
import "package:inventree/barcode.dart"; import "package:inventree/barcode.dart";
import "package:inventree/l10.dart";
import "package:inventree/inventree/stock.dart"; import "package:inventree/inventree/stock.dart";
import "package:inventree/widget/progress.dart";
import "package:inventree/widget/location_list.dart";
import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/widget/snacks.dart"; import "package:inventree/widget/snacks.dart";
import "package:inventree/widget/stock_detail.dart"; import "package:inventree/widget/stock_detail.dart";
import "package:inventree/l10.dart";
import "package:inventree/widget/stock_list.dart"; import "package:inventree/widget/stock_list.dart";
/*
* Widget for displaying detail view for a single StockLocation instance
*/
class LocationDisplayWidget extends StatefulWidget { class LocationDisplayWidget extends StatefulWidget {
LocationDisplayWidget(this.location, {Key? key}) : super(key: key); LocationDisplayWidget(this.location, {Key? key}) : super(key: key);
@ -32,6 +37,8 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
final InvenTreeStockLocation? location; final InvenTreeStockLocation? location;
bool showFilterOptions = false;
@override @override
String getAppBarTitle(BuildContext context) { return L10().stockLocation; } String getAppBarTitle(BuildContext context) { return L10().stockLocation; }
@ -103,19 +110,6 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
); );
} }
List<InvenTreeStockLocation> _sublocations = [];
String _locationFilter = "";
List<InvenTreeStockLocation> get sublocations {
if (_locationFilter.isEmpty || _sublocations.isEmpty) {
return _sublocations;
} else {
return _sublocations.where((loc) => loc.filter(_locationFilter)).toList();
}
}
@override @override
Future<void> onBuild(BuildContext context) async { Future<void> onBuild(BuildContext context) async {
refresh(context); refresh(context);
@ -124,8 +118,6 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
@override @override
Future<void> request(BuildContext context) async { Future<void> request(BuildContext context) async {
int pk = location?.pk ?? -1;
// Reload location information // Reload location information
if (location != null) { if (location != null) {
final bool result = await location!.reload(); final bool result = await location!.reload();
@ -135,17 +127,6 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
} }
} }
// Request a list of sub-locations under this one
await InvenTreeStockLocation().list(filters: {"parent": "$pk"}).then((var locs) {
_sublocations.clear();
for (var loc in locs) {
if (loc is InvenTreeStockLocation) {
_sublocations.add(loc);
}
}
});
setState(() {}); setState(() {});
} }
@ -214,7 +195,11 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
if (location == null) { if (location == null) {
return Card( return Card(
child: ListTile( child: ListTile(
title: Text(L10().stockTopLevel), title: Text(
L10().stockTopLevel,
style: TextStyle(fontStyle: FontStyle.italic)
),
leading: FaIcon(FontAwesomeIcons.boxes),
) )
); );
} else { } else {
@ -223,7 +208,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
ListTile( ListTile(
title: Text("${location!.name}"), title: Text("${location!.name}"),
subtitle: Text("${location!.description}"), subtitle: Text("${location!.description}"),
trailing: Text("${location!.itemcount}"), leading: FaIcon(FontAwesomeIcons.boxes),
), ),
]; ];
@ -286,20 +271,15 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
Widget getSelectedWidget(int index) { Widget getSelectedWidget(int index) {
// Construct filters for paginated stock list
Map<String, String> filters = {};
if (location != null) {
filters["location"] = "${location!.pk}";
}
switch (index) { switch (index) {
case 0: case 0:
return ListView( return Column(
children: detailTiles(), children: detailTiles(),
); );
case 1: case 1:
return PaginatedStockItemList(filters); return Column(
children: stockTiles(),
);
case 2: case 2:
return ListView( return ListView(
children: ListTile.divideTiles( children: ListTile.divideTiles(
@ -317,7 +297,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
return getSelectedWidget(tabIndex); return getSelectedWidget(tabIndex);
} }
// Construct the "details" panel
List<Widget> detailTiles() { List<Widget> detailTiles() {
List<Widget> tiles = [ List<Widget> tiles = [
locationDescriptionCard(), locationDescriptionCard(),
@ -326,27 +306,61 @@ List<Widget> detailTiles() {
L10().sublocations, L10().sublocations,
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
trailing: sublocations.isNotEmpty ? Text("${sublocations.length}") : null, trailing: GestureDetector(
), child: FaIcon(FontAwesomeIcons.filter),
]; onTap: () async {
setState(() {
if (loading) { showFilterOptions = !showFilterOptions;
tiles.add(progressIndicator()); });
} else if (_sublocations.isNotEmpty) { },
tiles.add(SublocationList(_sublocations));
} else {
tiles.add(ListTile(
title: Text(L10().sublocationNone),
subtitle: Text(
L10().sublocationNoneDetail,
style: TextStyle(fontStyle: FontStyle.italic)
) )
)); ),
} Expanded(
child: PaginatedStockLocationList(
{
"parent": location?.pk.toString() ?? "null",
},
showFilterOptions,
),
flex: 10,
)
];
return tiles; return tiles;
} }
// Construct the "stock" panel
List<Widget> stockTiles() {
Map<String, String> filters = {
"location": location?.pk.toString() ?? "null",
};
return [
locationDescriptionCard(includeActions: false),
ListTile(
title: Text(
L10().stock,
style: TextStyle(fontWeight: FontWeight.bold),
),
trailing: GestureDetector(
child: FaIcon(FontAwesomeIcons.filter),
onTap: () async {
setState(() {
showFilterOptions = !showFilterOptions;
});
},
),
),
Expanded(
child: PaginatedStockItemList(
filters,
showFilterOptions,
),
flex: 10,
)
];
}
List<Widget> actionTiles() { List<Widget> actionTiles() {
List<Widget> tiles = []; List<Widget> tiles = [];
@ -452,48 +466,4 @@ List<Widget> detailTiles() {
return tiles; return tiles;
} }
}
class SublocationList extends StatelessWidget {
const SublocationList(this._locations);
final List<InvenTreeStockLocation> _locations;
void _openLocation(BuildContext context, int pk) {
InvenTreeStockLocation().get(pk).then((var loc) {
if (loc is InvenTreeStockLocation) {
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc)));
}
});
}
Widget _build(BuildContext context, int index) {
InvenTreeStockLocation loc = _locations[index];
return ListTile(
title: Text("${loc.name}"),
subtitle: Text("${loc.description}"),
trailing: Text("${loc.itemcount}"),
onTap: () {
_openLocation(context, loc.pk);
},
);
}
@override
Widget build(BuildContext context) {
return ListView.separated(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
itemBuilder: _build,
separatorBuilder: (_, __) => const Divider(height: 3),
itemCount: _locations.length
);
}
} }

View File

@ -1,4 +1,5 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/inventree/model.dart"; import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/stock.dart"; import "package:inventree/inventree/stock.dart";
@ -26,30 +27,57 @@ class _StockLocationListState extends RefreshableState<StockLocationList> {
final Map<String, String> filters; final Map<String, String> filters;
bool showFilterOptions = false;
@override
List<Widget> getAppBarActions(BuildContext context) => [
IconButton(
icon: FaIcon(FontAwesomeIcons.filter),
onPressed: () async {
setState(() {
showFilterOptions = !showFilterOptions;
});
},
)
];
@override @override
String getAppBarTitle(BuildContext context) => L10().stockLocations; String getAppBarTitle(BuildContext context) => L10().stockLocations;
@override @override
Widget getBody(BuildContext context) { Widget getBody(BuildContext context) {
return PaginatedStockLocationList(filters); return PaginatedStockLocationList(filters, showFilterOptions);
} }
} }
class PaginatedStockLocationList extends StatefulWidget { class PaginatedStockLocationList extends PaginatedSearchWidget {
const PaginatedStockLocationList(this.filters); const PaginatedStockLocationList(Map<String, String> filters, bool showSearch) : super(filters: filters, showSearch: showSearch);
final Map<String, String> filters;
@override @override
_PaginatedStockLocationListState createState() => _PaginatedStockLocationListState(filters); _PaginatedStockLocationListState createState() => _PaginatedStockLocationListState();
} }
class _PaginatedStockLocationListState extends PaginatedSearchState<PaginatedStockLocationList> { class _PaginatedStockLocationListState extends PaginatedSearchState<PaginatedStockLocationList> {
_PaginatedStockLocationListState(Map<String, String> filters) : super(filters); _PaginatedStockLocationListState() : super();
@override
Map<String, String> get orderingOptions => {
"name": L10().name,
"level": L10().level,
};
@override
Map<String, Map<String, dynamic>> get filterOptions => {
"cascade": {
"label": L10().includeSublocations,
"help_text": L10().includeSublocationsDetail,
"tristate": false,
}
};
@override @override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async { Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {

View File

@ -15,23 +15,72 @@ import "package:inventree/widget/refreshable_state.dart";
/* /*
* Generic stateful widget for displaying paginated data retrieved via the API * Abstract base widget class for rendering a PaginatedSearchState
*
* - Can be displayed as "full screen" (with app-bar and drawer)
* - Can be displayed as a standalone widget
*/ */
class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseWidgetProperties { abstract class PaginatedSearchWidget extends StatefulWidget {
PaginatedSearchState(this.filters); const PaginatedSearchWidget({this.filters = const {}, this.showSearch = false});
final Map<String, String> filters; final Map<String, String> filters;
final bool showSearch;
}
/*
* Generic stateful widget for displaying paginated data retrieved via the API
*/
abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends State<T> with BaseWidgetProperties {
static const _pageSize = 25; static const _pageSize = 25;
// Prefix for storing and loading pagination options // Prefix for storing and loading pagination options
// Override in implementing class // Override in implementing class
String get prefix => "prefix_"; String get prefix => "prefix_";
// Return a map of boolean filtering options available for this list
// Should be overridden by an implementing subclass
Map<String, Map<String, dynamic>> get filterOptions => {};
// Return the boolean value of a particular boolean filter
Future<bool?> getBooleanFilterValue(String key) async {
key = "${prefix}bool_${key}";
Map<String, dynamic> opts = filterOptions[key] ?? {};
bool? backup;
dynamic v = opts["default"];
if (v is bool) {
backup = v;
}
final result = await InvenTreeSettingsManager().getTriState(key, backup);
return result;
}
// Set the boolean value of a particular boolean filter
Future<void> setBooleanFilterValue(String key, bool? value) async {
key = "${prefix}bool_${key}";
await InvenTreeSettingsManager().setValue(key, value);
}
// Construct the boolean filter options for this list
Future<Map<String, String>> constructBooleanFilters() async {
Map<String, String> f = {};
for (String k in filterOptions.keys) {
bool? value = await getBooleanFilterValue(k);
if (value is bool) {
f[k] = value ? "true" : "false";
}
}
return f;
}
// Return a map of sorting options available for this list // Return a map of sorting options available for this list
// Should be overridden by an implementing subclass // Should be overridden by an implementing subclass
Map<String, String> get orderingOptions => {}; Map<String, String> get orderingOptions => {};
@ -115,6 +164,34 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW
} }
}; };
// Add in boolean filter options
for (String key in filterOptions.keys) {
Map<String, dynamic> opts = filterOptions[key] ?? {};
// Determine field information
String label = (opts["label"] ?? key) as String;
String? help_text = opts["help_text"] as String?;
bool tristate = (opts["tristate"] ?? true) as bool;
bool? v = await getBooleanFilterValue(key);
// Prevent null value if not tristate
if (!tristate && v == null) {
v = false;
}
// Add in the particular field
fields[key] = {
"type": "boolean",
"display_name": label,
"label": label,
"help_text": help_text,
"value": v,
"tristate": (opts["tristate"] ?? true) as bool,
};
}
// Launch an interactive form for the user to select options // Launch an interactive form for the user to select options
launchApiForm( launchApiForm(
context, context,
@ -132,6 +209,20 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW
await InvenTreeSettingsManager().setValue("${prefix}ordering_field", f); await InvenTreeSettingsManager().setValue("${prefix}ordering_field", f);
await InvenTreeSettingsManager().setValue("${prefix}ordering_order", o); await InvenTreeSettingsManager().setValue("${prefix}ordering_order", o);
// Save boolean fields
for (String key in filterOptions.keys) {
bool? v;
dynamic value = data[key];
if (value is bool) {
v = value;
}
await setBooleanFilterValue(key, v);
}
// Refresh data from the server // Refresh data from the server
_pagingController.refresh(); _pagingController.refresh();
} }
@ -189,10 +280,12 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW
*/ */
Future<void> _fetchPage(int pageKey) async { Future<void> _fetchPage(int pageKey) async {
try { try {
Map<String, String> params = filters; Map<String, String> params = widget.filters;
// Include user search term // Include user search term
if (searchTerm.isNotEmpty) {
params["search"] = "${searchTerm}"; params["search"] = "${searchTerm}";
}
// Use custom query ordering if available // Use custom query ordering if available
String o = await orderingString; String o = await orderingString;
@ -200,12 +293,24 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW
params["ordering"] = o; params["ordering"] = o;
} }
Map<String, String> f = await constructBooleanFilters();
if (f.isNotEmpty) {
params.addAll(f);
}
final page = await requestPage( final page = await requestPage(
_pageSize, _pageSize,
pageKey, pageKey,
params params
); );
// We may have disposed of the widget while the request was in progress
// If this is the case, abort
if (!mounted) {
return;
}
int pageLength = page?.length ?? 0; int pageLength = page?.length ?? 0;
int pageCount = page?.count ?? 0; int pageCount = page?.count ?? 0;
@ -263,10 +368,13 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW
@override @override
Widget build (BuildContext context) { Widget build (BuildContext context) {
return Column( List<Widget> children = [];
mainAxisAlignment: MainAxisAlignment.start,
children: [ if (widget.showSearch) {
buildSearchInput(context), children.add(buildSearchInput(context));
}
children.add(
Expanded( Expanded(
child: CustomScrollView( child: CustomScrollView(
shrinkWrap: true, shrinkWrap: true,
@ -288,7 +396,11 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW
] ]
) )
) )
] );
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: children,
); );
} }

View File

@ -606,7 +606,8 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
); );
case 1: case 1:
return PaginatedStockItemList( return PaginatedStockItemList(
{"part": "${part.pk}"} {"part": "${part.pk}"},
true,
); );
case 2: case 2:
return Center( return Center(

View File

@ -1,13 +1,15 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/api.dart";
import "package:inventree/l10.dart";
import "package:inventree/inventree/model.dart"; import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/part.dart"; import "package:inventree/inventree/part.dart";
import "package:inventree/widget/paginator.dart"; import "package:inventree/widget/paginator.dart";
import "package:inventree/widget/part_detail.dart"; import "package:inventree/widget/part_detail.dart";
import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/api.dart";
import "package:inventree/preferences.dart";
import "package:inventree/l10.dart";
class PartList extends StatefulWidget { class PartList extends StatefulWidget {
@ -31,35 +33,43 @@ class _PartListState extends RefreshableState<PartList> {
final Map<String, String> filters; final Map<String, String> filters;
bool showFilterOptions = false;
@override @override
String getAppBarTitle(BuildContext context) => title.isNotEmpty ? title : L10().parts; String getAppBarTitle(BuildContext context) => title.isNotEmpty ? title : L10().parts;
@override @override
Widget getBody(BuildContext context) { List<Widget> getAppBarActions(BuildContext context) => [
return PaginatedPartList(filters); IconButton(
} icon: FaIcon(FontAwesomeIcons.filter),
onPressed: () async {
} setState(() {
showFilterOptions = !showFilterOptions;
});
class PaginatedPartList extends StatefulWidget { },
)
const PaginatedPartList(this.filters, {this.onTotalChanged}); ];
final Map<String, String> filters;
final Function(int)? onTotalChanged;
@override @override
_PaginatedPartListState createState() => _PaginatedPartListState(filters, onTotalChanged); Widget getBody(BuildContext context) {
return PaginatedPartList(filters, showFilterOptions);
}
}
class PaginatedPartList extends PaginatedSearchWidget {
const PaginatedPartList(Map<String, String> filters, bool showSearch) : super(filters: filters, showSearch: showSearch);
@override
_PaginatedPartListState createState() => _PaginatedPartListState();
} }
class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> { class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> {
_PaginatedPartListState(Map<String, String> filters, this.onTotalChanged) : super(filters); _PaginatedPartListState() : super();
Function(int)? onTotalChanged;
@override @override
String get prefix => "part_"; String get prefix => "part_";
@ -71,13 +81,42 @@ class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> {
"IPN": L10().internalPartNumber, "IPN": L10().internalPartNumber,
}; };
@override
Map<String, Map<String, dynamic>> get filterOptions => {
"cascade": {
"default": true,
"label": L10().includeSubcategories,
"help_text": L10().includeSubcategoriesDetail,
},
"active": {
"label": L10().filterActive,
"help_text": L10().filterActiveDetail,
},
"assembly": {
"label": L10().filterAssembly,
"help_text": L10().filterAssemblyDetail
},
"component": {
"label": L10().filterComponent,
"help_text": L10().filterComponentDetail,
},
"is_template": {
"label": L10().filterTemplate,
"help_text": L10().filterTemplateDetail
},
"virtual": {
"label": L10().filterVirtual,
"help_text": L10().filterVirtualDetail,
},
"has_stock": {
"label": L10().filterInStock,
"help_text": L10().filterInStockDetail,
}
};
@override @override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async { Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
final bool cascade = await InvenTreeSettingsManager().getBool(INV_PART_SUBCATEGORY, true);
params["cascade"] = "${cascade}";
final page = await InvenTreePart().listPaginated(limit, offset, filters: params); final page = await InvenTreePart().listPaginated(limit, offset, filters: params);
return page; return page;

View File

@ -391,7 +391,7 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
"purchase_order": "${order.pk}" "purchase_order": "${order.pk}"
}; };
return PaginatedStockItemList(filters); return PaginatedStockItemList(filters, true);
default: default:
return ListView(); return ListView();

View File

@ -1,4 +1,5 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/inventree/company.dart"; import "package:inventree/inventree/company.dart";
import "package:inventree/inventree/model.dart"; import "package:inventree/inventree/model.dart";
@ -29,32 +30,43 @@ class _PurchaseOrderListWidgetState extends RefreshableState<PurchaseOrderListWi
final Map<String, String> filters; final Map<String, String> filters;
bool showFilterOptions = false;
@override @override
String getAppBarTitle(BuildContext context) => L10().purchaseOrders; String getAppBarTitle(BuildContext context) => L10().purchaseOrders;
@override @override
Widget getBody(BuildContext context) { List<Widget> getAppBarActions(BuildContext context) => [
return PaginatedPurchaseOrderList(filters); IconButton(
} icon: FaIcon(FontAwesomeIcons.filter),
} onPressed: () async {
setState(() {
showFilterOptions = !showFilterOptions;
class PaginatedPurchaseOrderList extends StatefulWidget { });
},
const PaginatedPurchaseOrderList(this.filters); )
];
final Map<String, String> filters;
@override @override
_PaginatedPurchaseOrderListState createState() => _PaginatedPurchaseOrderListState(filters); Widget getBody(BuildContext context) {
return PaginatedPurchaseOrderList(filters, showFilterOptions);
}
}
class PaginatedPurchaseOrderList extends PaginatedSearchWidget {
const PaginatedPurchaseOrderList(Map<String, String> filters, bool showSearch) : super(filters: filters, showSearch: showSearch);
@override
_PaginatedPurchaseOrderListState createState() => _PaginatedPurchaseOrderListState();
} }
class _PaginatedPurchaseOrderListState extends PaginatedSearchState<PaginatedPurchaseOrderList> { class _PaginatedPurchaseOrderListState extends PaginatedSearchState<PaginatedPurchaseOrderList> {
_PaginatedPurchaseOrderListState(Map<String, String> filters) : super(filters); _PaginatedPurchaseOrderListState() : super();
// Purchase order prefix // Purchase order prefix
String _poPrefix = ""; String _poPrefix = "";

View File

@ -9,9 +9,7 @@ import "package:flutter/material.dart";
mixin BaseWidgetProperties { mixin BaseWidgetProperties {
// Return a list of appBar actions (default = None) // Return a list of appBar actions (default = None)
List<Widget> getAppBarActions(BuildContext context) { List<Widget> getAppBarActions(BuildContext context) => [];
return [];
}
// Return a title for the appBar // Return a title for the appBar
String getAppBarTitle(BuildContext context) { return "--- app bar ---"; } String getAppBarTitle(BuildContext context) { return "--- app bar ---"; }

View File

@ -1,17 +1,19 @@
import "dart:async"; import "dart:async";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/api.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/l10.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/inventree/purchase_order.dart"; import "package:inventree/inventree/purchase_order.dart";
import "package:inventree/inventree/stock.dart";
import "package:inventree/widget/part_list.dart"; import "package:inventree/widget/part_list.dart";
import "package:inventree/widget/purchase_order_list.dart"; import "package:inventree/widget/purchase_order_list.dart";
import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/api.dart";
import "package:inventree/l10.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/inventree/stock.dart";
import "package:inventree/widget/stock_list.dart"; import "package:inventree/widget/stock_list.dart";
import "package:inventree/widget/category_list.dart"; import "package:inventree/widget/category_list.dart";
import "package:inventree/widget/company_list.dart"; import "package:inventree/widget/company_list.dart";
@ -222,21 +224,10 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
// Search input // Search input
tiles.add( tiles.add(
TextFormField( ListTile(
title: TextFormField(
decoration: InputDecoration( decoration: InputDecoration(
hintText: L10().queryEmpty, hintText: L10().queryEmpty,
prefixIcon: IconButton(
icon: FaIcon(FontAwesomeIcons.search),
onPressed: null,
),
suffixIcon: IconButton(
icon: FaIcon(FontAwesomeIcons.backspace, color: Colors.red),
onPressed: () {
searchController.clear();
onSearchTextChanged("", immediate: true);
_focusNode.requestFocus();
}
),
), ),
readOnly: false, readOnly: false,
autofocus: true, autofocus: true,
@ -247,6 +238,18 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
onSearchTextChanged(text); onSearchTextChanged(text);
}, },
), ),
trailing: GestureDetector(
child: FaIcon(
searchController.text.isEmpty ? FontAwesomeIcons.search : FontAwesomeIcons.backspace,
color: searchController.text.isEmpty ? COLOR_CLICK : COLOR_DANGER,
),
onTap: () {
searchController.clear();
onSearchTextChanged("", immediate: true);
},
),
)
); );
String query = searchController.text; String query = searchController.text;
@ -406,8 +409,11 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
if (!isSearching() && results.isEmpty && searchController.text.isNotEmpty) { if (!isSearching() && results.isEmpty && searchController.text.isNotEmpty) {
tiles.add( tiles.add(
ListTile( ListTile(
title: Text(L10().queryNoResults), title: Text(
leading: FaIcon(FontAwesomeIcons.search), L10().queryNoResults,
style: TextStyle(fontStyle: FontStyle.italic),
),
leading: FaIcon(FontAwesomeIcons.searchMinus),
) )
); );
} else { } else {

View File

@ -1,11 +1,11 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/inventree/model.dart"; import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/stock.dart"; import "package:inventree/inventree/stock.dart";
import "package:inventree/widget/paginator.dart"; import "package:inventree/widget/paginator.dart";
import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/l10.dart"; import "package:inventree/l10.dart";
import "package:inventree/preferences.dart";
import "package:inventree/widget/stock_detail.dart"; import "package:inventree/widget/stock_detail.dart";
import "package:inventree/api.dart"; import "package:inventree/api.dart";
@ -27,30 +27,42 @@ class _StockListState extends RefreshableState<StockItemList> {
final Map<String, String> filters; final Map<String, String> filters;
bool showFilterOptions = false;
@override @override
String getAppBarTitle(BuildContext context) => L10().stockItems; String getAppBarTitle(BuildContext context) => L10().stockItems;
@override @override
Widget getBody(BuildContext context) { List<Widget> getAppBarActions(BuildContext context) => [
return PaginatedStockItemList(filters); IconButton(
} icon: FaIcon(FontAwesomeIcons.filter),
} onPressed: () async {
setState(() {
class PaginatedStockItemList extends StatefulWidget { showFilterOptions = !showFilterOptions;
});
const PaginatedStockItemList(this.filters); },
)
final Map<String, String> filters; ];
@override @override
_PaginatedStockItemListState createState() => _PaginatedStockItemListState(filters); Widget getBody(BuildContext context) {
return PaginatedStockItemList(filters, showFilterOptions);
}
}
class PaginatedStockItemList extends PaginatedSearchWidget {
const PaginatedStockItemList(Map<String, String> filters, bool showSearch) : super(filters: filters, showSearch: showSearch);
@override
_PaginatedStockItemListState createState() => _PaginatedStockItemListState();
} }
class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockItemList> { class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockItemList> {
_PaginatedStockItemListState(Map<String, String> filters) : super(filters); _PaginatedStockItemListState() : super();
@override @override
String get prefix => "stock_"; String get prefix => "stock_";
@ -66,14 +78,23 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt
"stocktake_date": L10().lastStocktake, "stocktake_date": L10().lastStocktake,
}; };
@override
Map<String, Map<String, dynamic>> get filterOptions => {
"cascade": {
"default": false,
"label": L10().includeSublocations,
"help_text": L10().includeSublocationsDetail,
"tristate": false,
},
"serialized": {
"label": L10().filterSerialized,
"help_text": L10().filterSerializedDetail,
}
};
@override @override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async { Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
// Do we include stock items from sub-locations?
final bool cascade = await InvenTreeSettingsManager().getBool(INV_STOCK_SUBLOCATION, true);
params["cascade"] = "${cascade}";
final page = await InvenTreeStockItem().listPaginated( final page = await InvenTreeStockItem().listPaginated(
limit, limit,
offset, offset,

View File

@ -63,7 +63,11 @@ void main() {
// List *all* parts // List *all* parts
results = await InvenTreePart().list(); results = await InvenTreePart().list();
assert(results.length == 13); expect(results.length, equals(14));
// List with active filter
results = await InvenTreePart().list(filters: {"active": "true"});
expect(results.length, equals(13));
for (var result in results) { for (var result in results) {
// results must be InvenTreePart instances // results must be InvenTreePart instances

View File

@ -8,7 +8,6 @@ import "package:inventree/preferences.dart";
void main() { void main() {
setUp(() async { setUp(() async {
}); });
group("Settings Tests:", () { group("Settings Tests:", () {
@ -26,5 +25,44 @@ void main() {
expect(await InvenTreeSettingsManager().getValue("abc", "123"), equals("xyz")); expect(await InvenTreeSettingsManager().getValue("abc", "123"), equals("xyz"));
}); });
test("Booleans", () async {
// Tests for boolean values
await InvenTreeSettingsManager().removeValue("chicken");
// Use default values when a setting does not exist
assert(await InvenTreeSettingsManager().getBool("chicken", true) == true);
assert(await InvenTreeSettingsManager().getBool("chicken", false) == false);
// Explicitly set to true
await InvenTreeSettingsManager().setValue("chicken", true);
assert(await InvenTreeSettingsManager().getBool("chicken", false) == true);
// Explicitly set to false
await InvenTreeSettingsManager().setValue("chicken", false);
assert(await InvenTreeSettingsManager().getBool("chicken", true) == false);
});
test("Tri State", () async {
// Tests for tristate values
await InvenTreeSettingsManager().removeValue("dog");
// Use default values when a setting does not exist
for (bool? value in [true, false, null]) {
assert(await InvenTreeSettingsManager().getTriState("dog", value) == value);
}
// Explicitly set to a value
for (bool? value in [true, false, null]) {
await InvenTreeSettingsManager().setValue("dog", value);
assert(await InvenTreeSettingsManager().getTriState("dog", true) == value);
assert(await InvenTreeSettingsManager().getTriState("dog", false) == value);
assert(await InvenTreeSettingsManager().getTriState("dog", null) == value);
}
});
}); });
} }