mirror of
https://github.com/inventree/inventree-app.git
synced 2025-04-27 21:16:48 +00:00
Stock display (#379)
* Display stock quantity more prominently * Cleanup search widget * Update for stock_detail widget * More tweaks * Change bottom bar icon * Display boolean parameters appropriately * Adds ability to edit part parameters * Bump icon size a bit * Improvements to filter options - Allow filtering by "option" type - To start with, filter stock by status code * Remove debug message * Remove getTriState method - No longer needed - Remove associated unit tests * Adjust filters based on server API version * Muted colors
This commit is contained in:
parent
8076887e39
commit
e9eb84eace
1
.gitignore
vendored
1
.gitignore
vendored
@ -13,6 +13,7 @@ coverage/*
|
|||||||
|
|
||||||
# This file is auto-generated as part of the CI process
|
# This file is auto-generated as part of the CI process
|
||||||
test/coverage_helper_test.dart
|
test/coverage_helper_test.dart
|
||||||
|
InvenTreeSettings.db
|
||||||
|
|
||||||
# Sentry API key
|
# Sentry API key
|
||||||
lib/dsn.dart
|
lib/dsn.dart
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
### -
|
### -
|
||||||
---
|
---
|
||||||
|
|
||||||
|
- Edit part parameters from within the app
|
||||||
|
- Increase visibility of stock quantity in widgets
|
||||||
- Improved filters for stock list
|
- Improved filters for stock list
|
||||||
|
|
||||||
### 0.12.2 - June 2023
|
### 0.12.2 - June 2023
|
||||||
|
@ -241,6 +241,7 @@
|
|||||||
files = (
|
files = (
|
||||||
);
|
);
|
||||||
inputPaths = (
|
inputPaths = (
|
||||||
|
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||||
);
|
);
|
||||||
name = "Thin Binary";
|
name = "Thin Binary";
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
|
15
lib/api.dart
15
lib/api.dart
@ -1332,7 +1332,20 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
static String get staticThumb => "/static/img/blank_image.thumbnail.png";
|
static String get staticThumb => "/static/img/blank_image.thumbnail.png";
|
||||||
|
|
||||||
CachedNetworkImage getThumbnail(String imageUrl, {double size = 40}) => getImage(imageUrl, width: size, height: size);
|
CachedNetworkImage? getThumbnail(String imageUrl, {double size = 40, bool hideIfNull = false}) {
|
||||||
|
|
||||||
|
if (hideIfNull) {
|
||||||
|
if (imageUrl.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getImage(
|
||||||
|
imageUrl,
|
||||||
|
width: size,
|
||||||
|
height: size
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Load image from the InvenTree server,
|
* Load image from the InvenTree server,
|
||||||
|
@ -275,6 +275,7 @@ class APIFormField {
|
|||||||
|
|
||||||
// Construct a widget for this input
|
// Construct a widget for this input
|
||||||
Widget constructField(BuildContext context) {
|
Widget constructField(BuildContext context) {
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "string":
|
case "string":
|
||||||
case "url":
|
case "url":
|
||||||
@ -696,6 +697,14 @@ class APIFormField {
|
|||||||
// Construct a string input element
|
// Construct a string input element
|
||||||
Widget _constructString() {
|
Widget _constructString() {
|
||||||
|
|
||||||
|
if (readOnly) {
|
||||||
|
return ListTile(
|
||||||
|
title: Text(label),
|
||||||
|
subtitle: Text(helpText),
|
||||||
|
trailing: Text(value.toString()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return TextFormField(
|
return TextFormField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: required ? label + "*" : label,
|
labelText: required ? label + "*" : label,
|
||||||
@ -724,12 +733,21 @@ class APIFormField {
|
|||||||
// Construct a boolean input element
|
// Construct a boolean input element
|
||||||
Widget _constructBoolean() {
|
Widget _constructBoolean() {
|
||||||
|
|
||||||
|
bool? initial_value;
|
||||||
|
|
||||||
|
if (value is bool || value == null) {
|
||||||
|
initial_value = value as bool?;
|
||||||
|
} else {
|
||||||
|
String vs = value.toString().toLowerCase();
|
||||||
|
initial_value = ["1", "true", "yes"].contains(vs);
|
||||||
|
}
|
||||||
|
|
||||||
return CheckBoxField(
|
return CheckBoxField(
|
||||||
label: label,
|
label: label,
|
||||||
labelStyle: _labelStyle(),
|
labelStyle: _labelStyle(),
|
||||||
helperText: helpText,
|
helperText: helpText,
|
||||||
helperStyle: _helperStyle(),
|
helperStyle: _helperStyle(),
|
||||||
initial: value as bool?,
|
initial: initial_value,
|
||||||
tristate: (getParameter("tristate") ?? false) as bool,
|
tristate: (getParameter("tristate") ?? false) as bool,
|
||||||
onSaved: (val) {
|
onSaved: (val) {
|
||||||
data["value"] = val;
|
data["value"] = val;
|
||||||
@ -1262,6 +1280,10 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
|||||||
|
|
||||||
for (var field in widget.fields) {
|
for (var field in widget.fields) {
|
||||||
|
|
||||||
|
if (field.readOnly) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (field.isSimple) {
|
if (field.isSimple) {
|
||||||
// Simple top-level field data
|
// Simple top-level field data
|
||||||
data[field.name] = field.data["value"];
|
data[field.name] = field.data["value"];
|
||||||
|
@ -16,6 +16,6 @@ Color get COLOR_ACTION {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Color COLOR_WARNING = Color.fromRGBO(250, 150, 50, 1);
|
const Color COLOR_WARNING = Color.fromRGBO(250, 150, 50, 1);
|
||||||
const Color COLOR_DANGER = Color.fromRGBO(250, 50, 50, 1);
|
const Color COLOR_DANGER = Color.fromRGBO(200, 50, 75, 1);
|
||||||
const Color COLOR_SUCCESS = Color.fromRGBO(50, 250, 50, 1);
|
const Color COLOR_SUCCESS = Color.fromRGBO(100, 200, 75, 1);
|
||||||
const Color COLOR_PROGRESS = Color.fromRGBO(50, 50, 250, 1);
|
const Color COLOR_PROGRESS = Color.fromRGBO(50, 100, 200, 1);
|
@ -133,12 +133,29 @@ class InvenTreePartParameter extends InvenTreeModel {
|
|||||||
@override
|
@override
|
||||||
String get URL => "part/parameter/";
|
String get URL => "part/parameter/";
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String> get rolesRequired => ["part"];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreePartParameter.fromJson(json);
|
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreePartParameter.fromJson(json);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> formFields() {
|
Map<String, dynamic> formFields() {
|
||||||
return {};
|
|
||||||
|
Map<String, dynamic> fields = {
|
||||||
|
"header": {
|
||||||
|
"type": "string",
|
||||||
|
"read_only": true,
|
||||||
|
"label": name,
|
||||||
|
"help_text": description,
|
||||||
|
"value": "",
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "string",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -160,7 +177,11 @@ class InvenTreePartParameter extends InvenTreeModel {
|
|||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get as_bool => value.toLowerCase() == "true";
|
||||||
|
|
||||||
String get units => getString("units", subKey: "template_detail");
|
String get units => getString("units", subKey: "template_detail");
|
||||||
|
|
||||||
|
bool get is_checkbox => getBool("checkbox", subKey: "template_detail", backup: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -23,6 +23,26 @@ class InvenTreeStatusCode {
|
|||||||
// Internal status code data loaded from server
|
// Internal status code data loaded from server
|
||||||
Map<String, dynamic> data = {};
|
Map<String, dynamic> data = {};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Construct a list of "choices" suitable for a form
|
||||||
|
*/
|
||||||
|
List<dynamic> get choices {
|
||||||
|
List<dynamic> _choices = [];
|
||||||
|
|
||||||
|
for (String key in data.keys) {
|
||||||
|
dynamic _entry = data[key];
|
||||||
|
|
||||||
|
if (_entry is Map<String, dynamic>) {
|
||||||
|
_choices.add({
|
||||||
|
"value": _entry["key"],
|
||||||
|
"display_name": _entry["label"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _choices;
|
||||||
|
}
|
||||||
|
|
||||||
// Load status code information from the server
|
// Load status code information from the server
|
||||||
Future<void> load({bool forceReload = false}) async {
|
Future<void> load({bool forceReload = false}) async {
|
||||||
|
|
||||||
|
@ -399,7 +399,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
|
|
||||||
double get quantity => getDouble("quantity");
|
double get quantity => getDouble("quantity");
|
||||||
|
|
||||||
String quantityString({bool includeUnits = false}){
|
String quantityString({bool includeUnits = true}){
|
||||||
|
|
||||||
String q = "";
|
String q = "";
|
||||||
|
|
||||||
@ -467,7 +467,13 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
if (serialNumber.isNotEmpty) {
|
if (serialNumber.isNotEmpty) {
|
||||||
return "SN: $serialNumber";
|
return "SN: $serialNumber";
|
||||||
} else {
|
} else {
|
||||||
return simpleNumberString(quantity);
|
String q = simpleNumberString(quantity);
|
||||||
|
|
||||||
|
if (units.isNotEmpty) {
|
||||||
|
q += " ${units}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return q;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,6 +278,9 @@
|
|||||||
"editNotes": "Edit Notes",
|
"editNotes": "Edit Notes",
|
||||||
"@editNotes": {},
|
"@editNotes": {},
|
||||||
|
|
||||||
|
"editParameter": "Edit Parameter",
|
||||||
|
"@editParameter": {},
|
||||||
|
|
||||||
"editPart": "Edit Part",
|
"editPart": "Edit Part",
|
||||||
"@editPart": {
|
"@editPart": {
|
||||||
"description": "edit part"
|
"description": "edit part"
|
||||||
|
@ -126,7 +126,12 @@ class InvenTreeSettingsManager {
|
|||||||
|
|
||||||
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);
|
dynamic value = await store.record(key).get(await _db);
|
||||||
|
|
||||||
|
// Retrieve value
|
||||||
|
if (value == "__null__") {
|
||||||
|
value = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return backup;
|
return backup;
|
||||||
@ -148,32 +153,11 @@ class InvenTreeSettingsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// 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
|
// Encode null values as strings
|
||||||
value ??= "null";
|
value ??= "__null__";
|
||||||
|
|
||||||
await store.record(key).put(await _db, value);
|
await store.record(key).put(await _db, value);
|
||||||
}
|
}
|
||||||
|
@ -154,8 +154,8 @@ class CheckBoxField extends FormField<bool> {
|
|||||||
onSaved: onSaved,
|
onSaved: onSaved,
|
||||||
initialValue: initial,
|
initialValue: initial,
|
||||||
builder: (FormFieldState<bool> state) {
|
builder: (FormFieldState<bool> state) {
|
||||||
|
|
||||||
return CheckboxListTile(
|
return CheckboxListTile(
|
||||||
//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,
|
tristate: tristate,
|
||||||
|
@ -38,44 +38,39 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
|
|||||||
// 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
|
// Should be overridden by an implementing subclass
|
||||||
Map<String, Map<String, dynamic>> get filterOptions => {};
|
Map<String, Map<String, dynamic>> get filterOptions => {};
|
||||||
|
|
||||||
// Return the boolean value of a particular boolean filter
|
// Return the boolean value of a particular boolean filter
|
||||||
Future<bool?> getBooleanFilterValue(String key) async {
|
Future<dynamic> getFilterValue(String key) async {
|
||||||
key = "${prefix}bool_${key}";
|
key = "${prefix}filter_${key}";
|
||||||
|
|
||||||
Map<String, dynamic> opts = filterOptions[key] ?? {};
|
Map<String, dynamic> opts = filterOptions[key] ?? {};
|
||||||
|
dynamic backup = opts["default"];
|
||||||
|
final result = await InvenTreeSettingsManager().getValue(key, backup);
|
||||||
|
|
||||||
bool? backup;
|
|
||||||
dynamic v = opts["default"];
|
|
||||||
|
|
||||||
if (v is bool) {
|
|
||||||
backup = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
final result = await InvenTreeSettingsManager().getTriState(key, backup);
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the boolean value of a particular boolean filter
|
// Set the boolean value of a particular boolean filter
|
||||||
Future<void> setBooleanFilterValue(String key, bool? value) async {
|
Future<void> setFilterValue(String key, dynamic value) async {
|
||||||
key = "${prefix}bool_${key}";
|
key = "${prefix}filter_${key}";
|
||||||
await InvenTreeSettingsManager().setValue(key, value);
|
await InvenTreeSettingsManager().setValue(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct the boolean filter options for this list
|
// Construct the boolean filter options for this list
|
||||||
Future<Map<String, String>> constructBooleanFilters() async {
|
Future<Map<String, String>> constructFilters() async {
|
||||||
|
|
||||||
Map<String, String> f = {};
|
Map<String, String> f = {};
|
||||||
|
|
||||||
for (String k in filterOptions.keys) {
|
for (String k in filterOptions.keys) {
|
||||||
bool? value = await getBooleanFilterValue(k);
|
dynamic value = await getFilterValue(k);
|
||||||
|
|
||||||
if (value is bool) {
|
// Skip null values
|
||||||
f[k] = value ? "true" : "false";
|
if (value == null) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
f[k] = value.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return f;
|
return f;
|
||||||
@ -164,7 +159,7 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add in boolean filter options
|
// Add in selected filter options
|
||||||
for (String key in filterOptions.keys) {
|
for (String key in filterOptions.keys) {
|
||||||
Map<String, dynamic> opts = filterOptions[key] ?? {};
|
Map<String, dynamic> opts = filterOptions[key] ?? {};
|
||||||
|
|
||||||
@ -172,17 +167,18 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
|
|||||||
String label = (opts["label"] ?? key) as String;
|
String label = (opts["label"] ?? key) as String;
|
||||||
String? help_text = opts["help_text"] as String?;
|
String? help_text = opts["help_text"] as String?;
|
||||||
|
|
||||||
|
List<dynamic> choices = (opts["choices"] ?? []) as List<dynamic>;
|
||||||
|
|
||||||
bool tristate = (opts["tristate"] ?? true) as bool;
|
bool tristate = (opts["tristate"] ?? true) as bool;
|
||||||
|
|
||||||
bool? v = await getBooleanFilterValue(key);
|
dynamic v = await getFilterValue(key);
|
||||||
|
|
||||||
// Prevent null value if not tristate
|
// Prevent null value if not tristate
|
||||||
if (!tristate && v == null) {
|
if (!tristate && v == null) {
|
||||||
v = false;
|
v = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add in the particular field
|
Map<String, dynamic> filter = {
|
||||||
fields[key] = {
|
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"display_name": label,
|
"display_name": label,
|
||||||
"label": label,
|
"label": label,
|
||||||
@ -190,6 +186,16 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
|
|||||||
"value": v,
|
"value": v,
|
||||||
"tristate": (opts["tristate"] ?? true) as bool,
|
"tristate": (opts["tristate"] ?? true) as bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (choices.isNotEmpty) {
|
||||||
|
// Configure as a choice input
|
||||||
|
filter["type"] = "choice";
|
||||||
|
filter["choices"] = choices;
|
||||||
|
|
||||||
|
filter.remove("tristate");
|
||||||
|
}
|
||||||
|
|
||||||
|
fields[key] = filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Launch an interactive form for the user to select options
|
// Launch an interactive form for the user to select options
|
||||||
@ -211,16 +217,7 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
|
|||||||
|
|
||||||
// Save boolean fields
|
// Save boolean fields
|
||||||
for (String key in filterOptions.keys) {
|
for (String key in filterOptions.keys) {
|
||||||
|
await setFilterValue(key, data[key]);
|
||||||
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
|
||||||
@ -293,7 +290,7 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
|
|||||||
params["ordering"] = o;
|
params["ordering"] = o;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String> f = await constructBooleanFilters();
|
Map<String, String> f = await constructFilters();
|
||||||
|
|
||||||
if (f.isNotEmpty) {
|
if (f.isNotEmpty) {
|
||||||
params.addAll(f);
|
params.addAll(f);
|
||||||
@ -348,6 +345,10 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
|
|||||||
void updateSearchTerm() {
|
void updateSearchTerm() {
|
||||||
searchTerm = searchController.text;
|
searchTerm = searchController.text;
|
||||||
_pagingController.refresh();
|
_pagingController.refresh();
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to construct a single paginated item
|
// Function to construct a single paginated item
|
||||||
@ -409,19 +410,19 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
|
|||||||
*/
|
*/
|
||||||
Widget buildSearchInput(BuildContext context) {
|
Widget buildSearchInput(BuildContext context) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
trailing: orderingOptions.isEmpty ? null : GestureDetector(
|
leading: orderingOptions.isEmpty ? null : GestureDetector(
|
||||||
child: FaIcon(FontAwesomeIcons.sort, color: COLOR_ACTION),
|
child: Icon(Icons.filter_list, color: COLOR_ACTION, size: 32),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
_saveOrderingOptions(context);
|
_saveOrderingOptions(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
leading: GestureDetector(
|
trailing: GestureDetector(
|
||||||
child: FaIcon(
|
child: FaIcon(
|
||||||
searchController.text.isEmpty ? FontAwesomeIcons.magnifyingGlass : FontAwesomeIcons.deleteLeft,
|
searchController.text.isEmpty ? FontAwesomeIcons.magnifyingGlass : FontAwesomeIcons.deleteLeft,
|
||||||
color: searchController.text.isNotEmpty ? COLOR_DANGER : null,
|
color: searchController.text.isNotEmpty ? COLOR_DANGER : COLOR_ACTION,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (searchController.text.isEmpty) {
|
if (searchController.text.isNotEmpty) {
|
||||||
searchController.clear();
|
searchController.clear();
|
||||||
}
|
}
|
||||||
updateSearchTerm();
|
updateSearchTerm();
|
||||||
|
@ -228,20 +228,6 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Toggle the "star" status of this paricular part
|
|
||||||
*/
|
|
||||||
Future <void> _toggleStar(BuildContext context) async {
|
|
||||||
|
|
||||||
if (InvenTreePart().canView) {
|
|
||||||
showLoadingOverlay(context);
|
|
||||||
await part.update(values: {"starred": "${!part.starred}"});
|
|
||||||
hideLoadingOverlay();
|
|
||||||
refresh(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _editPartDialog(BuildContext context) {
|
void _editPartDialog(BuildContext context) {
|
||||||
|
|
||||||
part.editForm(
|
part.editForm(
|
||||||
@ -259,13 +245,11 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text("${part.fullname}"),
|
title: Text("${part.fullname}"),
|
||||||
subtitle: Text("${part.description}"),
|
subtitle: Text("${part.description}"),
|
||||||
trailing: IconButton(
|
trailing: Text(
|
||||||
icon: FaIcon(part.starred ? FontAwesomeIcons.solidStar : FontAwesomeIcons.star,
|
part.stockString(),
|
||||||
color: part.starred ? Colors.yellowAccent : null,
|
style: TextStyle(
|
||||||
),
|
fontSize: 20,
|
||||||
onPressed: () {
|
)
|
||||||
_toggleStar(context);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
leading: GestureDetector(
|
leading: GestureDetector(
|
||||||
child: api.getImage(part.thumbnail),
|
child: api.getImage(part.thumbnail),
|
||||||
|
@ -133,7 +133,13 @@ class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> {
|
|||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(part.fullname),
|
title: Text(part.fullname),
|
||||||
subtitle: Text(part.description),
|
subtitle: Text(part.description),
|
||||||
trailing: Text(part.stockString()),
|
trailing: Text(
|
||||||
|
part.stockString(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold
|
||||||
|
)
|
||||||
|
),
|
||||||
leading: InvenTreeAPI().getThumbnail(part.thumbnail),
|
leading: InvenTreeAPI().getThumbnail(part.thumbnail),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
|
Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
|
||||||
|
@ -4,6 +4,7 @@ import "package:inventree/inventree/model.dart";
|
|||||||
import "package:inventree/l10.dart";
|
import "package:inventree/l10.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/progress.dart";
|
||||||
import "package:inventree/widget/refreshable_state.dart";
|
import "package:inventree/widget/refreshable_state.dart";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -75,7 +76,7 @@ class _PaginatedParameterState extends PaginatedSearchState<PaginatedParameterLi
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, String> get orderingOptions => {
|
Map<String, String> get orderingOptions => {
|
||||||
// TODO
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -91,6 +92,22 @@ class _PaginatedParameterState extends PaginatedSearchState<PaginatedParameterLi
|
|||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> editParameter(InvenTreePartParameter parameter) async {
|
||||||
|
|
||||||
|
// Checkbox values are handled separately
|
||||||
|
if (parameter.is_checkbox) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
parameter.editForm(
|
||||||
|
context,
|
||||||
|
L10().editParameter,
|
||||||
|
onSuccess: (data) async {
|
||||||
|
updateSearchTerm();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildItem(BuildContext context, InvenTreeModel model) {
|
Widget buildItem(BuildContext context, InvenTreeModel model) {
|
||||||
|
|
||||||
@ -99,7 +116,28 @@ class _PaginatedParameterState extends PaginatedSearchState<PaginatedParameterLi
|
|||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(parameter.name),
|
title: Text(parameter.name),
|
||||||
subtitle: Text(parameter.description),
|
subtitle: Text(parameter.description),
|
||||||
trailing: Text(parameter.valueString),
|
trailing: parameter.is_checkbox
|
||||||
|
? Switch(
|
||||||
|
value: parameter.as_bool,
|
||||||
|
onChanged: (bool value) {
|
||||||
|
if (parameter.canEdit) {
|
||||||
|
showLoadingOverlay(context);
|
||||||
|
parameter.update(
|
||||||
|
values: {
|
||||||
|
"data": value.toString()
|
||||||
|
}
|
||||||
|
).then((value) async{
|
||||||
|
hideLoadingOverlay();
|
||||||
|
updateSearchTerm();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) : Text(parameter.valueString),
|
||||||
|
onTap: parameter.is_checkbox ? null : () async {
|
||||||
|
if (parameter.canEdit) {
|
||||||
|
editParameter(parameter);
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -71,7 +71,7 @@ mixin BaseWidgetProperties {
|
|||||||
*/
|
*/
|
||||||
BottomAppBar? buildBottomAppBar(BuildContext context, GlobalKey<ScaffoldState> key) {
|
BottomAppBar? buildBottomAppBar(BuildContext context, GlobalKey<ScaffoldState> key) {
|
||||||
|
|
||||||
const double iconSize = 32;
|
const double iconSize = 40;
|
||||||
|
|
||||||
List<Widget> icons = [
|
List<Widget> icons = [
|
||||||
IconButton(
|
IconButton(
|
||||||
@ -98,7 +98,7 @@ mixin BaseWidgetProperties {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.qr_code_scanner, color: COLOR_ACTION),
|
icon: Icon(Icons.barcode_reader, color: COLOR_ACTION),
|
||||||
iconSize: iconSize,
|
iconSize: iconSize,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (InvenTreeAPI().checkConnection()) {
|
if (InvenTreeAPI().checkConnection()) {
|
||||||
|
@ -577,8 +577,9 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
subtitle: Text("${widget.item.partDescription}"),
|
subtitle: Text("${widget.item.partDescription}"),
|
||||||
leading: InvenTreeAPI().getThumbnail(widget.item.partImage),
|
leading: InvenTreeAPI().getThumbnail(widget.item.partImage),
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
api.StockStatus.label(widget.item.status),
|
widget.item.quantityString(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
color: api.StockStatus.color(widget.item.status),
|
color: api.StockStatus.color(widget.item.status),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@ -615,6 +616,41 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
return tiles;
|
return tiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Location information
|
||||||
|
if ((widget.item.locationId > 0) && (widget.item.locationName.isNotEmpty)) {
|
||||||
|
tiles.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().stockLocation),
|
||||||
|
subtitle: Text("${widget.item.locationPathString}"),
|
||||||
|
leading: FaIcon(
|
||||||
|
FontAwesomeIcons.locationDot,
|
||||||
|
color: COLOR_ACTION,
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
if (widget.item.locationId > 0) {
|
||||||
|
|
||||||
|
showLoadingOverlay(context);
|
||||||
|
var loc = await InvenTreeStockLocation().get(widget.item.locationId);
|
||||||
|
hideLoadingOverlay();
|
||||||
|
|
||||||
|
if (loc is InvenTreeStockLocation) {
|
||||||
|
Navigator.push(context, MaterialPageRoute(
|
||||||
|
builder: (context) => LocationDisplayWidget(loc)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tiles.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().stockLocation),
|
||||||
|
leading: FaIcon(FontAwesomeIcons.locationDot),
|
||||||
|
subtitle: Text(L10().locationNotSet),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Quantity information
|
// Quantity information
|
||||||
if (widget.item.isSerialized()) {
|
if (widget.item.isSerialized()) {
|
||||||
tiles.add(
|
tiles.add(
|
||||||
@ -634,40 +670,19 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Location information
|
// Stock item status information
|
||||||
if ((widget.item.locationId > 0) && (widget.item.locationName.isNotEmpty)) {
|
tiles.add(
|
||||||
tiles.add(
|
ListTile(
|
||||||
ListTile(
|
title: Text(L10().status),
|
||||||
title: Text(L10().stockLocation),
|
leading: FaIcon(FontAwesomeIcons.circleInfo),
|
||||||
subtitle: Text("${widget.item.locationPathString}"),
|
trailing: Text(
|
||||||
leading: FaIcon(
|
api.StockStatus.label(widget.item.status),
|
||||||
FontAwesomeIcons.locationDot,
|
style: TextStyle(
|
||||||
color: COLOR_ACTION,
|
color: api.StockStatus.color(widget.item.status),
|
||||||
),
|
|
||||||
onTap: () async {
|
|
||||||
if (widget.item.locationId > 0) {
|
|
||||||
|
|
||||||
showLoadingOverlay(context);
|
|
||||||
var loc = await InvenTreeStockLocation().get(widget.item.locationId);
|
|
||||||
hideLoadingOverlay();
|
|
||||||
|
|
||||||
if (loc is InvenTreeStockLocation) {
|
|
||||||
Navigator.push(context, MaterialPageRoute(
|
|
||||||
builder: (context) => LocationDisplayWidget(loc)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
tiles.add(
|
|
||||||
ListTile(
|
|
||||||
title: Text(L10().stockLocation),
|
|
||||||
leading: FaIcon(FontAwesomeIcons.locationDot),
|
|
||||||
subtitle: Text(L10().locationNotSet),
|
|
||||||
)
|
)
|
||||||
);
|
)
|
||||||
}
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Supplier part information (if available)
|
// Supplier part information (if available)
|
||||||
if (widget.item.supplierPartId > 0) {
|
if (widget.item.supplierPartId > 0) {
|
||||||
@ -676,7 +691,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
title: Text(L10().supplierPart),
|
title: Text(L10().supplierPart),
|
||||||
subtitle: Text(widget.item.supplierSKU),
|
subtitle: Text(widget.item.supplierSKU),
|
||||||
leading: FaIcon(FontAwesomeIcons.building, color: COLOR_ACTION),
|
leading: FaIcon(FontAwesomeIcons.building, color: COLOR_ACTION),
|
||||||
trailing: InvenTreeAPI().getThumbnail(widget.item.supplierImage),
|
trailing: InvenTreeAPI().getThumbnail(widget.item.supplierImage, hideIfNull: true),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
showLoadingOverlay(context);
|
showLoadingOverlay(context);
|
||||||
var sp = await InvenTreeSupplierPart().get(
|
var sp = await InvenTreeSupplierPart().get(
|
||||||
|
@ -79,24 +79,37 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt
|
|||||||
};
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, Map<String, dynamic>> get filterOptions => {
|
Map<String, Map<String, dynamic>> get filterOptions {
|
||||||
"in_stock": {
|
Map<String, Map<String, dynamic>> filters = {
|
||||||
"default": true,
|
"in_stock": {
|
||||||
"label": L10().filterInStock,
|
"default": true,
|
||||||
"help_text": L10().filterInStockDetail,
|
"label": L10().filterInStock,
|
||||||
"tristate": true,
|
"help_text": L10().filterInStockDetail,
|
||||||
},
|
"tristate": true,
|
||||||
"cascade": {
|
},
|
||||||
"default": false,
|
"cascade": {
|
||||||
"label": L10().includeSublocations,
|
"default": false,
|
||||||
"help_text": L10().includeSublocationsDetail,
|
"label": L10().includeSublocations,
|
||||||
"tristate": false,
|
"help_text": L10().includeSublocationsDetail,
|
||||||
},
|
"tristate": false,
|
||||||
"serialized": {
|
},
|
||||||
"label": L10().filterSerialized,
|
"serialized": {
|
||||||
"help_text": L10().filterSerializedDetail,
|
"label": L10().filterSerialized,
|
||||||
|
"help_text": L10().filterSerializedDetail,
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"label": L10().status,
|
||||||
|
"help_text": L10().statusCode,
|
||||||
|
"choices": InvenTreeAPI().StockStatus.choices,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!InvenTreeAPI().supportsStatusLabelEndpoints) {
|
||||||
|
filters.remove("status");
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
return filters;
|
||||||
|
}
|
||||||
|
|
||||||
@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 {
|
||||||
@ -125,6 +138,7 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt
|
|||||||
trailing: Text("${item.displayQuantity}",
|
trailing: Text("${item.displayQuantity}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
color: InvenTreeAPI().StockStatus.color(item.status),
|
color: InvenTreeAPI().StockStatus.color(item.status),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -47,25 +47,5 @@ void main() {
|
|||||||
assert(await InvenTreeSettingsManager().getBool("chicken", true) == 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