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:
parent
e03a8561b9
commit
13ebaf43e1
@ -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;
|
||||||
},
|
},
|
||||||
|
@ -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",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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),
|
||||||
|
@ -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 {
|
||||||
|
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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,8 +297,8 @@ 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(),
|
||||||
ListTile(
|
ListTile(
|
||||||
@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
List<Widget> getAppBarActions(BuildContext context) => [
|
||||||
|
IconButton(
|
||||||
|
icon: FaIcon(FontAwesomeIcons.filter),
|
||||||
|
onPressed: () async {
|
||||||
|
setState(() {
|
||||||
|
showFilterOptions = !showFilterOptions;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget getBody(BuildContext context) {
|
Widget getBody(BuildContext context) {
|
||||||
return PaginatedPartList(filters);
|
return PaginatedPartList(filters, showFilterOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PaginatedPartList extends StatefulWidget {
|
class PaginatedPartList extends PaginatedSearchWidget {
|
||||||
|
|
||||||
const PaginatedPartList(this.filters, {this.onTotalChanged});
|
const PaginatedPartList(Map<String, String> filters, bool showSearch) : super(filters: filters, showSearch: showSearch);
|
||||||
|
|
||||||
final Map<String, String> filters;
|
|
||||||
|
|
||||||
final Function(int)? onTotalChanged;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_PaginatedPartListState createState() => _PaginatedPartListState(filters, onTotalChanged);
|
_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;
|
||||||
|
@ -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();
|
||||||
|
@ -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
|
||||||
|
List<Widget> getAppBarActions(BuildContext context) => [
|
||||||
|
IconButton(
|
||||||
|
icon: FaIcon(FontAwesomeIcons.filter),
|
||||||
|
onPressed: () async {
|
||||||
|
setState(() {
|
||||||
|
showFilterOptions = !showFilterOptions;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget getBody(BuildContext context) {
|
Widget getBody(BuildContext context) {
|
||||||
return PaginatedPurchaseOrderList(filters);
|
return PaginatedPurchaseOrderList(filters, showFilterOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PaginatedPurchaseOrderList extends StatefulWidget {
|
class PaginatedPurchaseOrderList extends PaginatedSearchWidget {
|
||||||
|
|
||||||
const PaginatedPurchaseOrderList(this.filters);
|
const PaginatedPurchaseOrderList(Map<String, String> filters, bool showSearch) : super(filters: filters, showSearch: showSearch);
|
||||||
|
|
||||||
final Map<String, String> filters;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_PaginatedPurchaseOrderListState createState() => _PaginatedPurchaseOrderListState(filters);
|
_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 = "";
|
||||||
|
|
||||||
|
@ -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 ---"; }
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
List<Widget> getAppBarActions(BuildContext context) => [
|
||||||
|
IconButton(
|
||||||
|
icon: FaIcon(FontAwesomeIcons.filter),
|
||||||
|
onPressed: () async {
|
||||||
|
setState(() {
|
||||||
|
showFilterOptions = !showFilterOptions;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget getBody(BuildContext context) {
|
Widget getBody(BuildContext context) {
|
||||||
return PaginatedStockItemList(filters);
|
return PaginatedStockItemList(filters, showFilterOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PaginatedStockItemList extends StatefulWidget {
|
class PaginatedStockItemList extends PaginatedSearchWidget {
|
||||||
|
|
||||||
const PaginatedStockItemList(this.filters);
|
const PaginatedStockItemList(Map<String, String> filters, bool showSearch) : super(filters: filters, showSearch: showSearch);
|
||||||
|
|
||||||
final Map<String, String> filters;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_PaginatedStockItemListState createState() => _PaginatedStockItemListState(filters);
|
_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,
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user