mirror of
https://github.com/inventree/inventree-app.git
synced 2025-04-28 13:36:50 +00:00
commit
51f3be899b
@ -1,6 +1,17 @@
|
|||||||
## InvenTree App Release Notes
|
## InvenTree App Release Notes
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 0.3.1 - July 2021
|
||||||
|
---
|
||||||
|
|
||||||
|
- Adds new "API driven" forms
|
||||||
|
- Improvements for Part editing form
|
||||||
|
- Improvements for PartCategory editing form
|
||||||
|
- Improvements for StockLocation editing form
|
||||||
|
- Adds ability to edit StockItem
|
||||||
|
- Display purchase price (where available) for StockItem
|
||||||
|
- Updated translations
|
||||||
|
|
||||||
### 0.2.10 - July 2021
|
### 0.2.10 - July 2021
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -6,8 +6,8 @@ export "COCOAPODS_PARALLEL_CODE_SIGN=true"
|
|||||||
export "FLUTTER_TARGET=lib\main.dart"
|
export "FLUTTER_TARGET=lib\main.dart"
|
||||||
export "FLUTTER_BUILD_DIR=build"
|
export "FLUTTER_BUILD_DIR=build"
|
||||||
export "SYMROOT=${SOURCE_ROOT}/../build\ios"
|
export "SYMROOT=${SOURCE_ROOT}/../build\ios"
|
||||||
export "FLUTTER_BUILD_NAME=0.2.10"
|
export "FLUTTER_BUILD_NAME=0.3.1"
|
||||||
export "FLUTTER_BUILD_NUMBER=18"
|
export "FLUTTER_BUILD_NUMBER=19"
|
||||||
export "DART_OBFUSCATION=false"
|
export "DART_OBFUSCATION=false"
|
||||||
export "TRACK_WIDGET_CREATION=false"
|
export "TRACK_WIDGET_CREATION=false"
|
||||||
export "TREE_SHAKE_ICONS=false"
|
export "TREE_SHAKE_ICONS=false"
|
||||||
|
27
lib/api.dart
27
lib/api.dart
@ -2,20 +2,20 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
import 'package:inventree/inventree/sentry.dart';
|
|
||||||
import 'package:inventree/user_profile.dart';
|
|
||||||
import 'package:inventree/widget/snacks.dart';
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
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:flutter_cache_manager/flutter_cache_manager.dart';
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
|
|
||||||
import 'package:inventree/widget/dialogs.dart';
|
import 'package:inventree/widget/dialogs.dart';
|
||||||
import 'package:inventree/l10.dart';
|
import 'package:inventree/l10.dart';
|
||||||
|
import 'package:inventree/inventree/sentry.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:inventree/user_profile.dart';
|
||||||
|
import 'package:inventree/widget/snacks.dart';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -93,7 +93,7 @@ class InvenTreeFileService extends FileService {
|
|||||||
class InvenTreeAPI {
|
class InvenTreeAPI {
|
||||||
|
|
||||||
// Minimum required API version for server
|
// Minimum required API version for server
|
||||||
static const _minApiVersion = 6;
|
static const _minApiVersion = 7;
|
||||||
|
|
||||||
// Endpoint for requesting an API token
|
// Endpoint for requesting an API token
|
||||||
static const _URL_GET_TOKEN = "user/token/";
|
static const _URL_GET_TOKEN = "user/token/";
|
||||||
@ -128,7 +128,13 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
String get imageUrl => _makeUrl("/image/");
|
String get imageUrl => _makeUrl("/image/");
|
||||||
|
|
||||||
String makeApiUrl(String endpoint) => _makeUrl("/api/" + endpoint);
|
String makeApiUrl(String endpoint) {
|
||||||
|
if (endpoint.startsWith("/api/") || endpoint.startsWith("api/")) {
|
||||||
|
return _makeUrl(endpoint);
|
||||||
|
} else {
|
||||||
|
return _makeUrl("/api/" + endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String makeUrl(String endpoint) => _makeUrl(endpoint);
|
String makeUrl(String endpoint) => _makeUrl(endpoint);
|
||||||
|
|
||||||
@ -431,7 +437,7 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
|
|
||||||
// Perform a PATCH request
|
// Perform a PATCH request
|
||||||
Future<APIResponse> patch(String url, {Map<String, String> body = const {}, int expectedStatusCode=200}) async {
|
Future<APIResponse> patch(String url, {Map<String, String> body = const {}, int? expectedStatusCode}) async {
|
||||||
var _body = Map<String, String>();
|
var _body = Map<String, String>();
|
||||||
|
|
||||||
// Copy across provided data
|
// Copy across provided data
|
||||||
@ -593,8 +599,6 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
Uri? _uri = Uri.tryParse(_url);
|
Uri? _uri = Uri.tryParse(_url);
|
||||||
|
|
||||||
print("apiRequest ${method} -> ${url}");
|
|
||||||
|
|
||||||
if (_uri == null) {
|
if (_uri == null) {
|
||||||
showServerError(L10().invalidHost, L10().invalidHostDetails);
|
showServerError(L10().invalidHost, L10().invalidHostDetails);
|
||||||
return null;
|
return null;
|
||||||
@ -621,12 +625,15 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
return _request;
|
return _request;
|
||||||
} on SocketException catch (error) {
|
} on SocketException catch (error) {
|
||||||
|
print("SocketException at ${url}: ${error.toString()}");
|
||||||
showServerError(L10().connectionRefused, error.toString());
|
showServerError(L10().connectionRefused, error.toString());
|
||||||
return null;
|
return null;
|
||||||
} on TimeoutException {
|
} on TimeoutException {
|
||||||
|
print("TimeoutException at ${url}");
|
||||||
showTimeoutError();
|
showTimeoutError();
|
||||||
return null;
|
return null;
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
|
print("Server error at ${url}: ${error.toString()}");
|
||||||
showServerError(L10().serverError, error.toString());
|
showServerError(L10().serverError, error.toString());
|
||||||
sentryReportError(error, stackTrace);
|
sentryReportError(error, stackTrace);
|
||||||
return null;
|
return null;
|
||||||
|
681
lib/api_form.dart
Normal file
681
lib/api_form.dart
Normal file
@ -0,0 +1,681 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:dropdown_search/dropdown_search.dart';
|
||||||
|
|
||||||
|
import 'package:inventree/api.dart';
|
||||||
|
import 'package:inventree/app_colors.dart';
|
||||||
|
import 'package:inventree/inventree/part.dart';
|
||||||
|
import 'package:inventree/inventree/stock.dart';
|
||||||
|
import 'package:inventree/widget/fields.dart';
|
||||||
|
import 'package:inventree/l10.dart';
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:inventree/widget/snacks.dart';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Class that represents a single "form field",
|
||||||
|
* defined by the InvenTree API
|
||||||
|
*/
|
||||||
|
class APIFormField {
|
||||||
|
|
||||||
|
final _controller = TextEditingController();
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
APIFormField(this.name, this.data);
|
||||||
|
|
||||||
|
// Name of this field
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
// JSON data which defines the field
|
||||||
|
final dynamic data;
|
||||||
|
|
||||||
|
dynamic initial_data;
|
||||||
|
|
||||||
|
// Get the "api_url" associated with a related field
|
||||||
|
String get api_url => data["api_url"] ?? "";
|
||||||
|
|
||||||
|
// Get the "model" associated with a related field
|
||||||
|
String get model => data["model"] ?? "";
|
||||||
|
|
||||||
|
// Is this field hidden?
|
||||||
|
bool get hidden => (data['hidden'] ?? false) as bool;
|
||||||
|
|
||||||
|
// Is this field read only?
|
||||||
|
bool get readOnly => (data['read_only'] ?? false) as bool;
|
||||||
|
|
||||||
|
// Get the "value" as a string (look for "default" if not available)
|
||||||
|
dynamic get value => (data['value'] ?? data['default']);
|
||||||
|
|
||||||
|
// Get the "default" as a string
|
||||||
|
dynamic get defaultValue => data['default'];
|
||||||
|
|
||||||
|
Map<String, String> get filters {
|
||||||
|
|
||||||
|
Map<String, String> _filters = {};
|
||||||
|
|
||||||
|
// Start with the provided "model" filters
|
||||||
|
if (data.containsKey("filters")) {
|
||||||
|
|
||||||
|
dynamic f = data["filters"];
|
||||||
|
|
||||||
|
if (f is Map) {
|
||||||
|
f.forEach((key, value) {
|
||||||
|
_filters[key] = value.toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, look at the provided "instance_filters"
|
||||||
|
if (data.containsKey("instance_filters")) {
|
||||||
|
|
||||||
|
dynamic f = data["instance_filters"];
|
||||||
|
|
||||||
|
if (f is Map) {
|
||||||
|
f.forEach((key, value) {
|
||||||
|
_filters[key] = value.toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _filters;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasErrors() => errorMessages().length > 0;
|
||||||
|
|
||||||
|
// Return the error message associated with this field
|
||||||
|
List<String> errorMessages() {
|
||||||
|
List<dynamic> errors = data['errors'] ?? [];
|
||||||
|
|
||||||
|
List<String> messages = [];
|
||||||
|
|
||||||
|
for (dynamic error in errors) {
|
||||||
|
messages.add(error.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this field required?
|
||||||
|
bool get required => (data['required'] ?? false) as bool;
|
||||||
|
|
||||||
|
String get type => (data['type'] ?? '').toString();
|
||||||
|
|
||||||
|
String get label => (data['label'] ?? '').toString();
|
||||||
|
|
||||||
|
String get helpText => (data['help_text'] ?? '').toString();
|
||||||
|
|
||||||
|
String get placeholderText => (data['placeholder'] ?? '').toString();
|
||||||
|
|
||||||
|
List<dynamic> get choices => data["choices"] ?? [];
|
||||||
|
|
||||||
|
Future<void> loadInitialData() async {
|
||||||
|
|
||||||
|
// Only for "related fields"
|
||||||
|
if (type != "related field") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Null value? No point!
|
||||||
|
if (value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int? pk = int.tryParse(value.toString());
|
||||||
|
|
||||||
|
if (pk == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = api_url + "/" + pk.toString() + "/";
|
||||||
|
|
||||||
|
final APIResponse response = await InvenTreeAPI().get(
|
||||||
|
url,
|
||||||
|
params: filters,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.isValid()) {
|
||||||
|
initial_data = response.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct a widget for this input
|
||||||
|
Widget constructField() {
|
||||||
|
switch (type) {
|
||||||
|
case "string":
|
||||||
|
case "url":
|
||||||
|
return _constructString();
|
||||||
|
case "boolean":
|
||||||
|
return _constructBoolean();
|
||||||
|
case "related field":
|
||||||
|
return _constructRelatedField();
|
||||||
|
case "choice":
|
||||||
|
return _constructChoiceField();
|
||||||
|
default:
|
||||||
|
return ListTile(
|
||||||
|
title: Text(
|
||||||
|
"Unsupported field type: '${type}'",
|
||||||
|
style: TextStyle(
|
||||||
|
color: COLOR_DANGER,
|
||||||
|
fontStyle: FontStyle.italic),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _constructChoiceField() {
|
||||||
|
|
||||||
|
dynamic _initial;
|
||||||
|
|
||||||
|
// Check if the current value is within the allowed values
|
||||||
|
for (var opt in choices) {
|
||||||
|
if (opt['value'] == value) {
|
||||||
|
_initial = opt;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DropdownSearch<dynamic>(
|
||||||
|
mode: Mode.BOTTOM_SHEET,
|
||||||
|
showSelectedItem: false,
|
||||||
|
selectedItem: _initial,
|
||||||
|
items: choices,
|
||||||
|
label: label,
|
||||||
|
hint: helpText,
|
||||||
|
onChanged: null,
|
||||||
|
autoFocusSearchBox: true,
|
||||||
|
showClearButton: !required,
|
||||||
|
itemAsString: (dynamic item) {
|
||||||
|
return item['display_name'];
|
||||||
|
},
|
||||||
|
onSaved: (item) {
|
||||||
|
if (item == null) {
|
||||||
|
data['value'] = null;
|
||||||
|
} else {
|
||||||
|
data['value'] = item['value'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct an input for a related field
|
||||||
|
Widget _constructRelatedField() {
|
||||||
|
|
||||||
|
return DropdownSearch<dynamic>(
|
||||||
|
mode: Mode.BOTTOM_SHEET,
|
||||||
|
showSelectedItem: true,
|
||||||
|
selectedItem: initial_data,
|
||||||
|
onFind: (String filter) async {
|
||||||
|
|
||||||
|
Map<String, String> _filters = {};
|
||||||
|
|
||||||
|
filters.forEach((key, value) {
|
||||||
|
_filters[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
_filters["search"] = filter;
|
||||||
|
_filters["offset"] = "0";
|
||||||
|
_filters["limit"] = "25";
|
||||||
|
|
||||||
|
final APIResponse response = await InvenTreeAPI().get(
|
||||||
|
api_url,
|
||||||
|
params: _filters
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.isValid()) {
|
||||||
|
|
||||||
|
List<dynamic> results = [];
|
||||||
|
|
||||||
|
for (var result in response.data['results'] ?? []) {
|
||||||
|
results.add(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label: label,
|
||||||
|
hint: helpText,
|
||||||
|
onChanged: null,
|
||||||
|
showClearButton: !required,
|
||||||
|
itemAsString: (dynamic item) {
|
||||||
|
return item['pathstring'];
|
||||||
|
},
|
||||||
|
dropdownBuilder: (context, item, itemAsString) {
|
||||||
|
return _renderRelatedField(item, true, false);
|
||||||
|
},
|
||||||
|
popupItemBuilder: (context, item, isSelected) {
|
||||||
|
return _renderRelatedField(item, isSelected, true);
|
||||||
|
},
|
||||||
|
onSaved: (item) {
|
||||||
|
if (item != null) {
|
||||||
|
data['value'] = item['pk'] ?? null;
|
||||||
|
} else {
|
||||||
|
data['value'] = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isFilteredOnline: true,
|
||||||
|
showSearchBox: true,
|
||||||
|
autoFocusSearchBox: true,
|
||||||
|
compareFn: (dynamic item, dynamic selectedItem) {
|
||||||
|
// Comparison is based on the PK value
|
||||||
|
|
||||||
|
if (item == null || selectedItem == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item['pk'] == selectedItem['pk'];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _renderRelatedField(dynamic item, bool selected, bool extended) {
|
||||||
|
// Render a "related field" based on the "model" type
|
||||||
|
|
||||||
|
if (item == null) {
|
||||||
|
return Text(
|
||||||
|
helpText,
|
||||||
|
style: TextStyle(
|
||||||
|
fontStyle: FontStyle.italic
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (model) {
|
||||||
|
case "partcategory":
|
||||||
|
|
||||||
|
var cat = InvenTreePartCategory.fromJson(item);
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
title: Text(
|
||||||
|
cat.pathstring,
|
||||||
|
style: TextStyle(fontWeight: selected && extended ? FontWeight.bold : FontWeight.normal)
|
||||||
|
),
|
||||||
|
subtitle: extended ? Text(
|
||||||
|
cat.description,
|
||||||
|
style: TextStyle(fontWeight: selected ? FontWeight.bold : FontWeight.normal),
|
||||||
|
) : null,
|
||||||
|
);
|
||||||
|
case "stocklocation":
|
||||||
|
|
||||||
|
var loc = InvenTreeStockLocation.fromJson(item);
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
title: Text(
|
||||||
|
loc.pathstring,
|
||||||
|
style: TextStyle(fontWeight: selected && extended ? FontWeight.bold : FontWeight.normal)
|
||||||
|
),
|
||||||
|
subtitle: extended ? Text(
|
||||||
|
loc.description,
|
||||||
|
style: TextStyle(fontWeight: selected ? FontWeight.bold : FontWeight.normal),
|
||||||
|
) : null,
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return ListTile(
|
||||||
|
title: Text(
|
||||||
|
"Unsupported model",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: COLOR_DANGER
|
||||||
|
)
|
||||||
|
),
|
||||||
|
subtitle: Text("Model '${model}' rendering not supported"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct a string input element
|
||||||
|
Widget _constructString() {
|
||||||
|
|
||||||
|
return TextFormField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: required ? label + "*" : label,
|
||||||
|
labelStyle: _labelStyle(),
|
||||||
|
helperText: helpText,
|
||||||
|
helperStyle: _helperStyle(),
|
||||||
|
hintText: placeholderText,
|
||||||
|
),
|
||||||
|
initialValue: value ?? '',
|
||||||
|
onSaved: (val) {
|
||||||
|
data["value"] = val;
|
||||||
|
},
|
||||||
|
validator: (value) {
|
||||||
|
if (required && (value == null || value.isEmpty)) {
|
||||||
|
// return L10().valueCannotBeEmpty;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct a boolean input element
|
||||||
|
Widget _constructBoolean() {
|
||||||
|
|
||||||
|
return CheckBoxField(
|
||||||
|
label: label,
|
||||||
|
labelStyle: _labelStyle(),
|
||||||
|
helperText: helpText,
|
||||||
|
helperStyle: _helperStyle(),
|
||||||
|
initial: value,
|
||||||
|
onSaved: (val) {
|
||||||
|
data['value'] = val;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextStyle _labelStyle() {
|
||||||
|
return new TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 18,
|
||||||
|
fontFamily: "arial",
|
||||||
|
color: hasErrors() ? COLOR_DANGER : COLOR_GRAY,
|
||||||
|
fontStyle: FontStyle.normal,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextStyle _helperStyle() {
|
||||||
|
return new TextStyle(
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
color: hasErrors() ? COLOR_DANGER : COLOR_GRAY,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Extract field options from a returned OPTIONS request
|
||||||
|
*/
|
||||||
|
Map<String, dynamic> extractFields(APIResponse response) {
|
||||||
|
|
||||||
|
if (!response.isValid()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.data.containsKey("actions")) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
var actions = response.data["actions"];
|
||||||
|
|
||||||
|
return actions["POST"] ?? actions["PUT"] ?? actions["PATCH"] ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Launch an API-driven form,
|
||||||
|
* which uses the OPTIONS metadata (at the provided URL)
|
||||||
|
* to determine how the form elements should be rendered!
|
||||||
|
*
|
||||||
|
* @param title is the title text to display on the form
|
||||||
|
* @param url is the API URl to make the OPTIONS request to
|
||||||
|
* @param fields is a map of fields to display (with optional overrides)
|
||||||
|
* @param modelData is the (optional) existing modelData
|
||||||
|
* @param method is the HTTP method to use to send the form data to the server (e.g. POST / PATCH)
|
||||||
|
*/
|
||||||
|
|
||||||
|
Future<void> launchApiForm(BuildContext context, String title, String url, Map<String, dynamic> fields, {Map<String, dynamic> modelData = const {}, String method = "PATCH", Function? onSuccess, Function? onCancel}) async {
|
||||||
|
|
||||||
|
var options = await InvenTreeAPI().options(url);
|
||||||
|
|
||||||
|
// Invalid response from server
|
||||||
|
if (!options.isValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var availableFields = extractFields(options);
|
||||||
|
|
||||||
|
if (availableFields.isEmpty) {
|
||||||
|
// User does not have permission to perform this action
|
||||||
|
showSnackIcon(
|
||||||
|
L10().response403,
|
||||||
|
icon: FontAwesomeIcons.userTimes,
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct a list of APIFormField objects
|
||||||
|
List<APIFormField> formFields = [];
|
||||||
|
|
||||||
|
// Iterate through the provided fields we wish to display
|
||||||
|
for (String fieldName in fields.keys) {
|
||||||
|
|
||||||
|
// Check that the field is actually available at the API endpoint
|
||||||
|
if (!availableFields.containsKey(fieldName)) {
|
||||||
|
print("Field '${fieldName}' not available at '${url}'");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var remoteField = availableFields[fieldName] ?? {};
|
||||||
|
var localField = fields[fieldName] ?? {};
|
||||||
|
|
||||||
|
// Override defined field parameters, if provided
|
||||||
|
for (String key in localField.keys) {
|
||||||
|
// Special consideration must be taken here!
|
||||||
|
if (key == "filters") {
|
||||||
|
|
||||||
|
if (!remoteField.containsKey("filters")) {
|
||||||
|
remoteField["filters"] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
var filters = localField["filters"];
|
||||||
|
|
||||||
|
if (filters is Map) {
|
||||||
|
filters.forEach((key, value) {
|
||||||
|
remoteField["filters"][key] = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
remoteField[key] = localField[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update fields with existing model data
|
||||||
|
for (String key in modelData.keys) {
|
||||||
|
|
||||||
|
dynamic value = modelData[key];
|
||||||
|
|
||||||
|
if (availableFields.containsKey(key)) {
|
||||||
|
availableFields[key]['value'] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formFields.add(APIFormField(fieldName, remoteField));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab existing data for each form field
|
||||||
|
for (var field in formFields) {
|
||||||
|
await field.loadInitialData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, launch a new widget!
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (context) => APIFormWidget(
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
formFields,
|
||||||
|
onSuccess: onSuccess,
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class APIFormWidget extends StatefulWidget {
|
||||||
|
|
||||||
|
//! Form title to display
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
//! API URL
|
||||||
|
final String url;
|
||||||
|
|
||||||
|
final List<APIFormField> fields;
|
||||||
|
|
||||||
|
Function? onSuccess;
|
||||||
|
|
||||||
|
APIFormWidget(
|
||||||
|
this.title,
|
||||||
|
this.url,
|
||||||
|
this.fields,
|
||||||
|
{
|
||||||
|
Key? key,
|
||||||
|
this.onSuccess,
|
||||||
|
}
|
||||||
|
) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_APIFormWidgetState createState() => _APIFormWidgetState(title, url, fields, onSuccess);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class _APIFormWidgetState extends State<APIFormWidget> {
|
||||||
|
|
||||||
|
final _formKey = new GlobalKey<FormState>();
|
||||||
|
|
||||||
|
String title;
|
||||||
|
|
||||||
|
String url;
|
||||||
|
|
||||||
|
List<APIFormField> fields;
|
||||||
|
|
||||||
|
Function? onSuccess;
|
||||||
|
|
||||||
|
_APIFormWidgetState(this.title, this.url, this.fields, this.onSuccess) : super();
|
||||||
|
|
||||||
|
List<Widget> _buildForm() {
|
||||||
|
|
||||||
|
List<Widget> widgets = [];
|
||||||
|
|
||||||
|
for (var field in fields) {
|
||||||
|
|
||||||
|
if (field.hidden) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
widgets.add(field.constructField());
|
||||||
|
|
||||||
|
if (field.hasErrors()) {
|
||||||
|
for (String error in field.errorMessages()) {
|
||||||
|
widgets.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
error,
|
||||||
|
style: TextStyle(
|
||||||
|
color: COLOR_DANGER,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return widgets;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _save(BuildContext context) async {
|
||||||
|
|
||||||
|
// Package up the form data
|
||||||
|
Map<String, String> _data = {};
|
||||||
|
|
||||||
|
for (var field in fields) {
|
||||||
|
|
||||||
|
dynamic value = field.value;
|
||||||
|
|
||||||
|
if (value == null) {
|
||||||
|
_data[field.name] = "";
|
||||||
|
} else {
|
||||||
|
_data[field.name] = value.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Handle "POST" forms too!!
|
||||||
|
final response = await InvenTreeAPI().patch(
|
||||||
|
url,
|
||||||
|
body: _data,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.isValid()) {
|
||||||
|
// TODO: Display an error message!
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (response.statusCode) {
|
||||||
|
case 200:
|
||||||
|
case 201:
|
||||||
|
// Form was successfully validated by the server
|
||||||
|
|
||||||
|
// Hide this form
|
||||||
|
Navigator.pop(context);
|
||||||
|
|
||||||
|
// TODO: Display a snackBar
|
||||||
|
|
||||||
|
// Run custom onSuccess function
|
||||||
|
var successFunc = onSuccess;
|
||||||
|
|
||||||
|
if (successFunc != null) {
|
||||||
|
successFunc();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case 400:
|
||||||
|
// Form submission / validation error
|
||||||
|
|
||||||
|
// Update field errors
|
||||||
|
for (var field in fields) {
|
||||||
|
field.data['errors'] = response.data[field.name];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// TODO: Other status codes?
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
// Refresh the form
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(title),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: FaIcon(FontAwesomeIcons.save),
|
||||||
|
onPressed: () {
|
||||||
|
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
_formKey.currentState!.save();
|
||||||
|
|
||||||
|
_save(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
body: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: _buildForm(),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
19
lib/app_colors.dart
Normal file
19
lib/app_colors.dart
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
const Color COLOR_GRAY = Color.fromRGBO(50, 50, 50, 1);
|
||||||
|
const Color COLOR_GRAY_LIGHT = Color.fromRGBO(150, 150, 150, 1);
|
||||||
|
|
||||||
|
const Color COLOR_CLICK = Color.fromRGBO(175, 150, 100, 0.9);
|
||||||
|
|
||||||
|
const Color COLOR_BLUE = Color.fromRGBO(0, 0, 250, 1);
|
||||||
|
|
||||||
|
const Color COLOR_STAR = Color.fromRGBO(250, 250, 100, 1);
|
||||||
|
|
||||||
|
const Color COLOR_WARNING = Color.fromRGBO(250, 150, 50, 1);
|
||||||
|
const Color COLOR_DANGER = Color.fromRGBO(250, 50, 50, 1);
|
||||||
|
const Color COLOR_SUCCESS = Color.fromRGBO(50, 250, 50, 1);
|
||||||
|
const Color COLOR_PROGRESS = Color.fromRGBO(50, 50, 250, 1);
|
||||||
|
|
||||||
|
const Color COLOR_SELECTED = Color.fromRGBO(0, 0, 0, 0.05);
|
@ -271,8 +271,7 @@ class StockItemBarcodeAssignmentHandler extends BarcodeHandler {
|
|||||||
|
|
||||||
final InvenTreeStockItem item;
|
final InvenTreeStockItem item;
|
||||||
|
|
||||||
StockItemBarcodeAssignmentHandler(this.item) {
|
StockItemBarcodeAssignmentHandler(this.item);
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getOverlayText(BuildContext context) => L10().barcodeScanAssign;
|
String getOverlayText(BuildContext context) => L10().barcodeScanAssign;
|
||||||
|
@ -126,8 +126,7 @@ class InvenTreeModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return the API detail endpoint for this Model object
|
// Return the API detail endpoint for this Model object
|
||||||
String get url => "${URL}/${pk}/";
|
String get url => "${URL}/${pk}/".replaceAll("//", "/");
|
||||||
|
|
||||||
|
|
||||||
// Search this Model type in the database
|
// Search this Model type in the database
|
||||||
Future<List<InvenTreeModel>> search(BuildContext context, String searchTerm, {Map<String, String> filters = const {}}) async {
|
Future<List<InvenTreeModel>> search(BuildContext context, String searchTerm, {Map<String, String> filters = const {}}) async {
|
||||||
@ -277,8 +276,6 @@ class InvenTreeModel {
|
|||||||
params[key] = filters[key] ?? '';
|
params[key] = filters[key] ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
print("LIST: $URL ${params.toString()}");
|
|
||||||
|
|
||||||
var response = await api.get(URL, params: params);
|
var response = await api.get(URL, params: params);
|
||||||
|
|
||||||
// A list of "InvenTreeModel" items
|
// A list of "InvenTreeModel" items
|
||||||
@ -288,18 +285,22 @@ class InvenTreeModel {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO - handle possible error cases:
|
dynamic data;
|
||||||
// - No data receieved
|
|
||||||
// - Data is not a list of maps
|
|
||||||
|
|
||||||
for (var d in response.data) {
|
if (response.data is List) {
|
||||||
|
data = response.data;
|
||||||
|
} else if (response.data.containsKey('results')) {
|
||||||
|
data = response.data['results'];
|
||||||
|
} else {
|
||||||
|
data = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var d in data) {
|
||||||
|
|
||||||
// Create a new object (of the current class type
|
// Create a new object (of the current class type
|
||||||
InvenTreeModel obj = createFromJson(d);
|
InvenTreeModel obj = createFromJson(d);
|
||||||
|
|
||||||
if (obj != null) {
|
results.add(obj);
|
||||||
results.add(obj);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
|
@ -60,6 +60,8 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
|
|
||||||
String statusLabel(BuildContext context) {
|
String statusLabel(BuildContext context) {
|
||||||
|
|
||||||
|
// TODO: Delete me - The translated status values are provided by the API!
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case OK:
|
case OK:
|
||||||
return L10().ok;
|
return L10().ok;
|
||||||
@ -220,6 +222,15 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
|
|
||||||
int get partId => jsondata['part'] ?? -1;
|
int get partId => jsondata['part'] ?? -1;
|
||||||
|
|
||||||
|
String get purchasePrice => jsondata['purchase_price'];
|
||||||
|
|
||||||
|
bool get hasPurchasePrice {
|
||||||
|
|
||||||
|
String pp = purchasePrice;
|
||||||
|
|
||||||
|
return pp.isNotEmpty && pp.trim() != "-";
|
||||||
|
}
|
||||||
|
|
||||||
int get trackingItemCount => (jsondata['tracking_items'] ?? 0) as int;
|
int get trackingItemCount => (jsondata['tracking_items'] ?? 0) as int;
|
||||||
|
|
||||||
// Date of last update
|
// Date of last update
|
||||||
@ -476,14 +487,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
expectedStatusCode: 200
|
expectedStatusCode: 200
|
||||||
);
|
);
|
||||||
|
|
||||||
print("Adjustment completed!");
|
return response.isValid();
|
||||||
|
|
||||||
if (response == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stock adjustment succeeded!
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> countStock(BuildContext context, double q, {String? notes}) async {
|
Future<bool> countStock(BuildContext context, double q, {String? notes}) async {
|
||||||
|
2
lib/l10n
2
lib/l10n
@ -1 +1 @@
|
|||||||
Subproject commit af4cd9026a96d44d60f9187119f5ce19c74738d3
|
Subproject commit 46d08c9cc0043113fee5c0d134861c5d12554b71
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:inventree/app_colors.dart';
|
||||||
import 'package:inventree/widget/dialogs.dart';
|
import 'package:inventree/widget/dialogs.dart';
|
||||||
import 'package:inventree/widget/fields.dart';
|
import 'package:inventree/widget/fields.dart';
|
||||||
import 'package:inventree/widget/spinner.dart';
|
import 'package:inventree/widget/spinner.dart';
|
||||||
@ -55,7 +56,7 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
|||||||
key: _addProfileKey,
|
key: _addProfileKey,
|
||||||
callback: () {
|
callback: () {
|
||||||
if (createNew) {
|
if (createNew) {
|
||||||
// TODO - create the new profile...
|
|
||||||
UserProfile profile = UserProfile(
|
UserProfile profile = UserProfile(
|
||||||
name: _name,
|
name: _name,
|
||||||
server: _server,
|
server: _server,
|
||||||
@ -219,7 +220,7 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
|||||||
if ((InvenTreeAPI().profile?.key ?? '') != profile.key) {
|
if ((InvenTreeAPI().profile?.key ?? '') != profile.key) {
|
||||||
return FaIcon(
|
return FaIcon(
|
||||||
FontAwesomeIcons.questionCircle,
|
FontAwesomeIcons.questionCircle,
|
||||||
color: Color.fromRGBO(250, 150, 50, 1)
|
color: COLOR_WARNING
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,17 +228,17 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
|||||||
if (InvenTreeAPI().isConnected()) {
|
if (InvenTreeAPI().isConnected()) {
|
||||||
return FaIcon(
|
return FaIcon(
|
||||||
FontAwesomeIcons.checkCircle,
|
FontAwesomeIcons.checkCircle,
|
||||||
color: Color.fromRGBO(50, 250, 50, 1)
|
color: COLOR_SUCCESS
|
||||||
);
|
);
|
||||||
} else if (InvenTreeAPI().isConnecting()) {
|
} else if (InvenTreeAPI().isConnecting()) {
|
||||||
return Spinner(
|
return Spinner(
|
||||||
icon: FontAwesomeIcons.spinner,
|
icon: FontAwesomeIcons.spinner,
|
||||||
color: Color.fromRGBO(50, 50, 250, 1),
|
color: COLOR_PROGRESS,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return FaIcon(
|
return FaIcon(
|
||||||
FontAwesomeIcons.timesCircle,
|
FontAwesomeIcons.timesCircle,
|
||||||
color: Color.fromRGBO(250, 50, 50, 1),
|
color: COLOR_DANGER,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -255,7 +256,7 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
|||||||
title: Text(
|
title: Text(
|
||||||
profile.name,
|
profile.name,
|
||||||
),
|
),
|
||||||
tileColor: profile.selected ? Color.fromRGBO(0, 0, 0, 0.05) : null,
|
tileColor: profile.selected ? COLOR_SELECTED : null,
|
||||||
subtitle: Text("${profile.server}"),
|
subtitle: Text("${profile.server}"),
|
||||||
trailing: _getProfileIcon(profile),
|
trailing: _getProfileIcon(profile),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
import 'package:inventree/api.dart';
|
import 'package:inventree/api.dart';
|
||||||
|
import 'package:inventree/app_colors.dart';
|
||||||
import 'package:inventree/app_settings.dart';
|
import 'package:inventree/app_settings.dart';
|
||||||
import 'package:inventree/inventree/part.dart';
|
import 'package:inventree/inventree/part.dart';
|
||||||
import 'package:inventree/inventree/sentry.dart';
|
import 'package:inventree/inventree/sentry.dart';
|
||||||
@ -22,6 +23,8 @@ 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:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||||
|
|
||||||
|
import '../api_form.dart';
|
||||||
|
|
||||||
class CategoryDisplayWidget extends StatefulWidget {
|
class CategoryDisplayWidget extends StatefulWidget {
|
||||||
|
|
||||||
CategoryDisplayWidget(this.category, {Key? key}) : super(key: key);
|
CategoryDisplayWidget(this.category, {Key? key}) : super(key: key);
|
||||||
@ -35,7 +38,6 @@ class CategoryDisplayWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||||
|
|
||||||
final _editCategoryKey = GlobalKey<FormState>();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getAppBarTitle(BuildContext context) => L10().partCategory;
|
String getAppBarTitle(BuildContext context) => L10().partCategory;
|
||||||
@ -71,7 +73,9 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: FaIcon(FontAwesomeIcons.edit),
|
icon: FaIcon(FontAwesomeIcons.edit),
|
||||||
tooltip: L10().edit,
|
tooltip: L10().edit,
|
||||||
onPressed: _editCategoryDialog,
|
onPressed: () {
|
||||||
|
_editCategoryDialog(context);
|
||||||
|
},
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -80,49 +84,26 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _editCategory(Map<String, String> values) async {
|
void _editCategoryDialog(BuildContext context) {
|
||||||
|
|
||||||
final bool result = await category!.update(values: values);
|
final _cat = category;
|
||||||
|
|
||||||
showSnackIcon(
|
|
||||||
result ? "Category edited" : "Category editing failed",
|
|
||||||
success: result
|
|
||||||
);
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _editCategoryDialog() {
|
|
||||||
|
|
||||||
// Cannot edit top-level category
|
// Cannot edit top-level category
|
||||||
if (category == null) {
|
if (_cat == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var _name;
|
launchApiForm(
|
||||||
var _description;
|
context,
|
||||||
|
|
||||||
showFormDialog(
|
|
||||||
L10().editCategory,
|
L10().editCategory,
|
||||||
key: _editCategoryKey,
|
_cat.url,
|
||||||
callback: () {
|
{
|
||||||
_editCategory({
|
"name": {},
|
||||||
"name": _name,
|
"description": {},
|
||||||
"description": _description
|
"parent": {},
|
||||||
});
|
|
||||||
},
|
},
|
||||||
fields: <Widget>[
|
modelData: _cat.jsondata,
|
||||||
StringField(
|
onSuccess: refresh,
|
||||||
label: L10().name,
|
|
||||||
initial: category?.name,
|
|
||||||
onSaved: (value) => _name = value
|
|
||||||
),
|
|
||||||
StringField(
|
|
||||||
label: L10().description,
|
|
||||||
initial: category?.description,
|
|
||||||
onSaved: (value) => _description = value
|
|
||||||
)
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,7 +167,10 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
|||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().parentCategory),
|
title: Text(L10().parentCategory),
|
||||||
subtitle: Text("${category?.parentpathstring}"),
|
subtitle: Text("${category?.parentpathstring}"),
|
||||||
leading: FaIcon(FontAwesomeIcons.levelUpAlt),
|
leading: FaIcon(
|
||||||
|
FontAwesomeIcons.levelUpAlt,
|
||||||
|
color: COLOR_CLICK,
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (category == null || ((category?.parentId ?? 0) < 0)) {
|
if (category == null || ((category?.parentId ?? 0) < 0)) {
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
|
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
import 'package:inventree/l10.dart';
|
import 'package:inventree/l10.dart';
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
@ -92,23 +91,28 @@ class ImagePickerField extends FormField<File> {
|
|||||||
|
|
||||||
|
|
||||||
class CheckBoxField extends FormField<bool> {
|
class CheckBoxField extends FormField<bool> {
|
||||||
CheckBoxField({String? label, String? hint, bool initial = false, Function(bool?)? onSaved}) :
|
CheckBoxField({
|
||||||
|
String? label, bool initial = false, Function(bool?)? onSaved,
|
||||||
|
TextStyle? labelStyle,
|
||||||
|
String? helperText,
|
||||||
|
TextStyle? helperStyle,
|
||||||
|
}) :
|
||||||
super(
|
super(
|
||||||
onSaved: onSaved,
|
onSaved: onSaved,
|
||||||
initialValue: initial,
|
initialValue: initial,
|
||||||
builder: (FormFieldState<bool> state) {
|
builder: (FormFieldState<bool> state) {
|
||||||
return CheckboxListTile(
|
return CheckboxListTile(
|
||||||
//dense: state.hasError,
|
//dense: state.hasError,
|
||||||
title: label == null ? null : Text(label),
|
title: label != null ? Text(label, style: labelStyle) : null,
|
||||||
value: state.value,
|
value: state.value,
|
||||||
onChanged: state.didChange,
|
onChanged: state.didChange,
|
||||||
subtitle: hint == null ? null : Text(hint),
|
subtitle: helperText != null ? Text(helperText, style: helperStyle) : null,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class StringField extends TextFormField {
|
class StringField extends TextFormField {
|
||||||
|
|
||||||
StringField({String label = "", String? hint, String? initial, Function(String?)? onSaved, Function? validator, bool allowEmpty = false, bool isEnabled = true}) :
|
StringField({String label = "", String? hint, String? initial, Function(String?)? onSaved, Function? validator, bool allowEmpty = false, bool isEnabled = true}) :
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:inventree/app_colors.dart';
|
||||||
import 'package:inventree/user_profile.dart';
|
import 'package:inventree/user_profile.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -19,6 +20,7 @@ import 'package:inventree/widget/spinner.dart';
|
|||||||
import 'package:inventree/widget/drawer.dart';
|
import 'package:inventree/widget/drawer.dart';
|
||||||
|
|
||||||
class InvenTreeHomePage extends StatefulWidget {
|
class InvenTreeHomePage extends StatefulWidget {
|
||||||
|
|
||||||
InvenTreeHomePage({Key? key}) : super(key: key);
|
InvenTreeHomePage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -130,7 +132,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
|||||||
leading: FaIcon(FontAwesomeIcons.server),
|
leading: FaIcon(FontAwesomeIcons.server),
|
||||||
trailing: FaIcon(
|
trailing: FaIcon(
|
||||||
FontAwesomeIcons.user,
|
FontAwesomeIcons.user,
|
||||||
color: Color.fromRGBO(250, 50, 50, 1),
|
color: COLOR_DANGER,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_selectProfile();
|
_selectProfile();
|
||||||
@ -146,7 +148,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
|||||||
leading: FaIcon(FontAwesomeIcons.server),
|
leading: FaIcon(FontAwesomeIcons.server),
|
||||||
trailing: Spinner(
|
trailing: Spinner(
|
||||||
icon: FontAwesomeIcons.spinner,
|
icon: FontAwesomeIcons.spinner,
|
||||||
color: Color.fromRGBO(50, 50, 250, 1),
|
color: COLOR_PROGRESS,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_selectProfile();
|
_selectProfile();
|
||||||
@ -159,7 +161,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
|||||||
leading: FaIcon(FontAwesomeIcons.server),
|
leading: FaIcon(FontAwesomeIcons.server),
|
||||||
trailing: FaIcon(
|
trailing: FaIcon(
|
||||||
FontAwesomeIcons.checkCircle,
|
FontAwesomeIcons.checkCircle,
|
||||||
color: Color.fromRGBO(50, 250, 50, 1)
|
color: COLOR_SUCCESS
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_selectProfile();
|
_selectProfile();
|
||||||
@ -172,7 +174,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
|||||||
leading: FaIcon(FontAwesomeIcons.server),
|
leading: FaIcon(FontAwesomeIcons.server),
|
||||||
trailing: FaIcon(
|
trailing: FaIcon(
|
||||||
FontAwesomeIcons.timesCircle,
|
FontAwesomeIcons.timesCircle,
|
||||||
color: Color.fromRGBO(250, 50, 50, 1),
|
color: COLOR_DANGER,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_selectProfile();
|
_selectProfile();
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import 'package:inventree/api.dart';
|
import 'package:inventree/api.dart';
|
||||||
|
import 'package:inventree/api_form.dart';
|
||||||
|
import 'package:inventree/app_colors.dart';
|
||||||
import 'package:inventree/app_settings.dart';
|
import 'package:inventree/app_settings.dart';
|
||||||
import 'package:inventree/barcode.dart';
|
import 'package:inventree/barcode.dart';
|
||||||
import 'package:inventree/inventree/sentry.dart';
|
import 'package:inventree/inventree/sentry.dart';
|
||||||
@ -71,7 +73,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: FaIcon(FontAwesomeIcons.edit),
|
icon: FaIcon(FontAwesomeIcons.edit),
|
||||||
tooltip: L10().edit,
|
tooltip: L10().edit,
|
||||||
onPressed: _editLocationDialog,
|
onPressed: () { _editLocationDialog(context); },
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -79,23 +81,27 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
|||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _editLocation(Map<String, String> values) async {
|
void _editLocationDialog(BuildContext context) {
|
||||||
|
|
||||||
bool result = false;
|
final _loc = location;
|
||||||
|
|
||||||
if (location != null) {
|
if (_loc == null) {
|
||||||
result = await location!.update(values: values);
|
return;
|
||||||
|
|
||||||
showSnackIcon(
|
|
||||||
result ? "Location edited" : "Location editing failed",
|
|
||||||
success: result
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh();
|
launchApiForm(
|
||||||
}
|
context,
|
||||||
|
L10().editLocation,
|
||||||
|
_loc.url,
|
||||||
|
{
|
||||||
|
"name": {},
|
||||||
|
"description": {},
|
||||||
|
"parent": {},
|
||||||
|
},
|
||||||
|
modelData: _loc.jsondata,
|
||||||
|
onSuccess: refresh
|
||||||
|
);
|
||||||
|
|
||||||
void _editLocationDialog() {
|
|
||||||
// Values which an be edited
|
// Values which an be edited
|
||||||
var _name;
|
var _name;
|
||||||
var _description;
|
var _description;
|
||||||
@ -103,28 +109,6 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
|||||||
if (location == null) {
|
if (location == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
showFormDialog(L10().editLocation,
|
|
||||||
key: _editLocationKey,
|
|
||||||
callback: () {
|
|
||||||
_editLocation({
|
|
||||||
"name": _name,
|
|
||||||
"description": _description
|
|
||||||
});
|
|
||||||
},
|
|
||||||
fields: <Widget> [
|
|
||||||
StringField(
|
|
||||||
label: L10().name,
|
|
||||||
initial: location?.name ?? '',
|
|
||||||
onSaved: (value) => _name = value,
|
|
||||||
),
|
|
||||||
StringField(
|
|
||||||
label: L10().description,
|
|
||||||
initial: location?.description ?? '',
|
|
||||||
onSaved: (value) => _description = value,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_LocationDisplayState(this.location);
|
_LocationDisplayState(this.location);
|
||||||
@ -193,7 +177,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
|||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().parentCategory),
|
title: Text(L10().parentCategory),
|
||||||
subtitle: Text("${location!.parentpathstring}"),
|
subtitle: Text("${location!.parentpathstring}"),
|
||||||
leading: FaIcon(FontAwesomeIcons.levelUpAlt),
|
leading: FaIcon(FontAwesomeIcons.levelUpAlt, color: COLOR_CLICK),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|
||||||
int parent = location?.parentId ?? -1;
|
int parent = location?.parentId ?? -1;
|
||||||
@ -319,7 +303,7 @@ List<Widget> detailTiles() {
|
|||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().barcodeScanInItems),
|
title: Text(L10().barcodeScanInItems),
|
||||||
leading: FaIcon(FontAwesomeIcons.exchangeAlt),
|
leading: FaIcon(FontAwesomeIcons.exchangeAlt, color: COLOR_CLICK),
|
||||||
trailing: FaIcon(FontAwesomeIcons.qrcode),
|
trailing: FaIcon(FontAwesomeIcons.qrcode),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|
||||||
|
@ -21,19 +21,14 @@ class PaginatedSearchWidget extends StatelessWidget {
|
|||||||
leading: GestureDetector(
|
leading: GestureDetector(
|
||||||
child: FaIcon(controller.text.isEmpty ? FontAwesomeIcons.search : FontAwesomeIcons.backspace),
|
child: FaIcon(controller.text.isEmpty ? FontAwesomeIcons.search : FontAwesomeIcons.backspace),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (onChanged != null) {
|
controller.clear();
|
||||||
controller.clear();
|
onChanged();
|
||||||
onChanged();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
title: TextFormField(
|
title: TextFormField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
|
onChanged();
|
||||||
if (onChanged != null) {
|
|
||||||
onChanged();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: L10().search,
|
hintText: L10().search,
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:inventree/widget/part_notes.dart';
|
|
||||||
import 'package:inventree/widget/progress.dart';
|
|
||||||
import 'package:inventree/widget/snacks.dart';
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:inventree/l10.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:inventree/app_colors.dart';
|
||||||
|
|
||||||
|
import 'package:inventree/l10.dart';
|
||||||
|
import 'package:inventree/api_form.dart';
|
||||||
|
import 'package:inventree/widget/part_notes.dart';
|
||||||
|
import 'package:inventree/widget/progress.dart';
|
||||||
|
import 'package:inventree/widget/snacks.dart';
|
||||||
import 'package:inventree/inventree/part.dart';
|
import 'package:inventree/inventree/part.dart';
|
||||||
import 'package:inventree/widget/full_screen_image.dart';
|
import 'package:inventree/widget/full_screen_image.dart';
|
||||||
import 'package:inventree/widget/category_display.dart';
|
import 'package:inventree/widget/category_display.dart';
|
||||||
@ -59,7 +61,9 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: FaIcon(FontAwesomeIcons.edit),
|
icon: FaIcon(FontAwesomeIcons.edit),
|
||||||
tooltip: L10().edit,
|
tooltip: L10().edit,
|
||||||
onPressed: _editPartDialog,
|
onPressed: () {
|
||||||
|
_editPartDialog(context);
|
||||||
|
},
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -169,58 +173,36 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _editPartDialog() {
|
void _editPartDialog(BuildContext context) {
|
||||||
|
|
||||||
// Values which can be edited
|
launchApiForm(
|
||||||
var _name;
|
context,
|
||||||
var _description;
|
L10().editPart,
|
||||||
var _ipn;
|
part.url,
|
||||||
var _keywords;
|
{
|
||||||
var _link;
|
"name": {},
|
||||||
|
"description": {},
|
||||||
|
"IPN": {},
|
||||||
|
"revision": {},
|
||||||
|
"keywords": {},
|
||||||
|
"link": {},
|
||||||
|
|
||||||
showFormDialog(L10().editPart,
|
"category": {
|
||||||
key: _editPartKey,
|
},
|
||||||
callback: () {
|
|
||||||
_savePart({
|
// Checkbox fields
|
||||||
"name": _name,
|
"active": {},
|
||||||
"description": _description,
|
"assembly": {},
|
||||||
"IPN": _ipn,
|
"component": {},
|
||||||
"keywords": _keywords,
|
"purchaseable": {},
|
||||||
"link": _link
|
"salable": {},
|
||||||
});
|
"trackable": {},
|
||||||
},
|
"is_template": {},
|
||||||
fields: <Widget>[
|
"virtual": {},
|
||||||
StringField(
|
},
|
||||||
label: L10().name,
|
modelData: part.jsondata,
|
||||||
initial: part.name,
|
onSuccess: refresh,
|
||||||
onSaved: (value) => _name = value,
|
|
||||||
),
|
|
||||||
StringField(
|
|
||||||
label: L10().description,
|
|
||||||
initial: part.description,
|
|
||||||
onSaved: (value) => _description = value,
|
|
||||||
),
|
|
||||||
StringField(
|
|
||||||
label: L10().internalPartNumber,
|
|
||||||
initial: part.IPN,
|
|
||||||
allowEmpty: true,
|
|
||||||
onSaved: (value) => _ipn = value,
|
|
||||||
),
|
|
||||||
StringField(
|
|
||||||
label: L10().keywords,
|
|
||||||
initial: part.keywords,
|
|
||||||
allowEmpty: true,
|
|
||||||
onSaved: (value) => _keywords = value,
|
|
||||||
),
|
|
||||||
StringField(
|
|
||||||
label: L10().link,
|
|
||||||
initial: part.link,
|
|
||||||
allowEmpty: true,
|
|
||||||
onSaved: (value) => _link = value
|
|
||||||
)
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget headerTile() {
|
Widget headerTile() {
|
||||||
@ -230,7 +212,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
subtitle: Text("${part.description}"),
|
subtitle: Text("${part.description}"),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: FaIcon(part.starred ? FontAwesomeIcons.solidStar : FontAwesomeIcons.star,
|
icon: FaIcon(part.starred ? FontAwesomeIcons.solidStar : FontAwesomeIcons.star,
|
||||||
color: part.starred ? Color.fromRGBO(250, 250, 100, 1) : null,
|
color: part.starred ? COLOR_STAR : null,
|
||||||
),
|
),
|
||||||
onPressed: _toggleStar,
|
onPressed: _toggleStar,
|
||||||
),
|
),
|
||||||
@ -264,13 +246,36 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
return tiles;
|
return tiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!part.isActive) {
|
||||||
|
tiles.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
L10().inactive,
|
||||||
|
style: TextStyle(
|
||||||
|
color: COLOR_DANGER
|
||||||
|
)
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
L10().inactiveDetail,
|
||||||
|
style: TextStyle(
|
||||||
|
color: COLOR_DANGER
|
||||||
|
)
|
||||||
|
),
|
||||||
|
leading: FaIcon(
|
||||||
|
FontAwesomeIcons.exclamationCircle,
|
||||||
|
color: COLOR_DANGER
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Category information
|
// Category information
|
||||||
if (part.categoryName.isNotEmpty) {
|
if (part.categoryName.isNotEmpty) {
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().partCategory),
|
title: Text(L10().partCategory),
|
||||||
subtitle: Text("${part.categoryName}"),
|
subtitle: Text("${part.categoryName}"),
|
||||||
leading: FaIcon(FontAwesomeIcons.sitemap),
|
leading: FaIcon(FontAwesomeIcons.sitemap, color: COLOR_CLICK),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (part.categoryId > 0) {
|
if (part.categoryId > 0) {
|
||||||
InvenTreePartCategory().get(part.categoryId).then((var cat) {
|
InvenTreePartCategory().get(part.categoryId).then((var cat) {
|
||||||
@ -289,7 +294,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().partCategory),
|
title: Text(L10().partCategory),
|
||||||
subtitle: Text(L10().partCategoryTopLevel),
|
subtitle: Text(L10().partCategoryTopLevel),
|
||||||
leading: FaIcon(FontAwesomeIcons.sitemap),
|
leading: FaIcon(FontAwesomeIcons.sitemap, color: COLOR_CLICK),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
|
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
|
||||||
},
|
},
|
||||||
@ -301,7 +306,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().stock),
|
title: Text(L10().stock),
|
||||||
leading: FaIcon(FontAwesomeIcons.boxes),
|
leading: FaIcon(FontAwesomeIcons.boxes, color: COLOR_CLICK),
|
||||||
trailing: Text("${part.inStockString}"),
|
trailing: Text("${part.inStockString}"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -387,8 +392,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text("${part.link}"),
|
title: Text("${part.link}"),
|
||||||
leading: FaIcon(FontAwesomeIcons.link),
|
leading: FaIcon(FontAwesomeIcons.link, color: COLOR_CLICK),
|
||||||
trailing: FaIcon(FontAwesomeIcons.externalLinkAlt),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
part.openLink();
|
part.openLink();
|
||||||
},
|
},
|
||||||
@ -412,7 +416,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().notes),
|
title: Text(L10().notes),
|
||||||
leading: FaIcon(FontAwesomeIcons.stickyNote),
|
leading: FaIcon(FontAwesomeIcons.stickyNote, color: COLOR_CLICK),
|
||||||
trailing: Text(""),
|
trailing: Text(""),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:inventree/app_colors.dart';
|
||||||
|
|
||||||
class Spinner extends StatefulWidget {
|
class Spinner extends StatefulWidget {
|
||||||
final IconData? icon;
|
final IconData? icon;
|
||||||
@ -9,7 +10,7 @@ class Spinner extends StatefulWidget {
|
|||||||
final Color color;
|
final Color color;
|
||||||
|
|
||||||
const Spinner({
|
const Spinner({
|
||||||
this.color = const Color.fromRGBO(150, 150, 150, 1),
|
this.color = COLOR_GRAY_LIGHT,
|
||||||
Key? key,
|
Key? key,
|
||||||
@required this.icon,
|
@required this.icon,
|
||||||
this.duration = const Duration(milliseconds: 1800),
|
this.duration = const Duration(milliseconds: 1800),
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
import 'package:inventree/app_colors.dart';
|
||||||
import 'package:inventree/barcode.dart';
|
import 'package:inventree/barcode.dart';
|
||||||
|
import 'package:inventree/inventree/model.dart';
|
||||||
import 'package:inventree/inventree/stock.dart';
|
import 'package:inventree/inventree/stock.dart';
|
||||||
import 'package:inventree/inventree/part.dart';
|
import 'package:inventree/inventree/part.dart';
|
||||||
import 'package:inventree/widget/dialogs.dart';
|
import 'package:inventree/widget/dialogs.dart';
|
||||||
@ -17,9 +19,11 @@ import 'package:inventree/l10.dart';
|
|||||||
|
|
||||||
import 'package:inventree/api.dart';
|
import 'package:inventree/api.dart';
|
||||||
|
|
||||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
import 'package:dropdown_search/dropdown_search.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
|
||||||
|
import '../api_form.dart';
|
||||||
|
|
||||||
class StockDetailWidget extends StatefulWidget {
|
class StockDetailWidget extends StatefulWidget {
|
||||||
|
|
||||||
StockDetailWidget(this.item, {Key? key}) : super(key: key);
|
StockDetailWidget(this.item, {Key? key}) : super(key: key);
|
||||||
@ -49,20 +53,29 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<Widget> getAppBarActions(BuildContext context) {
|
List<Widget> getAppBarActions(BuildContext context) {
|
||||||
return <Widget>[
|
|
||||||
IconButton(
|
List<Widget> actions = [];
|
||||||
icon: FaIcon(FontAwesomeIcons.globe),
|
|
||||||
onPressed: _openInvenTreePage,
|
if (InvenTreeAPI().checkPermission('stock', 'view')) {
|
||||||
),
|
actions.add(
|
||||||
// TODO: Hide the 'edit' button if the user does not have permission!!
|
IconButton(
|
||||||
/*
|
icon: FaIcon(FontAwesomeIcons.globe),
|
||||||
IconButton(
|
onPressed: _openInvenTreePage,
|
||||||
icon: FaIcon(FontAwesomeIcons.edit),
|
)
|
||||||
tooltip: L10().edit,
|
);
|
||||||
onPressed: _editPartDialog,
|
}
|
||||||
)
|
|
||||||
*/
|
if (InvenTreeAPI().checkPermission('stock', 'change')) {
|
||||||
];
|
actions.add(
|
||||||
|
IconButton(
|
||||||
|
icon: FaIcon(FontAwesomeIcons.edit),
|
||||||
|
tooltip: L10().edit,
|
||||||
|
onPressed: () { _editStockItem(context); },
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _openInvenTreePage() async {
|
Future<void> _openInvenTreePage() async {
|
||||||
@ -95,6 +108,24 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
await item.getTestResults();
|
await item.getTestResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _editStockItem(BuildContext context) async {
|
||||||
|
|
||||||
|
launchApiForm(
|
||||||
|
context,
|
||||||
|
L10().editItem,
|
||||||
|
item.url,
|
||||||
|
{
|
||||||
|
"status": {},
|
||||||
|
"batch": {},
|
||||||
|
"packaging": {},
|
||||||
|
"link": {},
|
||||||
|
},
|
||||||
|
modelData: item.jsondata,
|
||||||
|
onSuccess: refresh
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
void _addStock() async {
|
void _addStock() async {
|
||||||
|
|
||||||
double quantity = double.parse(_quantityController.text);
|
double quantity = double.parse(_quantityController.text);
|
||||||
@ -241,7 +272,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void _transferStock(InvenTreeStockLocation location) async {
|
void _transferStock(int locationId) async {
|
||||||
|
|
||||||
double quantity = double.tryParse(_quantityController.text) ?? item.quantity;
|
double quantity = double.tryParse(_quantityController.text) ?? item.quantity;
|
||||||
String notes = _notesController.text;
|
String notes = _notesController.text;
|
||||||
@ -249,7 +280,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
_quantityController.clear();
|
_quantityController.clear();
|
||||||
_notesController.clear();
|
_notesController.clear();
|
||||||
|
|
||||||
var result = await item.transferStock(location.pk, quantity: quantity, notes: notes);
|
var result = await item.transferStock(locationId, quantity: quantity, notes: notes);
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
|
|
||||||
@ -258,22 +289,22 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _transferStockDialog() async {
|
void _transferStockDialog(BuildContext context) async {
|
||||||
|
|
||||||
var locations = await InvenTreeStockLocation().list();
|
var locations = await InvenTreeStockLocation().list();
|
||||||
final _selectedController = TextEditingController();
|
final _selectedController = TextEditingController();
|
||||||
|
|
||||||
InvenTreeStockLocation? selectedLocation;
|
int? location_pk;
|
||||||
|
|
||||||
_quantityController.text = "${item.quantityString}";
|
_quantityController.text = "${item.quantityString}";
|
||||||
|
|
||||||
showFormDialog(L10().transferStock,
|
showFormDialog(L10().transferStock,
|
||||||
key: _moveStockKey,
|
key: _moveStockKey,
|
||||||
callback: () {
|
callback: () {
|
||||||
var _loc = selectedLocation;
|
var _pk = location_pk;
|
||||||
|
|
||||||
if (_loc != null) {
|
if (_pk != null) {
|
||||||
_transferStock(_loc);
|
_transferStock(_pk);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fields: <Widget>[
|
fields: <Widget>[
|
||||||
@ -282,47 +313,57 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
controller: _quantityController,
|
controller: _quantityController,
|
||||||
max: item.quantity,
|
max: item.quantity,
|
||||||
),
|
),
|
||||||
TypeAheadFormField(
|
DropdownSearch<dynamic>(
|
||||||
textFieldConfiguration: TextFieldConfiguration(
|
mode: Mode.BOTTOM_SHEET,
|
||||||
controller: _selectedController,
|
showSelectedItem: false,
|
||||||
autofocus: true,
|
autoFocusSearchBox: true,
|
||||||
decoration: InputDecoration(
|
selectedItem: null,
|
||||||
hintText: L10().searchLocation,
|
errorBuilder: (context, entry, exception) {
|
||||||
border: OutlineInputBorder()
|
print("entry: $entry");
|
||||||
)
|
print(exception.toString());
|
||||||
),
|
|
||||||
suggestionsCallback: (pattern) async {
|
|
||||||
List<InvenTreeStockLocation> suggestions = [];
|
|
||||||
|
|
||||||
for (var loc in locations) {
|
return Text(
|
||||||
if (loc.matchAgainstString(pattern)) {
|
exception.toString(),
|
||||||
suggestions.add(loc as InvenTreeStockLocation);
|
style: TextStyle(
|
||||||
}
|
fontSize: 10,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onFind: (String filter) async {
|
||||||
|
|
||||||
|
Map<String, String> _filters = {
|
||||||
|
"search": filter,
|
||||||
|
"offset": "0",
|
||||||
|
"limit": "25"
|
||||||
|
};
|
||||||
|
|
||||||
|
final List<InvenTreeModel> results = await InvenTreeStockLocation().list(filters: _filters);
|
||||||
|
|
||||||
|
List<dynamic> items = [];
|
||||||
|
|
||||||
|
for (InvenTreeModel loc in results) {
|
||||||
|
if (loc is InvenTreeStockLocation) {
|
||||||
|
items.add(loc.jsondata);
|
||||||
}
|
}
|
||||||
|
|
||||||
return suggestions;
|
|
||||||
},
|
|
||||||
validator: (value) {
|
|
||||||
if (selectedLocation == null) {
|
|
||||||
return L10().selectLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
onSuggestionSelected: (suggestion) {
|
|
||||||
selectedLocation = suggestion as InvenTreeStockLocation;
|
|
||||||
_selectedController.text = selectedLocation!.pathstring;
|
|
||||||
},
|
|
||||||
onSaved: (value) {
|
|
||||||
},
|
|
||||||
itemBuilder: (context, suggestion) {
|
|
||||||
var location = suggestion as InvenTreeStockLocation;
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
title: Text("${location.pathstring}"),
|
|
||||||
subtitle: Text("${location.description}"),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
},
|
||||||
|
label: L10().stockLocation,
|
||||||
|
hint: L10().searchLocation,
|
||||||
|
onChanged: null,
|
||||||
|
itemAsString: (dynamic location) {
|
||||||
|
return location['pathstring'];
|
||||||
|
},
|
||||||
|
onSaved: (dynamic location) {
|
||||||
|
if (location == null) {
|
||||||
|
location_pk = null;
|
||||||
|
} else {
|
||||||
|
location_pk = location['pk'];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isFilteredOnline: true,
|
||||||
|
showSearchBox: true,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -394,7 +435,10 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().stockLocation),
|
title: Text(L10().stockLocation),
|
||||||
subtitle: Text("${item.locationPathString}"),
|
subtitle: Text("${item.locationPathString}"),
|
||||||
leading: FaIcon(FontAwesomeIcons.mapMarkerAlt),
|
leading: FaIcon(
|
||||||
|
FontAwesomeIcons.mapMarkerAlt,
|
||||||
|
color: COLOR_CLICK,
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (item.locationId > 0) {
|
if (item.locationId > 0) {
|
||||||
InvenTreeStockLocation().get(item.locationId).then((var loc) {
|
InvenTreeStockLocation().get(item.locationId).then((var loc) {
|
||||||
@ -463,9 +507,10 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text("${item.link}"),
|
title: Text("${item.link}"),
|
||||||
leading: FaIcon(FontAwesomeIcons.link),
|
leading: FaIcon(FontAwesomeIcons.link, color: COLOR_CLICK),
|
||||||
trailing: Text(""),
|
onTap: () {
|
||||||
onTap: null,
|
item.openLink();
|
||||||
|
},
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -474,7 +519,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().testResults),
|
title: Text(L10().testResults),
|
||||||
leading: FaIcon(FontAwesomeIcons.tasks),
|
leading: FaIcon(FontAwesomeIcons.tasks, color: COLOR_CLICK),
|
||||||
trailing: Text("${item.testResultCount}"),
|
trailing: Text("${item.testResultCount}"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
@ -489,6 +534,18 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.hasPurchasePrice) {
|
||||||
|
tiles.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().purchasePrice),
|
||||||
|
leading: FaIcon(FontAwesomeIcons.dollarSign),
|
||||||
|
trailing: Text(item.purchasePrice),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO - Is this stock item linked to a PurchaseOrder?
|
||||||
|
|
||||||
// TODO - Re-enable stock item history display
|
// TODO - Re-enable stock item history display
|
||||||
if (false && item.trackingItemCount > 0) {
|
if (false && item.trackingItemCount > 0) {
|
||||||
tiles.add(
|
tiles.add(
|
||||||
@ -510,8 +567,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().notes),
|
title: Text(L10().notes),
|
||||||
leading: FaIcon(FontAwesomeIcons.stickyNote),
|
leading: FaIcon(FontAwesomeIcons.stickyNote, color: COLOR_CLICK),
|
||||||
trailing: Text(""),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
@ -527,7 +583,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
return tiles;
|
return tiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> actionTiles() {
|
List<Widget> actionTiles(BuildContext context) {
|
||||||
List<Widget> tiles = [];
|
List<Widget> tiles = [];
|
||||||
|
|
||||||
tiles.add(headerTile());
|
tiles.add(headerTile());
|
||||||
@ -554,7 +610,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().countStock),
|
title: Text(L10().countStock),
|
||||||
leading: FaIcon(FontAwesomeIcons.checkCircle),
|
leading: FaIcon(FontAwesomeIcons.checkCircle, color: COLOR_CLICK),
|
||||||
onTap: _countStockDialog,
|
onTap: _countStockDialog,
|
||||||
trailing: Text(item.quantityString),
|
trailing: Text(item.quantityString),
|
||||||
)
|
)
|
||||||
@ -563,7 +619,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().removeStock),
|
title: Text(L10().removeStock),
|
||||||
leading: FaIcon(FontAwesomeIcons.minusCircle),
|
leading: FaIcon(FontAwesomeIcons.minusCircle, color: COLOR_CLICK),
|
||||||
onTap: _removeStockDialog,
|
onTap: _removeStockDialog,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -571,7 +627,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().addStock),
|
title: Text(L10().addStock),
|
||||||
leading: FaIcon(FontAwesomeIcons.plusCircle),
|
leading: FaIcon(FontAwesomeIcons.plusCircle, color: COLOR_CLICK),
|
||||||
onTap: _addStockDialog,
|
onTap: _addStockDialog,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -580,8 +636,8 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().transferStock),
|
title: Text(L10().transferStock),
|
||||||
leading: FaIcon(FontAwesomeIcons.exchangeAlt),
|
leading: FaIcon(FontAwesomeIcons.exchangeAlt, color: COLOR_CLICK),
|
||||||
onTap: _transferStockDialog,
|
onTap: () { _transferStockDialog(context); },
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -589,7 +645,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().scanIntoLocation),
|
title: Text(L10().scanIntoLocation),
|
||||||
leading: FaIcon(FontAwesomeIcons.exchangeAlt),
|
leading: FaIcon(FontAwesomeIcons.exchangeAlt, color: COLOR_CLICK),
|
||||||
trailing: FaIcon(FontAwesomeIcons.qrcode),
|
trailing: FaIcon(FontAwesomeIcons.qrcode),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
@ -607,7 +663,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().barcodeAssign),
|
title: Text(L10().barcodeAssign),
|
||||||
leading: FaIcon(FontAwesomeIcons.barcode),
|
leading: FaIcon(FontAwesomeIcons.barcode, color: COLOR_CLICK),
|
||||||
trailing: FaIcon(FontAwesomeIcons.qrcode),
|
trailing: FaIcon(FontAwesomeIcons.qrcode),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
@ -623,7 +679,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().barcodeUnassign),
|
title: Text(L10().barcodeUnassign),
|
||||||
leading: FaIcon(FontAwesomeIcons.barcode),
|
leading: FaIcon(FontAwesomeIcons.barcode, color: COLOR_CLICK),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_unassignBarcode(context);
|
_unassignBarcode(context);
|
||||||
}
|
}
|
||||||
@ -665,7 +721,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
return ListView(
|
return ListView(
|
||||||
children: ListTile.divideTiles(
|
children: ListTile.divideTiles(
|
||||||
context: context,
|
context: context,
|
||||||
tiles: actionTiles()
|
tiles: actionTiles(context)
|
||||||
).toList()
|
).toList()
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:inventree/app_colors.dart';
|
||||||
import 'package:inventree/inventree/part.dart';
|
import 'package:inventree/inventree/part.dart';
|
||||||
import 'package:inventree/inventree/stock.dart';
|
import 'package:inventree/inventree/stock.dart';
|
||||||
import 'package:inventree/inventree/model.dart';
|
import 'package:inventree/inventree/model.dart';
|
||||||
@ -84,7 +85,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
|||||||
),
|
),
|
||||||
CheckBoxField(
|
CheckBoxField(
|
||||||
label: L10().result,
|
label: L10().result,
|
||||||
hint: L10().testPassedOrFailed,
|
helperText: L10().testPassedOrFailed,
|
||||||
initial: true,
|
initial: true,
|
||||||
onSaved: (value) => _result = value ?? false,
|
onSaved: (value) => _result = value ?? false,
|
||||||
),
|
),
|
||||||
@ -207,7 +208,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
|||||||
String _value = "";
|
String _value = "";
|
||||||
String _notes = "";
|
String _notes = "";
|
||||||
|
|
||||||
FaIcon _icon = FaIcon(FontAwesomeIcons.questionCircle, color: Color.fromRGBO(0, 0, 250, 1));
|
FaIcon _icon = FaIcon(FontAwesomeIcons.questionCircle, color: COLOR_BLUE);
|
||||||
bool _valueRequired = false;
|
bool _valueRequired = false;
|
||||||
bool _attachmentRequired = false;
|
bool _attachmentRequired = false;
|
||||||
|
|
||||||
@ -229,11 +230,11 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
|||||||
|
|
||||||
if (_result == true) {
|
if (_result == true) {
|
||||||
_icon = FaIcon(FontAwesomeIcons.checkCircle,
|
_icon = FaIcon(FontAwesomeIcons.checkCircle,
|
||||||
color: Color.fromRGBO(0, 250, 0, 0.8)
|
color: COLOR_SUCCESS,
|
||||||
);
|
);
|
||||||
} else if (_result == false) {
|
} else if (_result == false) {
|
||||||
_icon = FaIcon(FontAwesomeIcons.timesCircle,
|
_icon = FaIcon(FontAwesomeIcons.timesCircle,
|
||||||
color: Color.fromRGBO(250, 0, 0, 0.8)
|
color: COLOR_DANGER,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
35
pubspec.lock
35
pubspec.lock
@ -127,6 +127,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.1"
|
||||||
|
dropdown_search:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: dropdown_search
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.3"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -167,27 +174,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
flutter_keyboard_visibility:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_keyboard_visibility
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "5.0.2"
|
|
||||||
flutter_keyboard_visibility_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_keyboard_visibility_platform_interface
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.0"
|
|
||||||
flutter_keyboard_visibility_web:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_keyboard_visibility_web
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.0"
|
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -226,13 +212,6 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
flutter_typeahead:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: flutter_typeahead
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.1.3"
|
|
||||||
flutter_web_plugins:
|
flutter_web_plugins:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -7,7 +7,7 @@ description: InvenTree stock management
|
|||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 0.2.10+18
|
version: 0.3.1+19
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.12.0 <3.0.0"
|
sdk: ">=2.12.0 <3.0.0"
|
||||||
@ -30,7 +30,6 @@ dependencies:
|
|||||||
font_awesome_flutter: ^9.1.0 # FontAwesome icon set
|
font_awesome_flutter: ^9.1.0 # FontAwesome icon set
|
||||||
flutter_speed_dial: ^3.0.5 # FAB menu elements
|
flutter_speed_dial: ^3.0.5 # FAB menu elements
|
||||||
sentry_flutter: 5.0.0 # Error reporting
|
sentry_flutter: 5.0.0 # Error reporting
|
||||||
flutter_typeahead: ^3.1.0 # Auto-complete input field
|
|
||||||
image_picker: ^0.8.0 # Select or take photos
|
image_picker: ^0.8.0 # Select or take photos
|
||||||
url_launcher: 6.0.0 # Open link in system browser
|
url_launcher: 6.0.0 # Open link in system browser
|
||||||
flutter_markdown: ^0.6.2 # Rendering markdown
|
flutter_markdown: ^0.6.2 # Rendering markdown
|
||||||
@ -40,6 +39,7 @@ dependencies:
|
|||||||
one_context: ^1.1.0 # Dialogs without requiring context
|
one_context: ^1.1.0 # Dialogs without requiring context
|
||||||
infinite_scroll_pagination: ^3.1.0 # Let the server do all the work!
|
infinite_scroll_pagination: ^3.1.0 # Let the server do all the work!
|
||||||
audioplayers: ^0.19.0 # Play audio files
|
audioplayers: ^0.19.0 # Play audio files
|
||||||
|
dropdown_search: 0.6.3 # Dropdown autocomplete form fields
|
||||||
path:
|
path:
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user