Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions assets/release_notes.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## xx.yy.z - Month Year
---

- Supports new multi-note API for model instances
- Implement rendering for HTML note content


## 0.25.2 - September 2026
---
Expand Down
2 changes: 1 addition & 1 deletion ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
end
end
end
5 changes: 5 additions & 0 deletions lib/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];

Expand Down
39 changes: 39 additions & 0 deletions lib/inventree/notes.dart
Original file line number Diff line number Diff line change
@@ -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");
}
3 changes: 3 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,9 @@
"description": "Notes"
},

"noNotesFound": "No notes found",
"@noNotesFound": {},

"notifications": "Notifications",
"@notifications": {},

Expand Down
14 changes: 2 additions & 12 deletions lib/widget/build/build_detail.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down
14 changes: 4 additions & 10 deletions lib/widget/company/company_detail.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
176 changes: 176 additions & 0 deletions lib/widget/notes_list_widget.dart
Original file line number Diff line number Diff line change
@@ -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),
),
);
},
);
}
17 changes: 9 additions & 8 deletions lib/widget/notes_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
14 changes: 2 additions & 12 deletions lib/widget/order/purchase_order_detail.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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(
Expand Down
Loading
Loading