mirror of
https://github.com/inventree/inventree-app.git
synced 2026-09-16 06:22:43 +00:00
Implement multi-note support (#883)
* Implement multi-note support Ref: https://github.com/inventree/InvenTree/pull/11971 * Bump iOS target
This commit is contained in:
@@ -344,6 +344,11 @@ class InvenTreeAPI {
|
||||
// Ref: https://github.com/inventree/InvenTree/pull/12011
|
||||
bool get supportsStockItemCreationDate => apiVersion >= 496;
|
||||
|
||||
// Does the server support the "multi notes" API?
|
||||
// (Multiple HTML notes may be attached to a single model instance)
|
||||
// Ref: https://github.com/inventree/InvenTree/pull/11971
|
||||
bool get supportsMultiNotes => apiVersion >= 537;
|
||||
|
||||
// Cached list of plugins (refreshed when we connect to the server)
|
||||
List<InvenTreePlugin> _plugins = [];
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import "package:inventree/inventree/model.dart";
|
||||
|
||||
/*
|
||||
* Class representing a single "note" entry.
|
||||
*
|
||||
* Notes can be attached to any model instance which supports the
|
||||
* InvenTree "multi notes" API - multiple notes may be attached to
|
||||
* a single model instance.
|
||||
*
|
||||
* Note content is provided by the server as editor-agnostic HTML,
|
||||
* and cannot be edited from within this app.
|
||||
*
|
||||
* Ref: https://github.com/inventree/InvenTree/pull/11971
|
||||
*/
|
||||
class InvenTreeNoteEntry extends InvenTreeModel {
|
||||
InvenTreeNoteEntry() : super();
|
||||
|
||||
InvenTreeNoteEntry.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
|
||||
@override
|
||||
InvenTreeNoteEntry createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreeNoteEntry.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "note/";
|
||||
|
||||
// The model type of the instance this note is associated with
|
||||
String get modelType => getString("model_type");
|
||||
|
||||
// The ID of the instance this note is associated with
|
||||
int get modelId => getInt("model_id");
|
||||
|
||||
String get title => getString("title");
|
||||
|
||||
// Rendered (HTML) content of this note
|
||||
String get content => getString("content");
|
||||
|
||||
DateTime? get updated => getDate("updated");
|
||||
}
|
||||
@@ -953,6 +953,9 @@
|
||||
"description": "Notes"
|
||||
},
|
||||
|
||||
"noNotesFound": "No notes found",
|
||||
"@noNotesFound": {},
|
||||
|
||||
"notifications": "Notifications",
|
||||
"@notifications": {},
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import "package:inventree/widget/attachment_widget.dart";
|
||||
|
||||
import "package:inventree/widget/dialogs.dart";
|
||||
import "package:inventree/widget/link_icon.dart";
|
||||
import "package:inventree/widget/notes_widget.dart";
|
||||
import "package:inventree/widget/notes_list_widget.dart";
|
||||
import "package:inventree/widget/refreshable_state.dart";
|
||||
import "package:inventree/widget/build/build_line_list.dart";
|
||||
import "package:inventree/widget/build/build_item_list.dart";
|
||||
@@ -553,17 +553,7 @@ class _BuildOrderDetailState extends RefreshableState<BuildOrderDetailWidget> {
|
||||
|
||||
// Notes tile
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().notes),
|
||||
leading: Icon(TablerIcons.notes, color: COLOR_ACTION),
|
||||
trailing: LinkIcon(),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => NotesWidget(widget.order)),
|
||||
);
|
||||
},
|
||||
),
|
||||
ShowNotesItem(context, widget.order, InvenTreeBuildOrder.MODEL_TYPE),
|
||||
);
|
||||
|
||||
// Attachments tile
|
||||
|
||||
@@ -13,6 +13,7 @@ import "package:inventree/inventree/purchase_order.dart";
|
||||
import "package:inventree/inventree/sales_order.dart";
|
||||
import "package:inventree/widget/attachment_widget.dart";
|
||||
import "package:inventree/widget/link_icon.dart";
|
||||
import "package:inventree/widget/notes_list_widget.dart";
|
||||
import "package:inventree/widget/order/purchase_order_list.dart";
|
||||
import "package:inventree/widget/order/sales_order_list.dart";
|
||||
import "package:inventree/widget/parameter_widget.dart";
|
||||
@@ -400,16 +401,9 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.company.notes.isNotEmpty) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().notes),
|
||||
subtitle: Text(widget.company.notes),
|
||||
leading: Icon(TablerIcons.note),
|
||||
onTap: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
tiles.add(
|
||||
ShowNotesItem(context, widget.company, InvenTreeCompany.MODEL_TYPE),
|
||||
);
|
||||
|
||||
ListTile? parameterTile = ShowParametersItem(
|
||||
context,
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import "package:cached_network_image/cached_network_image.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
||||
import "package:flutter_widget_from_html/flutter_widget_from_html.dart";
|
||||
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/inventree/model.dart";
|
||||
import "package:inventree/inventree/notes.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/widget/link_icon.dart";
|
||||
import "package:inventree/widget/notes_widget.dart";
|
||||
import "package:inventree/widget/refreshable_state.dart";
|
||||
|
||||
/*
|
||||
* A widget for displaying the list of "notes" attached to a given model instance,
|
||||
* for servers which support the "multi notes" API.
|
||||
*
|
||||
* Note content is rendered as HTML, and cannot be edited from within the app.
|
||||
*
|
||||
* Ref: https://github.com/inventree/InvenTree/pull/11971
|
||||
*/
|
||||
class NotesListWidget extends StatefulWidget {
|
||||
const NotesListWidget(this.modelType, this.modelId, {Key? key})
|
||||
: super(key: key);
|
||||
|
||||
final String modelType;
|
||||
final int modelId;
|
||||
|
||||
@override
|
||||
_NotesListWidgetState createState() => _NotesListWidgetState();
|
||||
}
|
||||
|
||||
class _NotesListWidgetState extends RefreshableState<NotesListWidget> {
|
||||
_NotesListWidgetState();
|
||||
|
||||
List<InvenTreeNoteEntry> notes = [];
|
||||
|
||||
@override
|
||||
String getAppBarTitle() => L10().notes;
|
||||
|
||||
@override
|
||||
Future<void> request(BuildContext context) async {
|
||||
Map<String, String> filters = {
|
||||
"model_type": widget.modelType,
|
||||
"model_id": widget.modelId.toString(),
|
||||
};
|
||||
|
||||
List<InvenTreeNoteEntry> results = [];
|
||||
|
||||
await InvenTreeNoteEntry().list(filters: filters).then((var entries) {
|
||||
for (var entry in entries) {
|
||||
if (entry is InvenTreeNoteEntry) {
|
||||
results.add(entry);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
notes = results;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
List<Widget> getTiles(BuildContext context) {
|
||||
List<Widget> tiles = [];
|
||||
|
||||
for (var note in notes) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(note.title.isNotEmpty ? note.title : L10().notes),
|
||||
subtitle: note.description.isNotEmpty ? Text(note.description) : null,
|
||||
leading: Icon(TablerIcons.note, color: COLOR_ACTION),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => NoteViewWidget(note)),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (tiles.isEmpty && !loading) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
leading: Icon(TablerIcons.note_off, color: COLOR_WARNING),
|
||||
title: Text(L10().noNotesFound),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return tiles;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Custom WidgetFactory which authenticates image requests against the
|
||||
* InvenTree server, so that (e.g.) note images hosted behind the server's
|
||||
* media API can be displayed.
|
||||
*/
|
||||
class _NoteWidgetFactory extends WidgetFactory {
|
||||
@override
|
||||
ImageProvider<Object>? imageProviderFromNetwork(String url) {
|
||||
if (url.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return CachedNetworkImageProvider(
|
||||
url,
|
||||
headers: InvenTreeAPI().defaultHeaders(),
|
||||
cacheManager: InvenTreeAPI().imageCacheManager,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* A read-only widget for viewing the (HTML) content of a single note.
|
||||
*
|
||||
* Note content is generated server-side by a rich text editor,
|
||||
* and cannot be edited from within this app.
|
||||
*/
|
||||
class NoteViewWidget extends StatelessWidget {
|
||||
const NoteViewWidget(this.note, {Key? key}) : super(key: key);
|
||||
|
||||
final InvenTreeNoteEntry note;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(note.title.isNotEmpty ? note.title : L10().notes),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: HtmlWidget(
|
||||
note.content,
|
||||
baseUrl: Uri.tryParse(InvenTreeAPI().baseUrl),
|
||||
factoryBuilder: () => _NoteWidgetFactory(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Return a ListTile which navigates to the "notes" for the given model instance.
|
||||
*
|
||||
* If the server supports the "multi notes" API, the user is first presented
|
||||
* with a list of available notes to select from.
|
||||
*
|
||||
* Otherwise, we fall back to the legacy single-notes (markdown) implementation.
|
||||
*/
|
||||
ListTile ShowNotesItem(
|
||||
BuildContext context,
|
||||
InvenTreeModel model,
|
||||
String modelType,
|
||||
) {
|
||||
return ListTile(
|
||||
title: Text(L10().notes),
|
||||
leading: Icon(TablerIcons.note, color: COLOR_ACTION),
|
||||
trailing: LinkIcon(),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => InvenTreeAPI().supportsMultiNotes
|
||||
? NotesListWidget(modelType, model.pk)
|
||||
: LegacyNotesWidget(model),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -6,26 +6,27 @@ import "package:flutter_markdown/flutter_markdown.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
/*
|
||||
* A widget for displaying the notes associated with a given model.
|
||||
* A widget for displaying the single (markdown) notes field associated with
|
||||
* a given model, for servers which do not support the "multi notes" API.
|
||||
* We need to pass in the following parameters:
|
||||
*
|
||||
*
|
||||
* - Model instance
|
||||
* - Title for the app bar
|
||||
*/
|
||||
class NotesWidget extends StatefulWidget {
|
||||
const NotesWidget(this.model, {Key? key}) : super(key: key);
|
||||
class LegacyNotesWidget extends StatefulWidget {
|
||||
const LegacyNotesWidget(this.model, {Key? key}) : super(key: key);
|
||||
|
||||
final InvenTreeModel model;
|
||||
|
||||
@override
|
||||
_NotesState createState() => _NotesState();
|
||||
_LegacyNotesState createState() => _LegacyNotesState();
|
||||
}
|
||||
|
||||
/*
|
||||
* Class representing the state of the NotesWidget
|
||||
* Class representing the state of the LegacyNotesWidget
|
||||
*/
|
||||
class _NotesState extends RefreshableState<NotesWidget> {
|
||||
_NotesState();
|
||||
class _LegacyNotesState extends RefreshableState<LegacyNotesWidget> {
|
||||
_LegacyNotesState();
|
||||
|
||||
@override
|
||||
Future<void> request(BuildContext context) async {
|
||||
|
||||
@@ -22,7 +22,7 @@ import "package:inventree/widget/order/po_extra_line_list.dart";
|
||||
import "package:inventree/widget/order/po_line_list.dart";
|
||||
|
||||
import "package:inventree/widget/attachment_widget.dart";
|
||||
import "package:inventree/widget/notes_widget.dart";
|
||||
import "package:inventree/widget/notes_list_widget.dart";
|
||||
import "package:inventree/widget/parameter_widget.dart";
|
||||
import "package:inventree/widget/progress.dart";
|
||||
import "package:inventree/widget/refreshable_state.dart";
|
||||
@@ -567,17 +567,7 @@ class _PurchaseOrderDetailState
|
||||
|
||||
// Notes tile
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().notes),
|
||||
leading: Icon(TablerIcons.note, color: COLOR_ACTION),
|
||||
trailing: LinkIcon(),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => NotesWidget(widget.order)),
|
||||
);
|
||||
},
|
||||
),
|
||||
ShowNotesItem(context, widget.order, InvenTreePurchaseOrder.MODEL_TYPE),
|
||||
);
|
||||
|
||||
ListTile? parameterTile = ShowParametersItem(
|
||||
|
||||
@@ -20,7 +20,7 @@ import "package:inventree/l10.dart";
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/widget/attachment_widget.dart";
|
||||
import "package:inventree/widget/dialogs.dart";
|
||||
import "package:inventree/widget/notes_widget.dart";
|
||||
import "package:inventree/widget/notes_list_widget.dart";
|
||||
import "package:inventree/widget/snacks.dart";
|
||||
import "package:inventree/widget/progress.dart";
|
||||
|
||||
@@ -492,17 +492,7 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
|
||||
|
||||
// Notes tile
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().notes),
|
||||
leading: Icon(TablerIcons.note, color: COLOR_ACTION),
|
||||
trailing: LinkIcon(),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => NotesWidget(widget.order)),
|
||||
);
|
||||
},
|
||||
),
|
||||
ShowNotesItem(context, widget.order, InvenTreeSalesOrder.MODEL_TYPE),
|
||||
);
|
||||
|
||||
ListTile? parameterTile = ShowParametersItem(
|
||||
|
||||
@@ -14,7 +14,7 @@ import "package:inventree/l10.dart";
|
||||
import "package:inventree/preferences.dart";
|
||||
import "package:inventree/widget/attachment_widget.dart";
|
||||
import "package:inventree/widget/link_icon.dart";
|
||||
import "package:inventree/widget/notes_widget.dart";
|
||||
import "package:inventree/widget/notes_list_widget.dart";
|
||||
import "package:inventree/widget/order/so_allocation_list.dart";
|
||||
import "package:inventree/widget/refreshable_state.dart";
|
||||
import "package:inventree/widget/snacks.dart";
|
||||
@@ -336,18 +336,10 @@ class _SOShipmentDetailWidgetState
|
||||
|
||||
// Notes tile
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().notes),
|
||||
leading: Icon(TablerIcons.note, color: COLOR_ACTION),
|
||||
trailing: LinkIcon(),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NotesWidget(widget.shipment),
|
||||
),
|
||||
);
|
||||
},
|
||||
ShowNotesItem(
|
||||
context,
|
||||
widget.shipment,
|
||||
InvenTreeSalesOrderShipment.MODEL_TYPE,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import "package:inventree/inventree/transfer_order.dart";
|
||||
|
||||
import "package:inventree/widget/dialogs.dart";
|
||||
import "package:inventree/widget/link_icon.dart";
|
||||
import "package:inventree/widget/notes_widget.dart";
|
||||
import "package:inventree/widget/notes_list_widget.dart";
|
||||
import "package:inventree/widget/order/transfer_order_line_list.dart";
|
||||
import "package:inventree/widget/progress.dart";
|
||||
import "package:inventree/widget/refreshable_state.dart";
|
||||
@@ -406,17 +406,7 @@ class _TransferOrderDetailState
|
||||
}
|
||||
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().notes),
|
||||
leading: Icon(TablerIcons.note, color: COLOR_ACTION),
|
||||
trailing: LinkIcon(),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => NotesWidget(widget.order)),
|
||||
);
|
||||
},
|
||||
),
|
||||
ShowNotesItem(context, widget.order, InvenTreeTransferOrder.MODEL_TYPE),
|
||||
);
|
||||
|
||||
return tiles;
|
||||
|
||||
@@ -21,7 +21,7 @@ import "package:inventree/widget/link_icon.dart";
|
||||
import "package:inventree/widget/parameter_widget.dart";
|
||||
import "package:inventree/widget/part/bom_list.dart";
|
||||
import "package:inventree/widget/part/part_list.dart";
|
||||
import "package:inventree/widget/notes_widget.dart";
|
||||
import "package:inventree/widget/notes_list_widget.dart";
|
||||
import "package:inventree/widget/part/part_pricing.dart";
|
||||
import "package:inventree/widget/progress.dart";
|
||||
import "package:inventree/widget/part/category_display.dart";
|
||||
@@ -726,19 +726,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
}
|
||||
|
||||
// Notes field
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().notes),
|
||||
leading: Icon(TablerIcons.note, color: COLOR_ACTION),
|
||||
trailing: LinkIcon(),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => NotesWidget(part)),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
tiles.add(ShowNotesItem(context, part, InvenTreePart.MODEL_TYPE));
|
||||
|
||||
ListTile? parameterTile = ShowParametersItem(
|
||||
context,
|
||||
|
||||
@@ -27,7 +27,7 @@ import "package:inventree/widget/refreshable_state.dart";
|
||||
import "package:inventree/widget/snacks.dart";
|
||||
import "package:inventree/widget/stock/stock_item_history.dart";
|
||||
import "package:inventree/widget/stock/stock_item_test_results.dart";
|
||||
import "package:inventree/widget/notes_widget.dart";
|
||||
import "package:inventree/widget/notes_list_widget.dart";
|
||||
|
||||
class StockDetailWidget extends StatefulWidget {
|
||||
const StockDetailWidget(this.item, {Key? key}) : super(key: key);
|
||||
@@ -840,17 +840,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
|
||||
// Notes field
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().notes),
|
||||
leading: Icon(TablerIcons.note, color: COLOR_ACTION),
|
||||
trailing: LinkIcon(),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => NotesWidget(widget.item)),
|
||||
);
|
||||
},
|
||||
),
|
||||
ShowNotesItem(context, widget.item, InvenTreeStockItem.MODEL_TYPE),
|
||||
);
|
||||
|
||||
ListTile? attachmentTile = ShowAttachmentsItem(
|
||||
|
||||
Reference in New Issue
Block a user