2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 11:36:44 +00:00

[Plugin] Plugin context (#9439)

* Pass more stuff to window

* Expose form functions to plugin context

* Breaking: Render plugin component in context tree

- Required due to createRoot function
- Adds necessary context providers

* Fix context

* Provide MantineThemeContext

* Bundle mantine/core

* Hack for useNavigate within ApiForm

- Errors out if called within plugin context
- Workaround to catch the error

* Update build cmd

* Define config for building "Library" mode

* Update package.json

* Add basic index file

* Factor out ApiEndpoints

* factor out ModelType

* Factor out role enums

* Further refactoring

* More refactoring

* Cleanup

* Expose apiUrl function

* Add instance data to plugin context type def

* Tweaks for loading plugin components

- LanguageContext must be on the inside

* Tweak StylishText

* Externalize notifications system

* Update lingui config

* Add functions for checking plugin interface version

* Extract package version at build time

* Enhance version checking

* Revert variable name change

* Public package

* Add README.md

* adjust packge name

* Adjust name to include org

* Update project files

* Add basic changelog info

* Refactoring to expose URL functions

* Refactor navigation functions

* Update package and README

* Improve navigateToLink function

* Refactor stylish text

- Move into ./lib
- Do not require user state

* Revert changes

- StylishText throws error in plugin
- Low priority, can work out later

* expose function to refresh page index

* Provide RemoteComponent with a method to reload itself

* Bump version

* Cleanup tests

* Prevent duplicate --emptyOutDir arg

* Tweak playwright tests

* Expose role and permission enums

* Fix imports

* Updated docs

* Fix spelling, typos, etc

* Include more package version information

* Expose more version context

* Cleanup

* Probably don't need hooks

* Fix links

* Docs updates

* Fix links
This commit is contained in:
Oliver 2025-04-16 00:30:34 +10:00 committed by GitHub
parent f3d804d5ea
commit 5e7e258289
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
276 changed files with 2797 additions and 1854 deletions

1
.gitignore vendored
View File

@ -18,7 +18,6 @@ share/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/
lib/
lib64/ lib64/
parts/ parts/
sdist/ sdist/

View File

@ -18,7 +18,8 @@ If you add a lot of code (over ~1000 LOC) maybe split it into multiple plugins t
Great. Now please read the [plugin documentation](./plugins.md) to get an overview of the architecture. It is rather short as a the (builtin) mixins come with extensive docstrings. Great. Now please read the [plugin documentation](./plugins.md) to get an overview of the architecture. It is rather short as a the (builtin) mixins come with extensive docstrings.
### Pick your building blocks ### Pick your building blocks
Consider the usecase for your plugin and define the exact function of the plugin, maybe write it down in a short readme. Then pick the mixins you need (they help reduce custom code and keep the system reliable if internal calls change).
Consider the use-case for your plugin and define the exact function of the plugin, maybe write it down in a short readme. Then pick the mixins you need (they help reduce custom code and keep the system reliable if internal calls change).
- Is it just a simple REST-endpoint that runs a function ([ActionMixin](./plugins/action.md)) or a parser for a custom barcode format ([BarcodeMixin](./plugins/barcode.md))? - Is it just a simple REST-endpoint that runs a function ([ActionMixin](./plugins/action.md)) or a parser for a custom barcode format ([BarcodeMixin](./plugins/barcode.md))?
- How does the user interact with the plugin? Is it a UI separate from the main InvenTree UI ([UrlsMixin](./plugins/urls.md)), does it need multiple pages with navigation-links ([NavigationMixin](./plugins/navigation.md)). - How does the user interact with the plugin? Is it a UI separate from the main InvenTree UI ([UrlsMixin](./plugins/urls.md)), does it need multiple pages with navigation-links ([NavigationMixin](./plugins/navigation.md)).
@ -30,7 +31,7 @@ Consider the usecase for your plugin and define the exact function of the plugin
- Do you need the full power of Django with custom models and all the complexity that comes with that welcome to the danger zone and [AppMixin](./plugins/app.md). The plugin will be treated as a app by django and can maybe rack the whole instance. - Do you need the full power of Django with custom models and all the complexity that comes with that welcome to the danger zone and [AppMixin](./plugins/app.md). The plugin will be treated as a app by django and can maybe rack the whole instance.
### Define the metadata ### Define the metadata
Do not forget to [declare the metadata](./plugins.md#plugin-options) for your plugin, those will be used in the settings. At least provide a weblink so users can file issues / reach you. Do not forget to [declare the metadata](./plugins.md#plugin-options) for your plugin, those will be used in the settings. At least provide a web link so users can file issues / reach you.
### Development guidelines ### Development guidelines
If you want to make your life easier, try to follow these guidelines; break where it makes sense for your use case. If you want to make your life easier, try to follow these guidelines; break where it makes sense for your use case.
@ -139,9 +140,7 @@ from plugin.mixins import ActionMixin
class SampleActionPlugin(ActionMixin, InvenTreePlugin): class SampleActionPlugin(ActionMixin, InvenTreePlugin):
""" """Use docstrings for everything."""
Use docstrings for everything... pls
"""
NAME = "SampleActionPlugin" NAME = "SampleActionPlugin"
ACTION_NAME = "sample" ACTION_NAME = "sample"

View File

@ -4,40 +4,8 @@ title: Third Party Integrations
## Third Party Integrations ## Third Party Integrations
A list of known third-party InvenTree extensions is provided below. If you have an extension that should be listed here, contact the InvenTree team on [GitHub](https://github.com/inventree/). A list of known third-party InvenTree extensions is provided [on our website](https://inventree.org/extend/integrate/) If you have an extension that should be listed here, contact the InvenTree team on [GitHub](https://github.com/inventree/).
### Ki-nTree ## Available Plugins
[Ki-nTree](https://github.com/sparkmicro/Ki-nTree/) is a fantastic tool for automated creation of [KiCad](https://www.kicad.org/) library parts, with direct integration with InvenTree. Refer to the [InvenTree website](https://inventree.org/plugins.html) for a (non exhaustive) list of plugins that are available for InvenTree. This includes both official and third-party plugins.
### PK2InvenTree
[PK2InvenTree](https://github.com/rgilham/PK2InvenTree) is an open-source tool for migrating an existing [PartKeepr](https://github.com/partkeepr/PartKeepr) database to InvenTree.
### Digikey-Inventree-Integration
[Digikey-Inventree-Integration](https://github.com/EUdds/Digikey-Inventree-Integration) is a simple project that takes a digikey part number to creates a part in InvenTree.
### F360-InvenTree
[F360-InvenTree](https://github.com/matmair/F360-InvenTree/) is a tool for creating links between Autodesk Fusion 360 components and InvenTree parts.
Still under heavy development.
### DigitalOcean droplet
[InvenTree droplet](https://inventree.org/digitalocean) is a 1-click solution to deploy InvenTree in the cloud with DigitalOcean. You still have to administer and update your instance.
The source code for this droplet can be found in [inventree_droplet](https://github.com/invenhost/inventree_droplet).
### InvenTree zebra plugin
[InvenTree zebra plugin](https://github.com/SergeoLacruz/inventree-zebra-plugin) is a plugin to print labels with zebra printers.
Currently only the GK420T printer is supported.
### InvenTree Apprise
[InvenTree Apprise](https://github.com/matmair/inventree-apprise) is a plugin to send notifications via Apprise. This enables a wide variety of targets.
## First party plugins
### InvenTree brother plugin
[InvenTree brother plugin](https://github.com/inventree/inventree-brother-plugin) is a plugin to print labels with brother Q series printers.

View File

@ -4,7 +4,7 @@ title: Plugins
## InvenTree Plugin Architecture ## InvenTree Plugin Architecture
The InvenTree server code supports an extensible plugin architecture, allowing custom plugins to be integrated directly into the database server. This allows development of complex behaviours which are decoupled from core InvenTree code. The InvenTree server code supports an extensible plugin architecture, allowing custom plugins to be integrated directly into the database server. This allows development of complex behaviors which are decoupled from core InvenTree code.
Plugins can be added from multiple sources: Plugins can be added from multiple sources:
@ -35,8 +35,8 @@ create-inventree-plugin
Custom plugins must inherit from the [InvenTreePlugin class]({{ sourcefile("src/backend/InvenTree/plugin/plugin.py") }}). Any plugins installed via the methods outlined above will be "discovered" when the InvenTree server launches. Custom plugins must inherit from the [InvenTreePlugin class]({{ sourcefile("src/backend/InvenTree/plugin/plugin.py") }}). Any plugins installed via the methods outlined above will be "discovered" when the InvenTree server launches.
!!! warning "Namechange" !!! warning "Name Change"
The name of the base class was changed with `0.7.0` from `IntegrationPluginBase` to `InvenTreePlugin`. While the old name is still available till `0.8.0` we strongly suggest upgrading your plugins. Deprecation warnings are raised if the old name is used. The name of the base class was changed with `0.7.0` from `IntegrationPluginBase` to `InvenTreePlugin`.
### Imports ### Imports
@ -66,7 +66,7 @@ Mixins are split up internally to keep the source tree clean and enable better t
#### Models and other internal InvenTree APIs #### Models and other internal InvenTree APIs
!!! warning "Danger Zone" !!! warning "Danger Zone"
The APIs outside of the `plugin` namespace are not structured for public usage and require a more in-depth knowledge of the Django framework. Please ask in GitHub discussions of the `ÌnvenTree` org if you are not sure you are using something the intended way. The APIs outside of the `plugin` namespace are not structured for public usage and require a more in-depth knowledge of the Django framework. Please ask in GitHub discussions of the `InvenTree` org if you are not sure you are using something the intended way.
We do not provide stable interfaces to models or any other internal python APIs. If you need to integrate into these parts please make yourself familiar with the codebase. We follow general Django patterns and only stray from them in limited, special cases. We do not provide stable interfaces to models or any other internal python APIs. If you need to integrate into these parts please make yourself familiar with the codebase. We follow general Django patterns and only stray from them in limited, special cases.
If you need to react to state changes please use the [EventMixin](./plugins/event.md). If you need to react to state changes please use the [EventMixin](./plugins/event.md).
@ -121,6 +121,7 @@ Supported mixin classes are:
| [ReportMixin](./plugins/report.md) | Add custom context data to reports | | [ReportMixin](./plugins/report.md) | Add custom context data to reports |
| [ScheduleMixin](./plugins/schedule.md) | Schedule periodic tasks | | [ScheduleMixin](./plugins/schedule.md) | Schedule periodic tasks |
| [SettingsMixin](./plugins/settings.md) | Integrate user configurable settings | | [SettingsMixin](./plugins/settings.md) | Integrate user configurable settings |
| [UserInterfaceMixin](./plugins/ui.md) | Add custom user interface features |
| [UrlsMixin](./plugins/urls.md) | Respond to custom URL endpoints | | [UrlsMixin](./plugins/urls.md) | Respond to custom URL endpoints |
| [ValidationMixin](./plugins/validation.md) | Provide custom validation of database models | | [ValidationMixin](./plugins/validation.md) | Provide custom validation of database models |

View File

@ -7,4 +7,4 @@ title: App Mixin
If this mixin is added to a plugin the directory the plugin class is defined in is added to the list of `INSTALLED_APPS` in the InvenTree server configuration. If this mixin is added to a plugin the directory the plugin class is defined in is added to the list of `INSTALLED_APPS` in the InvenTree server configuration.
!!! warning "Danger Zone" !!! warning "Danger Zone"
Only use this mixin if you have an understanding of djangos [app system]({% include "django.html" %}/ref/applications). Plugins with this mixin are deeply integrated into InvenTree and can cause difficult to reproduce or long-running errors. Use the built-in testing functions of django to make sure your code does not cause unwanted behaviour in InvenTree before releasing. Only use this mixin if you have an understanding of Django's [app system]({% include "django.html" %}/ref/applications). Plugins with this mixin are deeply integrated into InvenTree and can cause difficult to reproduce or long-running errors. Use the built-in testing functions of Django to make sure your code does not cause unwanted behaviour in InvenTree before releasing.

View File

@ -5,7 +5,7 @@ title: Navigation Mixin
## NavigationMixin ## NavigationMixin
Use the class constant `NAVIGATION` for a array of links that should be added to InvenTrees navigation header. Use the class constant `NAVIGATION` for a array of links that should be added to InvenTrees navigation header.
The array must contain at least one dict that at least define a name and a link for each element. The link must be formatted for a URL pattern name lookup - links to external sites are not possible directly. The optional icon must be a class reference to an icon (InvenTree ships with fontawesome 4 by default). The array must contain at least one dict that at least define a name and a link for each element. The link must be formatted for a URL pattern name lookup - links to external sites are not possible directly. The optional icon must be a class reference to an icon.
``` python ``` python
class MyNavigationPlugin(NavigationMixin, InvenTreePlugin): class MyNavigationPlugin(NavigationMixin, InvenTreePlugin):

View File

@ -217,3 +217,47 @@ We are working to develop and distribute a library of custom InvenTree component
### Examples ### Examples
Refer to some of the existing InvenTree plugins linked above for examples of building custom UI plugins using the Mantine component library for seamless integration. Refer to some of the existing InvenTree plugins linked above for examples of building custom UI plugins using the Mantine component library for seamless integration.
## Building a User Interface Plugin
The technology stack which allows custom plugins to hook into the InvenTree user interface utilizes the following components:
- [React](https://react.dev)
- [Mantine](https://mantine.dev)
- [TypeScript](https://www.typescriptlang.org/)
- [Vite](https://vitejs.dev/)
While you don't need to be an expert in all of these technologies, it is recommended that you have a basic understanding of how they work together to build the InvenTree user interface. To get started, you should familiarize yourself with the frontend code (at `./src/frontend/`) as well as the vite configuration for the [InvenTree plugin creator](httsps://github.com/inventree/plugin-creator).
### Bundled with InvenTree
If a plugin is bundled with a separate copy of React libraries, issues may arise either due to version mismatches or because the React context is not shared between the InvenTree core and the plugin. This can lead to issues with rendering components, as the React context may not be shared between the two libraries.
To avoid issues, the InvenTree UI provides globally accessible components, which can be used as external modules by the plugin. This allows the plugin to use the same React context as the InvenTree core, and ensures that the plugin is compatible with the InvenTree user interface.
The following modules are provided as global objects at runtime:
- `React`
- `ReactDOM`
- `ReactDOMClient`
Additionally, for the Mantine library, the following modules are provided as global objects at runtime:
- `@mantine/core`
- `@mantine/hooks`
- `@mantine/notifications`
To use these modules in your plugin, they must be correctly *externalized* in the Vite configuration. Getting this right is crucial to ensure that the plugin is compatible with the InvenTree user interface. The [InvenTree plugin creator](https://github.com/inventree/plugin-creator) provides a good starting point for this configuration, and can be used to generate a new plugin with the correct configuration.
!!! info "Bundled Version"
Keep in mind that the version of React and Mantine used in the InvenTree core may differ from the version used in your plugin. It is recommended to use the same version as the InvenTree core to avoid compatibility issues.
### Plugin Creator
The [InvenTree plugin creator](https://github.com/inventree/plugin-creator) provides an out-of-the-box setup for creating InvenTree plugins which integrate into the user interface. This includes a pre-configured Vite setup, which allows you to quickly get started with building your own custom UI plugins.
Using the plugin creator tool is the recommended way to get started with building custom UI plugins for InvenTree, as it provides a solid foundation to build upon. It is also the only method which is officially supported by the InvenTree development team!
### DIY
Of course, you can also build your own custom UI plugins from scratch. This is a more advanced option, and requires a good understanding of the InvenTree codebase, as well as the technologies used to build the user interface. You are free to use other web technologies, however if you choose to do this, don't expect any support from the InvenTree development team. We will only provide support for plugins which are built using the recommended stack, and which follow the guidelines outlined in this documentation.

View File

@ -69,7 +69,7 @@ from plugin.mixins import ValidationMixin
import part.models import part.models
class MyValidationMixin(Validationixin, InvenTreePlugin): class MyValidationMixin(ValidationMixin, InvenTreePlugin):
"""Custom validation plugin.""" """Custom validation plugin."""
def validate_model_instance(self, instance, deltas=None): def validate_model_instance(self, instance, deltas=None):

View File

@ -41,8 +41,8 @@
"pseudo-LOCALE"], "pseudo-LOCALE"],
"catalogs": [{ "catalogs": [{
"path": "src/locales/{locale}/messages", "path": "src/locales/{locale}/messages",
"include": ["src"], "include": ["src", "lib"],
"exclude": ["**/node_modules/**"] "exclude": ["**/node_modules/**", "./dist/**"]
}], }],
"format": "po", "format": "po",
"orderBy": "origin", "orderBy": "origin",

14
src/frontend/.npmignore Normal file
View File

@ -0,0 +1,14 @@
# Testing code
playwright.config.ts
playwright/
tests/
test-results/
# Source files (not part of public API)
src/
# Build output
node_modules/
# Other files
.gitignore

15
src/frontend/CHANGELOG.md Normal file
View File

@ -0,0 +1,15 @@
## InvenTree UI Components - Changelog
This file contains historical changelog information for the InvenTree UI components library.
### 1.0.0 - April 2025
Published the first version of the UI components API. This allows external plugins to hook into the InvenTree user interface, and provides global access to the following objects:
- `window.React`: The core `react` library running on the UI
- `window.ReactDOM`: The `react-dom` library
- `window.ReactDOMClient`: The `react-dom/client` library
- `window.MantineCore`: The `@mantine/core` library
- `window.MantineNotifications`: The `@mantine/notifications` library
All of these components can be "externalized" in the plugin build step.

21
src/frontend/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 - InvenTree Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

28
src/frontend/README.md Normal file
View File

@ -0,0 +1,28 @@
## inventree-ui
User Interface (UI) elements for the [InvenTree](https://inventree.org) web interface.
### Description
This package provides a public interface allowing plugins to hook into core UI functionality. In particular, it defines a set of interface types provided by the InvenTree user interface, to be used by a custom plugin to implement some custom UI feature.
This library is intended to be used for creating plugins - any other use is outside of its scope and is not supported.
### Plugin Creator
This library is intended to be used with the [InvenTree Plugin Creator](https://github.com/inventree/plugin-creator). Read the documentation for the plugin creation tool for more information.
The plugin creation tool uses the types provided in this package at build time, but it is intended that most of the major packages are *externalized* - as these are provided as global objects by the core InvenTree UI code.
### Installation
This should be installed as a part of the plugin creator tool. If you need to install it manually, e.g. using `npm`:
```
npm i @inventreedb/ui
```
### Versioning
Each change to the plugin API will be described in the [CHANGELOG file](./CHANGELOG.md).

View File

@ -0,0 +1,279 @@
import { t } from '@lingui/core/macro';
import type { InvenTreeIconType } from '../types/Icons';
import { ApiEndpoints } from './ApiEndpoints';
import type { ModelType } from './ModelType';
export interface ModelInformationInterface {
label: string;
label_multiple: string;
url_overview?: string;
url_detail?: string;
api_endpoint: ApiEndpoints;
admin_url?: string;
icon: keyof InvenTreeIconType;
}
export interface TranslatableModelInformationInterface
extends Omit<ModelInformationInterface, 'label' | 'label_multiple'> {
label: () => string;
label_multiple: () => string;
}
export type ModelDict = {
[key in keyof typeof ModelType]: TranslatableModelInformationInterface;
};
export const ModelInformationDict: ModelDict = {
part: {
label: () => t`Part`,
label_multiple: () => t`Parts`,
url_overview: '/part/category/index/parts',
url_detail: '/part/:pk/',
api_endpoint: ApiEndpoints.part_list,
admin_url: '/part/part/',
icon: 'part'
},
partparametertemplate: {
label: () => t`Part Parameter Template`,
label_multiple: () => t`Part Parameter Templates`,
url_detail: '/partparametertemplate/:pk/',
api_endpoint: ApiEndpoints.part_parameter_template_list,
icon: 'test_templates'
},
parttesttemplate: {
label: () => t`Part Test Template`,
label_multiple: () => t`Part Test Templates`,
url_detail: '/parttesttemplate/:pk/',
api_endpoint: ApiEndpoints.part_test_template_list,
icon: 'test'
},
supplierpart: {
label: () => t`Supplier Part`,
label_multiple: () => t`Supplier Parts`,
url_overview: '/purchasing/index/supplier-parts',
url_detail: '/purchasing/supplier-part/:pk/',
api_endpoint: ApiEndpoints.supplier_part_list,
admin_url: '/company/supplierpart/',
icon: 'supplier_part'
},
manufacturerpart: {
label: () => t`Manufacturer Part`,
label_multiple: () => t`Manufacturer Parts`,
url_overview: '/purchasing/index/manufacturer-parts',
url_detail: '/purchasing/manufacturer-part/:pk/',
api_endpoint: ApiEndpoints.manufacturer_part_list,
admin_url: '/company/manufacturerpart/',
icon: 'manufacturers'
},
partcategory: {
label: () => t`Part Category`,
label_multiple: () => t`Part Categories`,
url_overview: '/part/category/parts/subcategories',
url_detail: '/part/category/:pk/',
api_endpoint: ApiEndpoints.category_list,
admin_url: '/part/partcategory/',
icon: 'category'
},
stockitem: {
label: () => t`Stock Item`,
label_multiple: () => t`Stock Items`,
url_overview: '/stock/location/index/stock-items',
url_detail: '/stock/item/:pk/',
api_endpoint: ApiEndpoints.stock_item_list,
admin_url: '/stock/stockitem/',
icon: 'stock'
},
stocklocation: {
label: () => t`Stock Location`,
label_multiple: () => t`Stock Locations`,
url_overview: '/stock/location',
url_detail: '/stock/location/:pk/',
api_endpoint: ApiEndpoints.stock_location_list,
admin_url: '/stock/stocklocation/',
icon: 'location'
},
stocklocationtype: {
label: () => t`Stock Location Type`,
label_multiple: () => t`Stock Location Types`,
api_endpoint: ApiEndpoints.stock_location_type_list,
icon: 'location'
},
stockhistory: {
label: () => t`Stock History`,
label_multiple: () => t`Stock Histories`,
api_endpoint: ApiEndpoints.stock_tracking_list,
icon: 'history'
},
build: {
label: () => t`Build`,
label_multiple: () => t`Builds`,
url_overview: '/manufacturing/index/buildorders/',
url_detail: '/manufacturing/build-order/:pk/',
api_endpoint: ApiEndpoints.build_order_list,
admin_url: '/build/build/',
icon: 'build_order'
},
buildline: {
label: () => t`Build Line`,
label_multiple: () => t`Build Lines`,
url_overview: '/build/line',
url_detail: '/build/line/:pk/',
api_endpoint: ApiEndpoints.build_line_list,
icon: 'build_order'
},
builditem: {
label: () => t`Build Item`,
label_multiple: () => t`Build Items`,
api_endpoint: ApiEndpoints.build_item_list,
icon: 'build_order'
},
company: {
label: () => t`Company`,
label_multiple: () => t`Companies`,
url_detail: '/company/:pk/',
api_endpoint: ApiEndpoints.company_list,
admin_url: '/company/company/',
icon: 'building'
},
projectcode: {
label: () => t`Project Code`,
label_multiple: () => t`Project Codes`,
url_detail: '/project-code/:pk/',
api_endpoint: ApiEndpoints.project_code_list,
icon: 'list_details'
},
purchaseorder: {
label: () => t`Purchase Order`,
label_multiple: () => t`Purchase Orders`,
url_overview: '/purchasing/index/purchaseorders',
url_detail: '/purchasing/purchase-order/:pk/',
api_endpoint: ApiEndpoints.purchase_order_list,
admin_url: '/order/purchaseorder/',
icon: 'purchase_orders'
},
purchaseorderlineitem: {
label: () => t`Purchase Order Line`,
label_multiple: () => t`Purchase Order Lines`,
api_endpoint: ApiEndpoints.purchase_order_line_list,
icon: 'purchase_orders'
},
salesorder: {
label: () => t`Sales Order`,
label_multiple: () => t`Sales Orders`,
url_overview: '/sales/index/salesorders',
url_detail: '/sales/sales-order/:pk/',
api_endpoint: ApiEndpoints.sales_order_list,
admin_url: '/order/salesorder/',
icon: 'sales_orders'
},
salesordershipment: {
label: () => t`Sales Order Shipment`,
label_multiple: () => t`Sales Order Shipments`,
url_detail: '/sales/shipment/:pk/',
api_endpoint: ApiEndpoints.sales_order_shipment_list,
icon: 'sales_orders'
},
returnorder: {
label: () => t`Return Order`,
label_multiple: () => t`Return Orders`,
url_overview: '/sales/index/returnorders',
url_detail: '/sales/return-order/:pk/',
api_endpoint: ApiEndpoints.return_order_list,
admin_url: '/order/returnorder/',
icon: 'return_orders'
},
returnorderlineitem: {
label: () => t`Return Order Line Item`,
label_multiple: () => t`Return Order Line Items`,
api_endpoint: ApiEndpoints.return_order_line_list,
icon: 'return_orders'
},
address: {
label: () => t`Address`,
label_multiple: () => t`Addresses`,
url_detail: '/address/:pk/',
api_endpoint: ApiEndpoints.address_list,
icon: 'address'
},
contact: {
label: () => t`Contact`,
label_multiple: () => t`Contacts`,
url_detail: '/contact/:pk/',
api_endpoint: ApiEndpoints.contact_list,
icon: 'group'
},
owner: {
label: () => t`Owner`,
label_multiple: () => t`Owners`,
url_detail: '/owner/:pk/',
api_endpoint: ApiEndpoints.owner_list,
icon: 'group'
},
user: {
label: () => t`User`,
label_multiple: () => t`Users`,
url_detail: '/core/user/:pk/',
api_endpoint: ApiEndpoints.user_list,
icon: 'user'
},
group: {
label: () => t`Group`,
label_multiple: () => t`Groups`,
url_detail: '/core/group/:pk/',
api_endpoint: ApiEndpoints.group_list,
admin_url: '/auth/group/',
icon: 'group'
},
importsession: {
label: () => t`Import Session`,
label_multiple: () => t`Import Sessions`,
url_overview: '/settings/admin/import',
url_detail: '/import/:pk/',
api_endpoint: ApiEndpoints.import_session_list,
icon: 'import'
},
labeltemplate: {
label: () => t`Label Template`,
label_multiple: () => t`Label Templates`,
url_overview: '/settings/admin/labels',
url_detail: '/settings/admin/labels/:pk/',
api_endpoint: ApiEndpoints.label_list,
icon: 'labels'
},
reporttemplate: {
label: () => t`Report Template`,
label_multiple: () => t`Report Templates`,
url_overview: '/settings/admin/reports',
url_detail: '/settings/admin/reports/:pk/',
api_endpoint: ApiEndpoints.report_list,
icon: 'reports'
},
pluginconfig: {
label: () => t`Plugin Configuration`,
label_multiple: () => t`Plugin Configurations`,
url_overview: '/settings/admin/plugin',
url_detail: '/settings/admin/plugin/:pk/',
api_endpoint: ApiEndpoints.plugin_list,
icon: 'plugin'
},
contenttype: {
label: () => t`Content Type`,
label_multiple: () => t`Content Types`,
api_endpoint: ApiEndpoints.content_type_list,
icon: 'list_details'
},
selectionlist: {
label: () => t`Selection List`,
label_multiple: () => t`Selection Lists`,
api_endpoint: ApiEndpoints.selectionlist_list,
icon: 'list_details'
},
error: {
label: () => t`Error`,
label_multiple: () => t`Errors`,
api_endpoint: ApiEndpoints.error_report_list,
url_overview: '/settings/admin/errors',
url_detail: '/settings/admin/errors/:pk/',
icon: 'exclamation'
}
};

View File

@ -0,0 +1,42 @@
import type { ApiEndpoints } from '../enums/ApiEndpoints';
import type { PathParams } from '../types/Core';
/**
* Function to return the API prefix.
* For now it is fixed, but may be configurable in the future.
*/
export function apiPrefix(): string {
return '/api/';
}
/**
* Construct an API URL with an endpoint and (optional) pk value
*/
export function apiUrl(
endpoint: ApiEndpoints | string,
pk?: any,
pathParams?: PathParams
): string {
let _url = endpoint;
// If the URL does not start with a '/', add the API prefix
if (!_url.startsWith('/')) {
_url = apiPrefix() + _url;
}
if (_url && pk) {
if (_url.indexOf(':id') >= 0) {
_url = _url.replace(':id', `${pk}`);
} else {
_url += `${pk}/`;
}
}
if (_url && pathParams) {
for (const key in pathParams) {
_url = _url.replace(`:${key}`, `${pathParams[key]}`);
}
}
return _url;
}

View File

@ -0,0 +1,70 @@
import type { NavigateFunction } from 'react-router-dom';
import { ModelInformationDict } from '../enums/ModelInformation';
import type { ModelType } from '../enums/ModelType';
import { cancelEvent } from './Events';
export const getBaseUrl = (): string =>
(window as any).INVENTREE_SETTINGS?.base_url || 'web';
/**
* Returns the detail view URL for a given model type
*/
export function getDetailUrl(
model: ModelType,
pk: number | string,
absolute?: boolean
): string {
const modelInfo = ModelInformationDict[model];
if (pk === undefined || pk === null) {
return '';
}
if (!!pk && modelInfo && modelInfo.url_detail) {
const url = modelInfo.url_detail.replace(':pk', pk.toString());
const base = getBaseUrl();
if (absolute && base) {
return `/${base}${url}`;
} else {
return url;
}
}
console.error(`No detail URL found for model ${model} <${pk}>`);
return '';
}
/*
* Navigate to a provided link.
* - If the link is to be opened externally, open it in a new tab.
* - Otherwise, navigate using the provided navigate function.
*/
export const navigateToLink = (
link: string,
navigate: NavigateFunction,
event: any
) => {
cancelEvent(event);
const base = `/${getBaseUrl()}`;
if (event?.ctrlKey || event?.shiftKey) {
// Open the link in a new tab
let url = link;
if (link.startsWith('/') && !link.startsWith(base)) {
url = `${base}${link}`;
}
window.open(url, '_blank');
} else {
// Navigate internally
let url = link;
if (link.startsWith(base)) {
// Strip the base URL from the link
url = link.replace(base, '');
}
navigate(url);
}
};

View File

@ -0,0 +1,30 @@
import {
INVENTREE_PLUGIN_VERSION,
type InvenTreePluginContext
} from '../types/Plugins';
function extractVersion(version: string) {
// Extract the version number from the string
const [major, minor, patch] = version
.split('.')
.map((v) => Number.parseInt(v, 10));
return { major, minor, patch };
}
/**
* Check th e
*/
export function checkPluginVersion(context: InvenTreePluginContext) {
const pluginVersion = extractVersion(INVENTREE_PLUGIN_VERSION);
const systemVersion = extractVersion(context.version.inventree);
const mismatch = `Plugin version mismatch! Expected version ${INVENTREE_PLUGIN_VERSION}, got ${context.version}`;
// A major version mismatch indicates a potentially breaking change
if (pluginVersion.major !== systemVersion.major) {
console.warn(mismatch);
} else if (INVENTREE_PLUGIN_VERSION != context.version.inventree) {
console.info(mismatch);
}
}

23
src/frontend/lib/index.ts Normal file
View File

@ -0,0 +1,23 @@
// Constant value definitions
export {
INVENTREE_PLUGIN_VERSION,
INVENTREE_REACT_VERSION,
INVENTREE_REACT_DOM_VERSION,
INVENTREE_MANTINE_VERSION
} from './types/Plugins';
// Common type definitions
export { ApiEndpoints } from './enums/ApiEndpoints';
export { ModelType } from './enums/ModelType';
export { UserRoles, UserPermissions } from './enums/Roles';
export type { InvenTreePluginContext } from './types/Plugins';
// Common utility functions
export { apiUrl } from './functions/Api';
export {
getBaseUrl,
getDetailUrl,
navigateToLink
} from './functions/Navigation';
export { checkPluginVersion } from './functions/Plugins';

View File

@ -0,0 +1,51 @@
export interface AuthContext {
status: number;
data: { flows: Flow[] };
meta: { is_authenticated: boolean };
}
export enum FlowEnum {
VerifyEmail = 'verify_email',
Login = 'login',
Signup = 'signup',
ProviderRedirect = 'provider_redirect',
ProviderSignup = 'provider_signup',
ProviderToken = 'provider_token',
MfaAuthenticate = 'mfa_authenticate',
Reauthenticate = 'reauthenticate',
MfaReauthenticate = 'mfa_reauthenticate'
}
export interface Flow {
id: FlowEnum;
providers?: string[];
is_pending?: boolean[];
}
export interface AuthProvider {
id: string;
name: string;
flows: string[];
client_id: string;
}
export interface AuthConfig {
account: {
authentication_method: string;
};
socialaccount: { providers: AuthProvider[] };
mfa: {
supported_types: string[];
};
usersessions: {
track_activity: boolean;
};
}
// Errors
export type ErrorResponse = {
data: any;
status: number;
statusText: string;
message?: string;
};

View File

@ -0,0 +1,13 @@
import type { MantineSize } from '@mantine/core';
export type UiSizeType = MantineSize | string | number;
export interface UserTheme {
primaryColor: string;
whiteColor: string;
blackColor: string;
radius: UiSizeType;
loader: string;
}
export type PathParams = Record<string, string | number>;

View File

@ -0,0 +1,69 @@
import type { ModelType } from '../enums/ModelType';
/**
* Interface for the table filter choice
*/
export type TableFilterChoice = {
value: string;
label: string;
};
/**
* Available filter types
*
* boolean: A simple true/false filter
* choice: A filter which allows selection from a list of (supplied)
* date: A filter which allows selection from a date input
* text: A filter which allows raw text input
* api: A filter which fetches its options from an API endpoint
*/
export type TableFilterType = 'boolean' | 'choice' | 'date' | 'text' | 'api';
/**
* Interface for the table filter type. Provides a number of options for selecting filter value:
*
* name: The name of the filter (used for query string)
* label: The label to display in the UI (human readable)
* description: A description of the filter (human readable)
* type: The type of filter (see TableFilterType)
* choices: A list of TableFilterChoice objects
* choiceFunction: A function which returns a list of TableFilterChoice objects
* defaultValue: The default value for the filter
* value: The current value of the filter
* displayValue: The current display value of the filter
* active: Whether the filter is active (false = hidden, not used)
* apiUrl: The API URL to use for fetching dynamic filter options
* model: The model type to use for fetching dynamic filter options
* modelRenderer: A function to render a simple text version of the model type
*/
export type TableFilter = {
name: string;
label: string;
description?: string;
type?: TableFilterType;
choices?: TableFilterChoice[];
choiceFunction?: () => TableFilterChoice[];
defaultValue?: any;
value?: any;
displayValue?: any;
active?: boolean;
apiUrl?: string;
model?: ModelType;
modelRenderer?: (instance: any) => string;
};
/*
* Type definition for representing the state of a group of filters.
* These may be applied to a data view (e.g. table, calendar) to filter the displayed data.
*
* filterKey: A unique key for the filter set
* activeFilters: An array of active filters
* setActiveFilters: A function to set the active filters
* clearActiveFilters: A function to clear all active filters
*/
export type FilterSetState = {
filterKey: string;
activeFilters: TableFilter[];
setActiveFilters: (filters: TableFilter[]) => void;
clearActiveFilters: () => void;
};

View File

@ -0,0 +1,187 @@
import type { DefaultMantineColor, MantineStyleProp } from '@mantine/core';
import type { UseFormReturnType } from '@mantine/form';
import type { ReactNode } from 'react';
import type { FieldValues, UseFormReturn } from 'react-hook-form';
import type { ApiEndpoints } from '../enums/ApiEndpoints';
import type { ModelType } from '../enums/ModelType';
import type { PathParams, UiSizeType } from './Core';
import type { TableState } from './Tables';
export interface ApiFormAction {
text: string;
variant?: 'outline';
color?: DefaultMantineColor;
onClick: () => void;
}
export type ApiFormData = UseFormReturnType<Record<string, unknown>>;
export type ApiFormAdjustFilterType = {
filters: any;
data: FieldValues;
};
export type ApiFormFieldChoice = {
value: any;
display_name: string;
};
// Define individual headers in a table field
export type ApiFormFieldHeader = {
title: string;
style?: MantineStyleProp;
};
/** Definition of the ApiForm field component.
* - The 'name' attribute *must* be provided
* - All other attributes are optional, and may be provided by the API
* - However, they can be overridden by the user
*
* @param name : The name of the field
* @param label : The label to display for the field
* @param value : The value of the field
* @param default : The default value of the field
* @param icon : An icon to display next to the field
* @param field_type : The type of field to render
* @param api_url : The API endpoint to fetch data from (for related fields)
* @param pk_field : The primary key field for the related field (default = "pk")
* @param model : The model to use for related fields
* @param filters : Optional API filters to apply to related fields
* @param required : Whether the field is required
* @param hidden : Whether the field is hidden
* @param disabled : Whether the field is disabled
* @param error : Optional error message to display
* @param exclude : Whether to exclude the field from the submitted data
* @param placeholder : The placeholder text to display
* @param description : The description to display for the field
* @param preFieldContent : Content to render before the field
* @param postFieldContent : Content to render after the field
* @param onValueChange : Callback function to call when the field value changes
* @param adjustFilters : Callback function to adjust the filters for a related field before a query is made
* @param adjustValue : Callback function to adjust the value of the field before it is sent to the API
* @param addRow : Callback function to add a new row to a table field
* @param onKeyDown : Callback function to get which key was pressed in the form to handle submission on enter
*/
export type ApiFormFieldType = {
label?: string;
value?: any;
default?: any;
icon?: ReactNode;
field_type?:
| 'related field'
| 'email'
| 'url'
| 'string'
| 'icon'
| 'boolean'
| 'date'
| 'datetime'
| 'integer'
| 'decimal'
| 'float'
| 'number'
| 'choice'
| 'file upload'
| 'nested object'
| 'dependent field'
| 'table';
api_url?: string;
pk_field?: string;
model?: ModelType;
modelRenderer?: (instance: any) => ReactNode;
filters?: any;
child?: ApiFormFieldType;
children?: { [key: string]: ApiFormFieldType };
required?: boolean;
error?: string;
choices?: ApiFormFieldChoice[];
hidden?: boolean;
disabled?: boolean;
exclude?: boolean;
read_only?: boolean;
placeholder?: string;
description?: string;
preFieldContent?: JSX.Element;
postFieldContent?: JSX.Element;
adjustValue?: (value: any) => any;
onValueChange?: (value: any, record?: any) => void;
adjustFilters?: (value: ApiFormAdjustFilterType) => any;
addRow?: () => any;
headers?: ApiFormFieldHeader[];
depends_on?: string[];
};
export type ApiFormFieldSet = Record<string, ApiFormFieldType>;
/**
* Properties for the ApiForm component
* @param url : The API endpoint to fetch the form data from
* @param pk : Optional primary-key value when editing an existing object
* @param pk_field : Optional primary-key field name (default: pk)
* @param pathParams : Optional path params for the url
* @param method : Optional HTTP method to use when submitting the form (default: GET)
* @param fields : The fields to render in the form
* @param submitText : Optional custom text to display on the submit button (default: Submit)4
* @param submitColor : Optional custom color for the submit button (default: green)
* @param fetchInitialData : Optional flag to fetch initial data from the server (default: true)
* @param preFormContent : Optional content to render before the form fields
* @param postFormContent : Optional content to render after the form fields
* @param successMessage : Optional message to display on successful form submission
* @param onFormSuccess : A callback function to call when the form is submitted successfully.
* @param onFormError : A callback function to call when the form is submitted with errors.
* @param processFormData : A callback function to process the form data before submission
* @param checkClose: A callback function to check if the form can be closed after submission
* @param modelType : Define a model type for this form
* @param follow : Boolean, follow the result of the form (if possible)
* @param table : Table to update on success (if provided)
*/
export interface ApiFormProps {
url: ApiEndpoints | string;
pk?: number | string;
pk_field?: string;
pathParams?: PathParams;
queryParams?: URLSearchParams;
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
fields?: ApiFormFieldSet;
focus?: string;
initialData?: FieldValues;
submitText?: string;
submitColor?: string;
fetchInitialData?: boolean;
ignorePermissionCheck?: boolean;
preFormContent?: JSX.Element;
preFormWarning?: string;
preFormSuccess?: string;
postFormContent?: JSX.Element;
successMessage?: string | null;
onFormSuccess?: (data: any, form: UseFormReturn) => void;
onFormError?: (response: any, form: UseFormReturn) => void;
processFormData?: (data: any, form: UseFormReturn) => any;
checkClose?: (data: any, form: UseFormReturn) => boolean;
table?: TableState;
modelType?: ModelType;
follow?: boolean;
actions?: ApiFormAction[];
timeout?: number;
}
/**
* @param title : The title to display in the modal header
* @param cancelText : Optional custom text to display on the cancel button (default: Cancel)
* @param cancelColor : Optional custom color for the cancel button (default: blue)
* @param onClose : A callback function to call when the modal is closed.
* @param onOpen : A callback function to call when the modal is opened.
*/
export interface ApiFormModalProps extends ApiFormProps {
title: string;
cancelText?: string;
cancelColor?: string;
onClose?: () => void;
onOpen?: () => void;
closeOnClickOutside?: boolean;
size?: UiSizeType;
}
export interface BulkEditApiFormModalProps extends ApiFormModalProps {
items: number[];
}

View File

@ -0,0 +1,9 @@
import type { Icon, IconProps } from '@tabler/icons-react';
export type TablerIconType = React.ForwardRefExoticComponent<
Omit<IconProps, 'ref'> & React.RefAttributes<Icon>
>;
export type InvenTreeIconType = {
[key: string]: TablerIconType;
};

View File

@ -0,0 +1,17 @@
import type { UiSizeType } from './Core';
export interface UseModalProps {
title: string;
children: React.ReactElement;
size?: UiSizeType;
onOpen?: () => void;
onClose?: () => void;
closeOnClickOutside?: boolean;
}
export interface UseModalReturn {
open: () => void;
close: () => void;
toggle: () => void;
modal: React.ReactElement;
}

View File

@ -0,0 +1,87 @@
import type { MantineColorScheme, MantineTheme } from '@mantine/core';
import type { QueryClient } from '@tanstack/react-query';
import type { AxiosInstance } from 'axios';
import type { NavigateFunction } from 'react-router-dom';
import type { ModelType } from '../enums/ModelType';
import type { ApiFormModalProps, BulkEditApiFormModalProps } from './Forms';
import type { UseModalReturn } from './Modals';
import type { SettingsStateProps } from './Settings';
import type { UserStateProps } from './User';
export interface PluginProps {
name: string;
slug: string;
version: null | string;
}
export interface PluginVersion {
inventree: string;
react: string;
reactDom: string;
mantine: string;
}
export type InvenTreeFormsContext = {
bulkEdit: (props: BulkEditApiFormModalProps) => UseModalReturn;
create: (props: ApiFormModalProps) => UseModalReturn;
delete: (props: ApiFormModalProps) => UseModalReturn;
edit: (props: ApiFormModalProps) => UseModalReturn;
};
/**
* A set of properties which are passed to a plugin,
* for rendering an element in the user interface.
*
* @param version - The version of the running InvenTree software stack
* @param api - The Axios API instance (see ../states/ApiState.tsx)
* @param user - The current user instance (see ../states/UserState.tsx)
* @param userSettings - The current user settings (see ../states/SettingsState.tsx)
* @param globalSettings - The global settings (see ../states/SettingsState.tsx)
* @param navigate - The navigation function (see react-router-dom)
* @param theme - The current Mantine theme
* @param colorScheme - The current Mantine color scheme (e.g. 'light' / 'dark')
* @param host - The current host URL
* @param locale - The current locale string (e.g. 'en' / 'de')
* @param model - The model type associated with the rendered component (if applicable)
* @param id - The ID (primary key) of the model instance for the plugin (if applicable)
* @param instance - The model instance data (if available)
* @param reloadContent - A function which can be called to reload the plugin content
* @param reloadInstance - A function which can be called to reload the model instance
* @param context - Any additional context data which may be passed to the plugin
*/
export type InvenTreePluginContext = {
version: PluginVersion;
api: AxiosInstance;
queryClient: QueryClient;
user: UserStateProps;
userSettings: SettingsStateProps;
globalSettings: SettingsStateProps;
host: string;
locale: string;
navigate: NavigateFunction;
theme: MantineTheme;
forms: InvenTreeFormsContext;
colorScheme: MantineColorScheme;
model?: ModelType | string;
id?: string | number | null;
instance?: any;
reloadContent?: () => void;
reloadInstance?: () => void;
context?: any;
};
/*
* The version of the InvenTree plugin context interface.
* This number should be incremented if the interface changes.
*/
// @ts-ignore
export const INVENTREE_PLUGIN_VERSION: string = __INVENTREE_LIB_VERSION__;
// @ts-ignore
export const INVENTREE_REACT_VERSION: string = __INVENTREE_REACT_VERSION__;
// @ts-ignore
export const INVENTREE_REACT_DOM_VERSION: string =
// @ts-ignore
__INVENTREE_REACT_DOM_VERSION__;
// @ts-ignore
export const INVENTREE_MANTINE_VERSION: string = __INVENTREE_MANTINE_VERSION__;

View File

@ -0,0 +1,8 @@
export interface Host {
host: string;
name: string;
}
export interface HostList {
[key: string]: Host;
}

View File

@ -0,0 +1,55 @@
import type { ApiEndpoints } from '../enums/ApiEndpoints';
import type { PathParams } from './Core';
export enum SettingTyp {
InvenTree = 'inventree',
Plugin = 'plugin',
User = 'user',
Notification = 'notification'
}
export enum SettingType {
Boolean = 'boolean',
Integer = 'integer',
String = 'string',
Choice = 'choice',
Model = 'related field'
}
// Type interface defining a single 'setting' object
export interface Setting {
pk: number;
key: string;
value: string;
name: string;
description: string;
type: SettingType;
units: string;
choices: SettingChoice[];
model_name: string | null;
model_filters: Record<string, any> | null;
api_url: string | null;
typ: SettingTyp;
plugin?: string;
method?: string;
required?: boolean;
}
export interface SettingChoice {
value: string;
display_name: string;
}
export type SettingsLookup = {
[key: string]: string;
};
export interface SettingsStateProps {
settings: Setting[];
lookup: SettingsLookup;
fetchSettings: () => Promise<boolean>;
endpoint: ApiEndpoints;
pathParams?: PathParams;
getSetting: (key: string, default_value?: string) => string; // Return a raw setting value
isSet: (key: string, default_value?: boolean) => boolean; // Check a "boolean" setting
}

View File

@ -0,0 +1,69 @@
import type { SetURLSearchParams } from 'react-router-dom';
import type { FilterSetState } from './Filters';
/*
* Type definition for representing the state of a table:
*
* tableKey: A unique key for the table. When this key changes, the table will be refreshed.
* refreshTable: A callback function to externally refresh the table.
* isLoading: A boolean flag to indicate if the table is currently loading data
* setIsLoading: A function to set the isLoading flag
* filterSet: A group of active filters
* queryFilters: A map of query filters (e.g. ?active=true&overdue=false) passed in the URL
* setQueryFilters: A function to set the query filters
* clearQueryFilters: A function to clear all query filters
* expandedRecords: An array of expanded records (rows) in the table
* setExpandedRecords: A function to set the expanded records
* isRowExpanded: A function to determine if a record is expanded
* selectedRecords: An array of selected records (rows) in the table
* selectedIds: An array of primary key values for selected records
* hasSelectedRecords: A boolean flag to indicate if any records are selected
* setSelectedRecords: A function to set the selected records
* clearSelectedRecords: A function to clear all selected records
* hiddenColumns: An array of hidden column names
* setHiddenColumns: A function to set the hidden columns
* searchTerm: The current search term for the table
* setSearchTerm: A function to set the search term
* recordCount: The total number of records in the table
* setRecordCount: A function to set the record count
* page: The current page number
* setPage: A function to set the current page number
* pageSize: The number of records per page
* setPageSize: A function to set the number of records per page
* records: An array of records (rows) in the table
* setRecords: A function to set the records
* updateRecord: A function to update a single record in the table
* idAccessor: The name of the primary key field in the records (default = 'pk')
*/
export type TableState = {
tableKey: string;
refreshTable: () => void;
isLoading: boolean;
setIsLoading: (value: boolean) => void;
filterSet: FilterSetState;
queryFilters: URLSearchParams;
setQueryFilters: SetURLSearchParams;
clearQueryFilters: () => void;
expandedRecords: any[];
setExpandedRecords: (records: any[]) => void;
isRowExpanded: (pk: number) => boolean;
selectedRecords: any[];
selectedIds: any[];
hasSelectedRecords: boolean;
setSelectedRecords: (records: any[]) => void;
clearSelectedRecords: () => void;
hiddenColumns: string[];
setHiddenColumns: (columns: string[]) => void;
searchTerm: string;
setSearchTerm: (term: string) => void;
recordCount: number;
setRecordCount: (count: number) => void;
page: number;
setPage: (page: number) => void;
pageSize: number;
setPageSize: (pageSize: number) => void;
records: any[];
setRecords: (records: any[]) => void;
updateRecord: (record: any) => void;
idAccessor?: string;
};

View File

@ -0,0 +1,62 @@
import type { ModelType } from '../enums/ModelType';
import type { UserPermissions, UserRoles } from '../enums/Roles';
export interface UserProfile {
language: string;
theme: any;
widgets: any;
displayname: string | null;
position: string | null;
status: string | null;
location: string | null;
active: boolean;
contact: string | null;
type: string;
organisation: string | null;
primary_group: number | null;
}
// Type interface fully defining the current user
export interface UserProps {
pk: number;
username: string;
first_name: string;
last_name: string;
email: string;
is_staff?: boolean;
is_superuser?: boolean;
roles?: Record<string, string[]>;
permissions?: Record<string, string[]>;
groups: any[] | null;
profile: UserProfile;
}
export interface UserStateProps {
user: UserProps | undefined;
is_authed: boolean;
userId: () => number | undefined;
username: () => string;
setAuthenticated: (authed?: boolean) => void;
fetchUserToken: () => Promise<void>;
setUser: (newUser: UserProps | undefined) => void;
getUser: () => UserProps | undefined;
fetchUserState: () => Promise<void>;
clearUserState: () => void;
checkUserRole: (role: UserRoles, permission: UserPermissions) => boolean;
hasDeleteRole: (role: UserRoles) => boolean;
hasChangeRole: (role: UserRoles) => boolean;
hasAddRole: (role: UserRoles) => boolean;
hasViewRole: (role: UserRoles) => boolean;
checkUserPermission: (
model: ModelType,
permission: UserPermissions
) => boolean;
hasDeletePermission: (model: ModelType) => boolean;
hasChangePermission: (model: ModelType) => boolean;
hasAddPermission: (model: ModelType) => boolean;
hasViewPermission: (model: ModelType) => boolean;
isAuthed: () => boolean;
isLoggedIn: () => boolean;
isStaff: () => boolean;
isSuperuser: () => boolean;
}

View File

@ -1,11 +1,40 @@
{ {
"name": "inventreeui", "name": "@inventreedb/ui",
"private": true, "description": "UI components for the InvenTree project",
"version": "0.1.0", "version": "0.0.8",
"private": false,
"type": "module", "type": "module",
"license": "MIT",
"keywords": [
"inventree"
],
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": "./dist/index.js"
},
"files": [
"dist",
"lib",
"LICENSE",
"README.md",
"CHANGELOG.md"
],
"homepage": "https://inventree.org",
"repository": {
"type": "git",
"url": "https://github.com/inventree/inventree"
},
"author": {
"name": "InvenTree Developers",
"email": "support@inventree.org",
"url": "https://inventree.org",
"org": "InvenTree"
},
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build --emptyOutDir",
"lib": "tsc --p ./tsconfig.lib.json && vite --config vite.lib.config.ts build",
"preview": "vite preview", "preview": "vite preview",
"extract": "lingui extract", "extract": "lingui extract",
"compile": "lingui compile --typescript" "compile": "lingui compile --typescript"
@ -101,6 +130,7 @@
"typescript": "^5.8.2", "typescript": "^5.8.2",
"vite": "^6.2.6", "vite": "^6.2.6",
"vite-plugin-babel-macros": "^1.0.6", "vite-plugin-babel-macros": "^1.0.6",
"vite-plugin-dts": "^4.5.3",
"vite-plugin-istanbul": "^6.0.2" "vite-plugin-istanbul": "^6.0.2"
} }
} }

View File

@ -1,16 +1,16 @@
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { ModelInformationDict } from '@lib/enums/ModelInformation';
import type { ModelType } from '@lib/enums/ModelType';
import { apiUrl } from '@lib/functions/Api';
import { getDetailUrl } from '@lib/functions/Navigation';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { Box, Divider, Modal } from '@mantine/core'; import { Box, Divider, Modal } from '@mantine/core';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { type NavigateFunction, useNavigate } from 'react-router-dom'; import { type NavigateFunction, useNavigate } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import type { ModelType } from '../../enums/ModelType';
import { extractErrorMessage } from '../../functions/api'; import { extractErrorMessage } from '../../functions/api';
import { getDetailUrl } from '../../functions/urls';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import { StylishText } from '../items/StylishText'; import { StylishText } from '../items/StylishText';
import { ModelInformationDict } from '../render/ModelType';
import { BarcodeInput } from './BarcodeInput'; import { BarcodeInput } from './BarcodeInput';
export default function BarcodeScanDialog({ export default function BarcodeScanDialog({

View File

@ -1,4 +1,4 @@
import type { ModelType } from '../../enums/ModelType'; import type { ModelType } from '@lib/enums/ModelType';
/** /**
* Interface defining a single barcode scan item * Interface defining a single barcode scan item

View File

@ -18,9 +18,9 @@ import { useQuery } from '@tanstack/react-query';
import QR from 'qrcode'; import QR from 'qrcode';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api';
import { api } from '../../App'; import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { apiUrl } from '../../states/ApiState';
import { useGlobalSettingsState } from '../../states/SettingsState'; import { useGlobalSettingsState } from '../../states/SettingsState';
import { CopyButton } from '../buttons/CopyButton'; import { CopyButton } from '../buttons/CopyButton';
import type { QrCodeType } from '../items/ActionDropdown'; import type { QrCodeType } from '../items/ActionDropdown';

View File

@ -2,11 +2,11 @@ import { t } from '@lingui/core/macro';
import { IconUserStar } from '@tabler/icons-react'; import { IconUserStar } from '@tabler/icons-react';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import type { ModelType } from '../../enums/ModelType'; import { ModelInformationDict } from '@lib/enums/ModelInformation';
import type { ModelType } from '@lib/enums/ModelType';
import { generateUrl } from '../../functions/urls'; import { generateUrl } from '../../functions/urls';
import { useServerApiState } from '../../states/ApiState'; import { useServerApiState } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import { ModelInformationDict } from '../render/ModelType';
import { ActionButton } from './ActionButton'; import { ActionButton } from './ActionButton';
export type AdminButtonProps = { export type AdminButtonProps = {

View File

@ -1,6 +1,7 @@
import { Button, Tooltip } from '@mantine/core'; import { Button, Tooltip } from '@mantine/core';
import { InvenTreeIcon, type InvenTreeIconType } from '../../functions/icons'; import type { InvenTreeIconType } from '@lib/types/Icons';
import { InvenTreeIcon } from '../../functions/icons';
/** /**
* A "primary action" button for display on a page detail, (for example) * A "primary action" button for display on a page detail, (for example)
@ -15,7 +16,7 @@ export default function PrimaryActionButton({
}: Readonly<{ }: Readonly<{
title: string; title: string;
tooltip?: string; tooltip?: string;
icon?: InvenTreeIconType; icon?: keyof InvenTreeIconType;
color?: string; color?: string;
hidden?: boolean; hidden?: boolean;
onClick: () => void; onClick: () => void;

View File

@ -1,19 +1,19 @@
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import type { ModelType } from '@lib/enums/ModelType';
import { apiUrl } from '@lib/functions/Api';
import type { ApiFormFieldSet } from '@lib/types/Forms';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { IconPrinter, IconReport, IconTags } from '@tabler/icons-react'; import { IconPrinter, IconReport, IconTags } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { api } from '../../App'; import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import type { ModelType } from '../../enums/ModelType';
import { extractAvailableFields } from '../../functions/forms'; import { extractAvailableFields } from '../../functions/forms';
import useDataOutput from '../../hooks/UseDataOutput'; import useDataOutput from '../../hooks/UseDataOutput';
import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useCreateApiFormModal } from '../../hooks/UseForm';
import { apiUrl } from '../../states/ApiState';
import { import {
useGlobalSettingsState, useGlobalSettingsState,
useUserSettingsState useUserSettingsState
} from '../../states/SettingsState'; } from '../../states/SettingsState';
import type { ApiFormFieldSet } from '../forms/fields/ApiFormField';
import { ActionDropdown } from '../items/ActionDropdown'; import { ActionDropdown } from '../items/ActionDropdown';
export function PrintingActions({ export function PrintingActions({

View File

@ -14,9 +14,9 @@ import {
IconLogin IconLogin
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import type { AuthProvider } from '@lib/types/Auth';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { ProviderLogin } from '../../functions/auth'; import { ProviderLogin } from '../../functions/auth';
import type { Provider } from '../../states/states';
const brandIcons: { [key: string]: JSX.Element } = { const brandIcons: { [key: string]: JSX.Element } = {
google: <IconBrandGoogle />, google: <IconBrandGoogle />,
@ -32,7 +32,7 @@ const brandIcons: { [key: string]: JSX.Element } = {
microsoft: <IconBrandAzure /> microsoft: <IconBrandAzure />
}; };
export function SsoButton({ provider }: Readonly<{ provider: Provider }>) { export function SsoButton({ provider }: Readonly<{ provider: AuthProvider }>) {
return ( return (
<Tooltip <Tooltip
label={t`You will be redirected to the provider for further actions.`} label={t`You will be redirected to the provider for further actions.`}
@ -48,6 +48,6 @@ export function SsoButton({ provider }: Readonly<{ provider: Provider }>) {
</Tooltip> </Tooltip>
); );
} }
function getBrandIcon(provider: Provider) { function getBrandIcon(provider: AuthProvider) {
return brandIcons[provider.id] || <IconLogin />; return brandIcons[provider.id] || <IconLogin />;
} }

View File

@ -10,8 +10,8 @@ import {
import { IconChevronDown } from '@tabler/icons-react'; import { IconChevronDown } from '@tabler/icons-react';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import type { TablerIconType } from '@lib/types/Icons';
import { identifierString } from '../../functions/conversion'; import { identifierString } from '../../functions/conversion';
import type { TablerIconType } from '../../functions/icons';
import * as classes from './SplitButton.css'; import * as classes from './SplitButton.css';
interface SplitButtonOption { interface SplitButtonOption {

View File

@ -1,10 +1,10 @@
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { ModelType } from '@lib/enums/ModelType';
import { apiUrl } from '@lib/functions/Api';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { showNotification } from '@mantine/notifications'; import { showNotification } from '@mantine/notifications';
import { IconBell } from '@tabler/icons-react'; import { IconBell } from '@tabler/icons-react';
import { useApi } from '../../contexts/ApiContext'; import { useApi } from '../../contexts/ApiContext';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { apiUrl } from '../../states/ApiState';
import { ActionButton } from './ActionButton'; import { ActionButton } from './ActionButton';
export default function StarredToggleButton({ export default function StarredToggleButton({

View File

@ -4,6 +4,7 @@ import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction'; import interactionPlugin from '@fullcalendar/interaction';
import FullCalendar from '@fullcalendar/react'; import FullCalendar from '@fullcalendar/react';
import type { TableFilter } from '@lib/types/Filters';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { import {
ActionIcon, ActionIcon,
@ -27,7 +28,6 @@ import {
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import type { CalendarState } from '../../hooks/UseCalendar'; import type { CalendarState } from '../../hooks/UseCalendar';
import { useLocalState } from '../../states/LocalState'; import { useLocalState } from '../../states/LocalState';
import type { TableFilter } from '../../tables/Filter';
import { FilterSelectDrawer } from '../../tables/FilterSelectDrawer'; import { FilterSelectDrawer } from '../../tables/FilterSelectDrawer';
import { TableSearchInput } from '../../tables/Search'; import { TableSearchInput } from '../../tables/Search';
import { Boundary } from '../Boundary'; import { Boundary } from '../Boundary';

View File

@ -3,6 +3,13 @@ import type {
EventClickArg, EventClickArg,
EventContentArg EventContentArg
} from '@fullcalendar/core'; } from '@fullcalendar/core';
import { ModelInformationDict } from '@lib/enums/ModelInformation';
import type { ModelType } from '@lib/enums/ModelType';
import type { UserRoles } from '@lib/enums/Roles';
import { apiUrl } from '@lib/functions/Api';
import { getDetailUrl } from '@lib/functions/Navigation';
import { navigateToLink } from '@lib/functions/Navigation';
import type { TableFilter } from '@lib/types/Filters';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { ActionIcon, Group, Text } from '@mantine/core'; import { ActionIcon, Group, Text } from '@mantine/core';
import { hideNotification, showNotification } from '@mantine/notifications'; import { hideNotification, showNotification } from '@mantine/notifications';
@ -15,22 +22,15 @@ import dayjs from 'dayjs';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
import type { ModelType } from '../../enums/ModelType';
import type { UserRoles } from '../../enums/Roles';
import { navigateToLink } from '../../functions/navigation';
import { getDetailUrl } from '../../functions/urls';
import useCalendar from '../../hooks/UseCalendar'; import useCalendar from '../../hooks/UseCalendar';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import { import {
AssignedToMeFilter, AssignedToMeFilter,
HasProjectCodeFilter, HasProjectCodeFilter,
OrderStatusFilter, OrderStatusFilter,
ProjectCodeFilter, ProjectCodeFilter,
ResponsibleFilter, ResponsibleFilter
type TableFilter
} from '../../tables/Filter'; } from '../../tables/Filter';
import { ModelInformationDict } from '../render/ModelType';
import { StatusRenderer } from '../render/StatusRenderer'; import { StatusRenderer } from '../render/StatusRenderer';
import Calendar from './Calendar'; import Calendar from './Calendar';

View File

@ -1,6 +1,6 @@
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '@lib/enums/ModelType';
import { useGlobalSettingsState } from '../../states/SettingsState'; import { useGlobalSettingsState } from '../../states/SettingsState';
import type { DashboardWidgetProps } from './DashboardWidget'; import type { DashboardWidgetProps } from './DashboardWidget';
import ColorToggleDashboardWidget from './widgets/ColorToggleWidget'; import ColorToggleDashboardWidget from './widgets/ColorToggleWidget';

View File

@ -14,10 +14,10 @@ import { IconMailCheck } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api';
import { api } from '../../../App'; import { api } from '../../../App';
import { formatDate } from '../../../defaults/formatters'; import { formatDate } from '../../../defaults/formatters';
import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
import { StylishText } from '../../items/StylishText'; import { StylishText } from '../../items/StylishText';

View File

@ -4,17 +4,15 @@ import { useQuery } from '@tanstack/react-query';
import { type ReactNode, useCallback } from 'react'; import { type ReactNode, useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ModelInformationDict } from '@lib/enums/ModelInformation';
import type { ModelType } from '@lib/enums/ModelType';
import { apiUrl } from '@lib/functions/Api';
import { navigateToLink } from '@lib/functions/Navigation';
import type { InvenTreeIconType } from '@lib/types/Icons';
import { useApi } from '../../../contexts/ApiContext'; import { useApi } from '../../../contexts/ApiContext';
import type { ModelType } from '../../../enums/ModelType'; import { InvenTreeIcon } from '../../../functions/icons';
import {
InvenTreeIcon,
type InvenTreeIconType
} from '../../../functions/icons';
import { navigateToLink } from '../../../functions/navigation';
import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
import { StylishText } from '../../items/StylishText'; import { StylishText } from '../../items/StylishText';
import { ModelInformationDict } from '../../render/ModelType';
import type { DashboardWidgetProps } from '../DashboardWidget'; import type { DashboardWidgetProps } from '../DashboardWidget';
/** /**
@ -28,7 +26,7 @@ function QueryCountWidget({
}: Readonly<{ }: Readonly<{
modelType: ModelType; modelType: ModelType;
title: string; title: string;
icon?: InvenTreeIconType; icon?: keyof InvenTreeIconType;
params: any; params: any;
}>): ReactNode { }>): ReactNode {
const api = useApi(); const api = useApi();

View File

@ -17,14 +17,15 @@ import { getValueAtPath } from 'mantine-datatable';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { ModelType } from '@lib/enums/ModelType';
import { apiUrl } from '@lib/functions/Api';
import { getDetailUrl } from '@lib/functions/Navigation';
import { navigateToLink } from '@lib/functions/Navigation';
import type { InvenTreeIconType } from '@lib/types/Icons';
import { useApi } from '../../contexts/ApiContext'; import { useApi } from '../../contexts/ApiContext';
import { formatDate } from '../../defaults/formatters'; import { formatDate } from '../../defaults/formatters';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { InvenTreeIcon } from '../../functions/icons';
import { ModelType } from '../../enums/ModelType';
import { InvenTreeIcon, type InvenTreeIconType } from '../../functions/icons';
import { navigateToLink } from '../../functions/navigation';
import { getDetailUrl } from '../../functions/urls';
import { apiUrl } from '../../states/ApiState';
import { useGlobalSettingsState } from '../../states/SettingsState'; import { useGlobalSettingsState } from '../../states/SettingsState';
import { CopyButton } from '../buttons/CopyButton'; import { CopyButton } from '../buttons/CopyButton';
import { YesNoButton } from '../buttons/YesNoButton'; import { YesNoButton } from '../buttons/YesNoButton';
@ -35,7 +36,7 @@ import { StatusRenderer } from '../render/StatusRenderer';
export type DetailsField = { export type DetailsField = {
hidden?: boolean; hidden?: boolean;
icon?: InvenTreeIconType; icon?: keyof InvenTreeIconType;
name: string; name: string;
label?: string; label?: string;
badge?: BadgeType; badge?: BadgeType;
@ -464,7 +465,7 @@ export function DetailsTableField({
<Table.Td style={{ minWidth: 75, lineBreak: 'auto', flex: 2 }}> <Table.Td style={{ minWidth: 75, lineBreak: 'auto', flex: 2 }}>
<Group gap='xs' wrap='nowrap'> <Group gap='xs' wrap='nowrap'>
<InvenTreeIcon <InvenTreeIcon
icon={field.icon ?? (field.name as InvenTreeIconType)} icon={field.icon ?? (field.name as keyof InvenTreeIconType)}
/> />
<Text style={{ paddingLeft: 10 }}>{field.label}</Text> <Text style={{ paddingLeft: 10 }}>{field.label}</Text>
</Group> </Group>

View File

@ -21,10 +21,10 @@ import { useHover } from '@mantine/hooks';
import { modals } from '@mantine/modals'; import { modals } from '@mantine/modals';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import type { UserRoles } from '@lib/enums/Roles';
import { cancelEvent } from '@lib/functions/Events';
import { showNotification } from '@mantine/notifications'; import { showNotification } from '@mantine/notifications';
import { api } from '../../App'; import { api } from '../../App';
import type { UserRoles } from '../../enums/Roles';
import { cancelEvent } from '../../functions/events';
import { InvenTreeIcon } from '../../functions/icons'; import { InvenTreeIcon } from '../../functions/icons';
import { showApiErrorMessage } from '../../functions/notifications'; import { showApiErrorMessage } from '../../functions/notifications';
import { useEditApiFormModal } from '../../hooks/UseForm'; import { useEditApiFormModal } from '../../hooks/UseForm';

View File

@ -7,11 +7,11 @@ import 'easymde/dist/easymde.min.css';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import SimpleMDE from 'react-simplemde-editor'; import SimpleMDE from 'react-simplemde-editor';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { ModelInformationDict } from '@lib/enums/ModelInformation';
import type { ModelType } from '@lib/enums/ModelType';
import { apiUrl } from '@lib/functions/Api';
import { useApi } from '../../contexts/ApiContext'; import { useApi } from '../../contexts/ApiContext';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import type { ModelType } from '../../enums/ModelType';
import { apiUrl } from '../../states/ApiState';
import { ModelInformationDict } from '../render/ModelType';
/* /*
* A text editor component for editing notes against a model type and instance. * A text editor component for editing notes against a model type and instance.

View File

@ -24,14 +24,14 @@ import Split from '@uiw/react-split';
import type React from 'react'; import type React from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ModelInformationDict } from '@lib/enums/ModelInformation';
import { ModelType } from '@lib/enums/ModelType';
import { apiUrl } from '@lib/functions/Api';
import { api } from '../../../App'; import { api } from '../../../App';
import { ModelType } from '../../../enums/ModelType';
import { apiUrl } from '../../../states/ApiState';
import type { TemplateI } from '../../../tables/settings/TemplateTable'; import type { TemplateI } from '../../../tables/settings/TemplateTable';
import { Boundary } from '../../Boundary'; import { Boundary } from '../../Boundary';
import { SplitButton } from '../../buttons/SplitButton'; import { SplitButton } from '../../buttons/SplitButton';
import { StandaloneField } from '../../forms/StandaloneField'; import { StandaloneField } from '../../forms/StandaloneField';
import { ModelInformationDict } from '../../render/ModelType';
type EditorProps = { type EditorProps = {
template: TemplateI; template: TemplateI;

View File

@ -2,7 +2,6 @@ import { t } from '@lingui/core/macro';
import { import {
Alert, Alert,
Button, Button,
type DefaultMantineColor,
Divider, Divider,
Group, Group,
LoadingOverlay, LoadingOverlay,
@ -19,14 +18,17 @@ import {
FormProvider, FormProvider,
type SubmitErrorHandler, type SubmitErrorHandler,
type SubmitHandler, type SubmitHandler,
type UseFormReturn,
useForm useForm
} from 'react-hook-form'; } from 'react-hook-form';
import { useNavigate } from 'react-router-dom'; import { type NavigateFunction, useNavigate } from 'react-router-dom';
import { getDetailUrl } from '@lib/functions/Navigation';
import type {
ApiFormFieldSet,
ApiFormFieldType,
ApiFormProps
} from '@lib/types/Forms';
import { useApi } from '../../contexts/ApiContext'; import { useApi } from '../../contexts/ApiContext';
import type { ApiEndpoints } from '../../enums/ApiEndpoints';
import type { ModelType } from '../../enums/ModelType';
import { import {
type NestedDict, type NestedDict,
constructField, constructField,
@ -38,74 +40,8 @@ import {
invalidResponse, invalidResponse,
showTimeoutNotification showTimeoutNotification
} from '../../functions/notifications'; } from '../../functions/notifications';
import { getDetailUrl } from '../../functions/urls';
import type { TableState } from '../../hooks/UseTable';
import type { PathParams } from '../../states/ApiState';
import { Boundary } from '../Boundary'; import { Boundary } from '../Boundary';
import { import { ApiFormField } from './fields/ApiFormField';
ApiFormField,
type ApiFormFieldSet,
type ApiFormFieldType
} from './fields/ApiFormField';
export interface ApiFormAction {
text: string;
variant?: 'outline';
color?: DefaultMantineColor;
onClick: () => void;
}
/**
* Properties for the ApiForm component
* @param url : The API endpoint to fetch the form data from
* @param pk : Optional primary-key value when editing an existing object
* @param pk_field : Optional primary-key field name (default: pk)
* @param pathParams : Optional path params for the url
* @param method : Optional HTTP method to use when submitting the form (default: GET)
* @param fields : The fields to render in the form
* @param submitText : Optional custom text to display on the submit button (default: Submit)4
* @param submitColor : Optional custom color for the submit button (default: green)
* @param fetchInitialData : Optional flag to fetch initial data from the server (default: true)
* @param preFormContent : Optional content to render before the form fields
* @param postFormContent : Optional content to render after the form fields
* @param successMessage : Optional message to display on successful form submission
* @param onFormSuccess : A callback function to call when the form is submitted successfully.
* @param onFormError : A callback function to call when the form is submitted with errors.
* @param processFormData : A callback function to process the form data before submission
* @param checkClose: A callback function to check if the form can be closed after submission
* @param modelType : Define a model type for this form
* @param follow : Boolean, follow the result of the form (if possible)
* @param table : Table to update on success (if provided)
*/
export interface ApiFormProps {
url: ApiEndpoints | string;
pk?: number | string;
pk_field?: string;
pathParams?: PathParams;
queryParams?: URLSearchParams;
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
fields?: ApiFormFieldSet;
focus?: string;
initialData?: FieldValues;
submitText?: string;
submitColor?: string;
fetchInitialData?: boolean;
ignorePermissionCheck?: boolean;
preFormContent?: JSX.Element;
preFormWarning?: string;
preFormSuccess?: string;
postFormContent?: JSX.Element;
successMessage?: string | null;
onFormSuccess?: (data: any, form: UseFormReturn) => void;
onFormError?: (response: any, form: UseFormReturn) => void;
processFormData?: (data: any, form: UseFormReturn) => any;
checkClose?: (data: any, form: UseFormReturn) => boolean;
table?: TableState;
modelType?: ModelType;
follow?: boolean;
actions?: ApiFormAction[];
timeout?: number;
}
export function OptionsApiForm({ export function OptionsApiForm({
props: _props, props: _props,
@ -219,7 +155,16 @@ export function ApiForm({
}>) { }>) {
const api = useApi(); const api = useApi();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const navigate = useNavigate();
// Accessor for the navigation function (which is used to redirect the user)
let navigate: NavigateFunction | null = null;
try {
navigate = useNavigate();
} catch (_error) {
// Note: If we launch a form within a plugin context, useNavigate() may not be available
navigate = null;
}
const [fields, setFields] = useState<ApiFormFieldSet>( const [fields, setFields] = useState<ApiFormFieldSet>(
() => props.fields ?? {} () => props.fields ?? {}
@ -482,7 +427,9 @@ export function ApiForm({
if (props.follow && props.modelType && response.data?.pk) { if (props.follow && props.modelType && response.data?.pk) {
// If we want to automatically follow the returned data // If we want to automatically follow the returned data
navigate(getDetailUrl(props.modelType, response.data?.pk)); if (!!navigate) {
navigate(getDetailUrl(props.modelType, response.data?.pk));
}
} else if (props.table) { } else if (props.table) {
// If we want to automatically update or reload a linked table // If we want to automatically update or reload a linked table
const pk_field = props.pk_field ?? 'pk'; const pk_field = props.pk_field ?? 'pk';

View File

@ -15,9 +15,10 @@ import { useDisclosure } from '@mantine/hooks';
import { useState } from 'react'; import { useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api';
import { showNotification } from '@mantine/notifications'; import { showNotification } from '@mantine/notifications';
import { api } from '../../App'; import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { import {
doBasicLogin, doBasicLogin,
doSimpleLogin, doSimpleLogin,
@ -25,7 +26,7 @@ import {
followRedirect followRedirect
} from '../../functions/auth'; } from '../../functions/auth';
import { showLoginNotification } from '../../functions/notifications'; import { showLoginNotification } from '../../functions/notifications';
import { apiUrl, useServerApiState } from '../../states/ApiState'; import { useServerApiState } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import { SsoButton } from '../buttons/SSOButton'; import { SsoButton } from '../buttons/SSOButton';

View File

@ -1,3 +1,4 @@
import type { HostList } from '@lib/types/Server';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { import {
@ -13,8 +14,6 @@ import { useForm } from '@mantine/form';
import { randomId } from '@mantine/hooks'; import { randomId } from '@mantine/hooks';
import { IconSquarePlus, IconTrash } from '@tabler/icons-react'; import { IconSquarePlus, IconTrash } from '@tabler/icons-react';
import type { HostList } from '../../states/states';
export function HostOptionsForm({ export function HostOptionsForm({
data, data,
saveOptions saveOptions

View File

@ -20,10 +20,10 @@ import {
IconServerSpark IconServerSpark
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import type { HostList } from '@lib/types/Server';
import { Wrapper } from '../../pages/Auth/Layout'; import { Wrapper } from '../../pages/Auth/Layout';
import { useServerApiState } from '../../states/ApiState'; import { useServerApiState } from '../../states/ApiState';
import { useLocalState } from '../../states/LocalState'; import { useLocalState } from '../../states/LocalState';
import type { HostList } from '../../states/states';
import { ActionButton } from '../buttons/ActionButton'; import { ActionButton } from '../buttons/ActionButton';
import { HostOptionsForm } from './HostOptionsForm'; import { HostOptionsForm } from './HostOptionsForm';

View File

@ -1,7 +1,8 @@
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { ApiFormField, type ApiFormFieldType } from './fields/ApiFormField'; import type { ApiFormFieldType } from '@lib/types/Forms';
import { ApiFormField } from './fields/ApiFormField';
export function StandaloneField({ export function StandaloneField({
fieldDefinition, fieldDefinition,

View File

@ -1,18 +1,10 @@
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { import { Alert, FileInput, NumberInput, Stack, Switch } from '@mantine/core';
Alert,
FileInput,
type MantineStyleProp,
NumberInput,
Stack,
Switch
} from '@mantine/core';
import type { UseFormReturnType } from '@mantine/form';
import { useId } from '@mantine/hooks'; import { useId } from '@mantine/hooks';
import { type ReactNode, useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { type Control, type FieldValues, useController } from 'react-hook-form'; import { type Control, type FieldValues, useController } from 'react-hook-form';
import type { ModelType } from '../../../enums/ModelType'; import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms';
import { isTrue } from '../../../functions/conversion'; import { isTrue } from '../../../functions/conversion';
import { ChoiceField } from './ChoiceField'; import { ChoiceField } from './ChoiceField';
import DateField from './DateField'; import DateField from './DateField';
@ -23,103 +15,6 @@ import { RelatedModelField } from './RelatedModelField';
import { TableField } from './TableField'; import { TableField } from './TableField';
import TextField from './TextField'; import TextField from './TextField';
export type ApiFormData = UseFormReturnType<Record<string, unknown>>;
export type ApiFormAdjustFilterType = {
filters: any;
data: FieldValues;
};
export type ApiFormFieldChoice = {
value: any;
display_name: string;
};
// Define individual headers in a table field
export type ApiFormFieldHeader = {
title: string;
style?: MantineStyleProp;
};
/** Definition of the ApiForm field component.
* - The 'name' attribute *must* be provided
* - All other attributes are optional, and may be provided by the API
* - However, they can be overridden by the user
*
* @param name : The name of the field
* @param label : The label to display for the field
* @param value : The value of the field
* @param default : The default value of the field
* @param icon : An icon to display next to the field
* @param field_type : The type of field to render
* @param api_url : The API endpoint to fetch data from (for related fields)
* @param pk_field : The primary key field for the related field (default = "pk")
* @param model : The model to use for related fields
* @param filters : Optional API filters to apply to related fields
* @param required : Whether the field is required
* @param hidden : Whether the field is hidden
* @param disabled : Whether the field is disabled
* @param error : Optional error message to display
* @param exclude : Whether to exclude the field from the submitted data
* @param placeholder : The placeholder text to display
* @param description : The description to display for the field
* @param preFieldContent : Content to render before the field
* @param postFieldContent : Content to render after the field
* @param onValueChange : Callback function to call when the field value changes
* @param adjustFilters : Callback function to adjust the filters for a related field before a query is made
* @param adjustValue : Callback function to adjust the value of the field before it is sent to the API
* @param addRow : Callback function to add a new row to a table field
* @param onKeyDown : Callback function to get which key was pressed in the form to handle submission on enter
*/
export type ApiFormFieldType = {
label?: string;
value?: any;
default?: any;
icon?: ReactNode;
field_type?:
| 'related field'
| 'email'
| 'url'
| 'string'
| 'icon'
| 'boolean'
| 'date'
| 'datetime'
| 'integer'
| 'decimal'
| 'float'
| 'number'
| 'choice'
| 'file upload'
| 'nested object'
| 'dependent field'
| 'table';
api_url?: string;
pk_field?: string;
model?: ModelType;
modelRenderer?: (instance: any) => ReactNode;
filters?: any;
child?: ApiFormFieldType;
children?: { [key: string]: ApiFormFieldType };
required?: boolean;
error?: string;
choices?: ApiFormFieldChoice[];
hidden?: boolean;
disabled?: boolean;
exclude?: boolean;
read_only?: boolean;
placeholder?: string;
description?: string;
preFieldContent?: JSX.Element;
postFieldContent?: JSX.Element;
adjustValue?: (value: any) => any;
onValueChange?: (value: any, record?: any) => void;
adjustFilters?: (value: ApiFormAdjustFilterType) => any;
addRow?: () => any;
headers?: ApiFormFieldHeader[];
depends_on?: string[];
};
/** /**
* Render an individual form field * Render an individual form field
*/ */
@ -384,5 +279,3 @@ export function ApiFormField({
</Stack> </Stack>
); );
} }
export type ApiFormFieldSet = Record<string, ApiFormFieldType>;

View File

@ -1,10 +1,9 @@
import type { ApiFormFieldType } from '@lib/types/Forms';
import { Select } from '@mantine/core'; import { Select } from '@mantine/core';
import { useId } from '@mantine/hooks'; import { useId } from '@mantine/hooks';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import type { FieldValues, UseControllerReturn } from 'react-hook-form'; import type { FieldValues, UseControllerReturn } from 'react-hook-form';
import type { ApiFormFieldType } from './ApiFormField';
/** /**
* Render a 'select' field for selecting from a list of choices * Render a 'select' field for selecting from a list of choices
*/ */

View File

@ -1,11 +1,10 @@
import type { ApiFormFieldType } from '@lib/types/Forms';
import { DateInput } from '@mantine/dates'; import { DateInput } from '@mantine/dates';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat'; import customParseFormat from 'dayjs/plugin/customParseFormat';
import { useCallback, useId, useMemo } from 'react'; import { useCallback, useId, useMemo } from 'react';
import type { FieldValues, UseControllerReturn } from 'react-hook-form'; import type { FieldValues, UseControllerReturn } from 'react-hook-form';
import type { ApiFormFieldType } from './ApiFormField';
dayjs.extend(customParseFormat); dayjs.extend(customParseFormat);
export default function DateField({ export default function DateField({

View File

@ -5,16 +5,13 @@ import {
useFormContext useFormContext
} from 'react-hook-form'; } from 'react-hook-form';
import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms';
import { useApi } from '../../../contexts/ApiContext'; import { useApi } from '../../../contexts/ApiContext';
import { import {
constructField, constructField,
extractAvailableFields extractAvailableFields
} from '../../../functions/forms'; } from '../../../functions/forms';
import { import { ApiFormField } from './ApiFormField';
ApiFormField,
type ApiFormFieldSet,
type ApiFormFieldType
} from './ApiFormField';
export function DependentField({ export function DependentField({
control, control,

View File

@ -21,9 +21,9 @@ import { startTransition, useEffect, useMemo, useRef, useState } from 'react';
import type { FieldValues, UseControllerReturn } from 'react-hook-form'; import type { FieldValues, UseControllerReturn } from 'react-hook-form';
import { FixedSizeGrid as Grid } from 'react-window'; import { FixedSizeGrid as Grid } from 'react-window';
import type { ApiFormFieldType } from '@lib/types/Forms';
import { useIconState } from '../../../states/IconState'; import { useIconState } from '../../../states/IconState';
import { ApiIcon } from '../../items/ApiIcon'; import { ApiIcon } from '../../items/ApiIcon';
import type { ApiFormFieldType } from './ApiFormField';
export default function IconField({ export default function IconField({
controller, controller,

View File

@ -1,11 +1,7 @@
import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms';
import { Accordion, Divider, Stack, Text } from '@mantine/core'; import { Accordion, Divider, Stack, Text } from '@mantine/core';
import type { Control, FieldValues } from 'react-hook-form'; import type { Control, FieldValues } from 'react-hook-form';
import { ApiFormField } from './ApiFormField';
import {
ApiFormField,
type ApiFormFieldSet,
type ApiFormFieldType
} from './ApiFormField';
export function NestedObjectField({ export function NestedObjectField({
control, control,

View File

@ -15,10 +15,10 @@ import {
} from 'react-hook-form'; } from 'react-hook-form';
import Select from 'react-select'; import Select from 'react-select';
import type { ApiFormFieldType } from '@lib/types/Forms';
import { useApi } from '../../../contexts/ApiContext'; import { useApi } from '../../../contexts/ApiContext';
import { vars } from '../../../theme'; import { vars } from '../../../theme';
import { RenderInstance } from '../../render/Instance'; import { RenderInstance } from '../../render/Instance';
import type { ApiFormFieldType } from './ApiFormField';
/** /**
* Render a 'select' field for searching the database against a particular model type * Render a 'select' field for searching the database against a particular model type

View File

@ -5,11 +5,11 @@ import { IconExclamationCircle } from '@tabler/icons-react';
import { type ReactNode, useCallback, useEffect, useMemo } from 'react'; import { type ReactNode, useCallback, useEffect, useMemo } from 'react';
import type { FieldValues, UseControllerReturn } from 'react-hook-form'; import type { FieldValues, UseControllerReturn } from 'react-hook-form';
import type { ApiFormFieldType } from '@lib/types/Forms';
import { identifierString } from '../../../functions/conversion'; import { identifierString } from '../../../functions/conversion';
import { InvenTreeIcon } from '../../../functions/icons'; import { InvenTreeIcon } from '../../../functions/icons';
import { AddItemButton } from '../../buttons/AddItemButton'; import { AddItemButton } from '../../buttons/AddItemButton';
import { StandaloneField } from '../StandaloneField'; import { StandaloneField } from '../StandaloneField';
import type { ApiFormFieldType } from './ApiFormField';
export interface TableFieldRowProps { export interface TableFieldRowProps {
item: any; item: any;

View File

@ -9,18 +9,19 @@ import {
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { type ReactNode, useCallback, useMemo, useState } from 'react'; import { type ReactNode, useCallback, useMemo, useState } from 'react';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api';
import { cancelEvent } from '@lib/functions/Events';
import type { TableFilter } from '@lib/types/Filters';
import type { ApiFormFieldSet } from '@lib/types/Forms';
import { useApi } from '../../contexts/ApiContext'; import { useApi } from '../../contexts/ApiContext';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { cancelEvent } from '../../functions/events';
import { import {
useDeleteApiFormModal, useDeleteApiFormModal,
useEditApiFormModal useEditApiFormModal
} from '../../hooks/UseForm'; } from '../../hooks/UseForm';
import type { ImportSessionState } from '../../hooks/UseImportSession'; import type { ImportSessionState } from '../../hooks/UseImportSession';
import { useTable } from '../../hooks/UseTable'; import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import type { TableColumn } from '../../tables/Column'; import type { TableColumn } from '../../tables/Column';
import type { TableFilter } from '../../tables/Filter';
import { InvenTreeTable } from '../../tables/InvenTreeTable'; import { InvenTreeTable } from '../../tables/InvenTreeTable';
import { import {
type RowAction, type RowAction,
@ -29,7 +30,6 @@ import {
} from '../../tables/RowActions'; } from '../../tables/RowActions';
import { ActionButton } from '../buttons/ActionButton'; import { ActionButton } from '../buttons/ActionButton';
import { YesNoButton } from '../buttons/YesNoButton'; import { YesNoButton } from '../buttons/YesNoButton';
import type { ApiFormFieldSet } from '../forms/fields/ApiFormField';
import { ProgressBar } from '../items/ProgressBar'; import { ProgressBar } from '../items/ProgressBar';
import { RenderRemoteInstance } from '../render/Instance'; import { RenderRemoteInstance } from '../render/Instance';

View File

@ -13,12 +13,12 @@ import {
import { IconCheck } from '@tabler/icons-react'; import { IconCheck } from '@tabler/icons-react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api';
import type { ApiFormFieldType } from '@lib/types/Forms';
import { useApi } from '../../contexts/ApiContext'; import { useApi } from '../../contexts/ApiContext';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import type { ImportSessionState } from '../../hooks/UseImportSession'; import type { ImportSessionState } from '../../hooks/UseImportSession';
import { apiUrl } from '../../states/ApiState';
import { StandaloneField } from '../forms/StandaloneField'; import { StandaloneField } from '../forms/StandaloneField';
import type { ApiFormFieldType } from '../forms/fields/ApiFormField';
function ImporterColumn({ function ImporterColumn({
column, column,

View File

@ -16,7 +16,7 @@ import {
import { IconCheck } from '@tabler/icons-react'; import { IconCheck } from '@tabler/icons-react';
import { type ReactNode, useMemo } from 'react'; import { type ReactNode, useMemo } from 'react';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '@lib/enums/ModelType';
import { useImportSession } from '../../hooks/UseImportSession'; import { useImportSession } from '../../hooks/UseImportSession';
import useStatusCodes from '../../hooks/UseStatusCodes'; import useStatusCodes from '../../hooks/UseStatusCodes';
import { StylishText } from '../items/StylishText'; import { StylishText } from '../items/StylishText';

View File

@ -3,7 +3,7 @@ import { Center, Container, Loader, Stack, Text } from '@mantine/core';
import { useInterval } from '@mantine/hooks'; import { useInterval } from '@mantine/hooks';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '@lib/enums/ModelType';
import type { ImportSessionState } from '../../hooks/UseImportSession'; import type { ImportSessionState } from '../../hooks/UseImportSession';
import useStatusCodes from '../../hooks/UseStatusCodes'; import useStatusCodes from '../../hooks/UseStatusCodes';
import { StylishText } from '../items/StylishText'; import { StylishText } from '../items/StylishText';

View File

@ -20,7 +20,7 @@ import {
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { type ReactNode, useMemo } from 'react'; import { type ReactNode, useMemo } from 'react';
import type { ModelType } from '../../enums/ModelType'; import type { ModelType } from '@lib/enums/ModelType';
import { identifierString } from '../../functions/conversion'; import { identifierString } from '../../functions/conversion';
import { InvenTreeIcon } from '../../functions/icons'; import { InvenTreeIcon } from '../../functions/icons';
import { InvenTreeQRCode, QRCodeLink, QRCodeUnlink } from '../barcodes/QRCode'; import { InvenTreeQRCode, QRCodeLink, QRCodeUnlink } from '../barcodes/QRCode';

View File

@ -11,15 +11,16 @@ import {
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { InvenTreeIcon, type InvenTreeIconType } from '../../functions/icons'; import { navigateToLink } from '@lib/functions/Navigation';
import { navigateToLink } from '../../functions/navigation'; import type { InvenTreeIconType } from '@lib/types/Icons';
import { InvenTreeIcon } from '../../functions/icons';
import { StylishText } from './StylishText'; import { StylishText } from './StylishText';
export interface MenuLinkItem { export interface MenuLinkItem {
id: string; id: string;
title: string | JSX.Element; title: string | JSX.Element;
description?: string; description?: string;
icon?: InvenTreeIconType; icon?: keyof InvenTreeIconType;
action?: () => void; action?: () => void;
link?: string; link?: string;
external?: boolean; external?: boolean;

View File

@ -1,3 +1,5 @@
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { import {
@ -13,8 +15,6 @@ import { notifications } from '@mantine/notifications';
import { IconCircleCheck, IconReload } from '@tabler/icons-react'; import { IconCircleCheck, IconReload } from '@tabler/icons-react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { api } from '../../App'; import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { apiUrl } from '../../states/ApiState';
export interface RuleSet { export interface RuleSet {
pk?: number; pk?: number;

View File

@ -3,33 +3,19 @@ import {
Text, Text,
darken, darken,
getThemeColor, getThemeColor,
lighten,
useMantineColorScheme,
useMantineTheme useMantineTheme
} from '@mantine/core'; } from '@mantine/core';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useLocalState } from '../../states/LocalState';
// Hook that memoizes the gradient color based on the primary color of the theme // Hook that memoizes the gradient color based on the primary color of the theme
const useThematicGradient = () => { const useThematicGradient = () => {
const { usertheme } = useLocalState();
const theme = useMantineTheme(); const theme = useMantineTheme();
const colorScheme = useMantineColorScheme();
const primary = useMemo(() => { const primary = useMemo(() => {
return getThemeColor(usertheme.primaryColor, theme); return getThemeColor(theme.primaryColor, theme);
}, [usertheme.primaryColor, theme]); }, [theme]);
const secondary = useMemo(() => { const secondary = useMemo(() => darken(primary, 0.25), [primary]);
let secondary = primary;
if (colorScheme.colorScheme == 'dark') {
secondary = lighten(primary, 0.3);
} else {
secondary = darken(primary, 0.3);
}
return secondary;
}, [usertheme, colorScheme, primary]);
return useMemo(() => { return useMemo(() => {
return { primary, secondary }; return { primary, secondary };

View File

@ -14,10 +14,11 @@ import {
import type { ContextModalProps } from '@mantine/modals'; import type { ContextModalProps } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api';
import { api } from '../../App'; import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { generateUrl } from '../../functions/urls'; import { generateUrl } from '../../functions/urls';
import { apiUrl, useServerApiState } from '../../states/ApiState'; import { useServerApiState } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import { CopyButton } from '../buttons/CopyButton'; import { CopyButton } from '../buttons/CopyButton';
import { StylishText } from '../items/StylishText'; import { StylishText } from '../items/StylishText';

View File

@ -14,9 +14,9 @@ import {
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api';
import { api } from '../../App'; import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { apiUrl } from '../../states/ApiState';
export function LicenceView(entries: Readonly<any[]>) { export function LicenceView(entries: Readonly<any[]>) {
return ( return (

View File

@ -10,8 +10,8 @@ import { IconMenu2 } from '@tabler/icons-react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { navigateToLink } from '@lib/functions/Navigation';
import { identifierString } from '../../functions/conversion'; import { identifierString } from '../../functions/conversion';
import { navigateToLink } from '../../functions/navigation';
export type Breadcrumb = { export type Breadcrumb = {
icon?: React.ReactNode; icon?: React.ReactNode;

View File

@ -4,7 +4,7 @@ import { useCallback, useMemo } from 'react';
import { Link, Route, Routes, useNavigate, useParams } from 'react-router-dom'; import { Link, Route, Routes, useNavigate, useParams } from 'react-router-dom';
import type { To } from 'react-router-dom'; import type { To } from 'react-router-dom';
import type { UiSizeType } from '../../defaults/formatters'; import type { UiSizeType } from '@lib/types/Core';
import { useLocalState } from '../../states/LocalState'; import { useLocalState } from '../../states/LocalState';
import { StylishText } from '../items/StylishText'; import { StylishText } from '../items/StylishText';
import * as classes from './DetailDrawer.css'; import * as classes from './DetailDrawer.css';

View File

@ -13,13 +13,14 @@ import { useQuery } from '@tanstack/react-query';
import { type ReactNode, useEffect, useMemo, useState } from 'react'; import { type ReactNode, useEffect, useMemo, useState } from 'react';
import { useMatch, useNavigate } from 'react-router-dom'; import { useMatch, useNavigate } from 'react-router-dom';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api';
import { navigateToLink } from '@lib/functions/Navigation';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { api } from '../../App'; import { api } from '../../App';
import { getNavTabs } from '../../defaults/links'; import { getNavTabs } from '../../defaults/links';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { navigateToLink } from '../../functions/navigation';
import * as classes from '../../main.css'; import * as classes from '../../main.css';
import { apiUrl, useServerApiState } from '../../states/ApiState'; import { useServerApiState } from '../../states/ApiState';
import { useLocalState } from '../../states/LocalState'; import { useLocalState } from '../../states/LocalState';
import { import {
useGlobalSettingsState, useGlobalSettingsState,

View File

@ -1,7 +1,7 @@
import { LoadingOverlay } from '@mantine/core'; import { LoadingOverlay } from '@mantine/core';
import type { ModelType } from '../../enums/ModelType'; import type { ModelType } from '@lib/enums/ModelType';
import type { UserRoles } from '../../enums/Roles'; import type { UserRoles } from '@lib/enums/Roles';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import ClientError from '../errors/ClientError'; import ClientError from '../errors/ClientError';
import PermissionDenied from '../errors/PermissionDenied'; import PermissionDenied from '../errors/PermissionDenied';

View File

@ -10,9 +10,9 @@ import {
import { useViewportSize } from '@mantine/hooks'; import { useViewportSize } from '@mantine/hooks';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { ModelType } from '@lib/enums/ModelType';
import { UserRoles } from '@lib/enums/Roles';
import { AboutLinks, DocumentationLinks } from '../../defaults/links'; import { AboutLinks, DocumentationLinks } from '../../defaults/links';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import useInstanceName from '../../hooks/UseInstanceName'; import useInstanceName from '../../hooks/UseInstanceName';
import * as classes from '../../main.css'; import * as classes from '../../main.css';
import { useGlobalSettingsState } from '../../states/SettingsState'; import { useGlobalSettingsState } from '../../states/SettingsState';

View File

@ -21,12 +21,12 @@ import { useQuery } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import type { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import type { ModelType } from '@lib/enums/ModelType';
import { apiUrl } from '@lib/functions/Api';
import { getDetailUrl } from '@lib/functions/Navigation';
import { navigateToLink } from '@lib/functions/Navigation';
import { useApi } from '../../contexts/ApiContext'; import { useApi } from '../../contexts/ApiContext';
import type { ApiEndpoints } from '../../enums/ApiEndpoints';
import type { ModelType } from '../../enums/ModelType';
import { navigateToLink } from '../../functions/navigation';
import { getDetailUrl } from '../../functions/urls';
import { apiUrl } from '../../states/ApiState';
import { ApiIcon } from '../items/ApiIcon'; import { ApiIcon } from '../items/ApiIcon';
import { StylishText } from '../items/StylishText'; import { StylishText } from '../items/StylishText';

View File

@ -18,17 +18,17 @@ import { useQuery } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { ModelInformationDict } from '@lib/enums/ModelInformation';
import type { ModelType } from '@lib/enums/ModelType';
import { apiUrl } from '@lib/functions/Api';
import { getDetailUrl } from '@lib/functions/Navigation';
import { getBaseUrl } from '@lib/functions/Navigation';
import { navigateToLink } from '@lib/functions/Navigation';
import { api } from '../../App'; import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import type { ModelType } from '../../enums/ModelType';
import { navigateToLink } from '../../functions/navigation';
import { getDetailUrl } from '../../functions/urls';
import { getBaseUrl } from '../../main';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import { Boundary } from '../Boundary'; import { Boundary } from '../Boundary';
import { StylishText } from '../items/StylishText'; import { StylishText } from '../items/StylishText';
import { ModelInformationDict } from '../render/ModelType';
/** /**
* Render a single notification entry in the drawer * Render a single notification entry in the drawer

View File

@ -32,19 +32,20 @@ import { useQuery } from '@tanstack/react-query';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { type NavigateFunction, useNavigate } from 'react-router-dom'; import { type NavigateFunction, useNavigate } from 'react-router-dom';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { ModelInformationDict } from '@lib/enums/ModelInformation';
import { ModelType } from '@lib/enums/ModelType';
import { UserRoles } from '@lib/enums/Roles';
import { apiUrl } from '@lib/functions/Api';
import { cancelEvent } from '@lib/functions/Events';
import { navigateToLink } from '@lib/functions/Navigation';
import { showNotification } from '@mantine/notifications'; import { showNotification } from '@mantine/notifications';
import { api } from '../../App'; import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { cancelEvent } from '../../functions/events';
import { navigateToLink } from '../../functions/navigation';
import { apiUrl } from '../../states/ApiState';
import { useUserSettingsState } from '../../states/SettingsState'; import { useUserSettingsState } from '../../states/SettingsState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import { Boundary } from '../Boundary'; import { Boundary } from '../Boundary';
import { RenderInstance } from '../render/Instance'; import { RenderInstance } from '../render/Instance';
import { ModelInformationDict, getModelInfo } from '../render/ModelType'; import { getModelInfo } from '../render/ModelType';
// Define type for handling individual search queries // Define type for handling individual search queries
type SearchQuery = { type SearchQuery = {

View File

@ -2,7 +2,7 @@ import { t } from '@lingui/core/macro';
import { Skeleton } from '@mantine/core'; import { Skeleton } from '@mantine/core';
import { IconPaperclip } from '@tabler/icons-react'; import { IconPaperclip } from '@tabler/icons-react';
import type { ModelType } from '../../enums/ModelType'; import type { ModelType } from '@lib/enums/ModelType';
import { AttachmentTable } from '../../tables/general/AttachmentTable'; import { AttachmentTable } from '../../tables/general/AttachmentTable';
import type { PanelType } from './Panel'; import type { PanelType } from './Panel';

View File

@ -2,7 +2,7 @@ import { t } from '@lingui/core/macro';
import { Skeleton } from '@mantine/core'; import { Skeleton } from '@mantine/core';
import { IconNotes } from '@tabler/icons-react'; import { IconNotes } from '@tabler/icons-react';
import type { ModelType } from '../../enums/ModelType'; import type { ModelType } from '@lib/enums/ModelType';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import NotesEditor from '../editors/NotesEditor'; import NotesEditor from '../editors/NotesEditor';
import type { PanelType } from './Panel'; import type { PanelType } from './Panel';

View File

@ -28,10 +28,10 @@ import {
useParams useParams
} from 'react-router-dom'; } from 'react-router-dom';
import type { ModelType } from '../../enums/ModelType'; import type { ModelType } from '@lib/enums/ModelType';
import { cancelEvent } from '@lib/functions/Events';
import { navigateToLink } from '@lib/functions/Navigation';
import { identifierString } from '../../functions/conversion'; import { identifierString } from '../../functions/conversion';
import { cancelEvent } from '../../functions/events';
import { navigateToLink } from '../../functions/navigation';
import { usePluginPanels } from '../../hooks/UsePluginPanels'; import { usePluginPanels } from '../../hooks/UsePluginPanels';
import { useLocalState } from '../../states/LocalState'; import { useLocalState } from '../../states/LocalState';
import { Boundary } from '../Boundary'; import { Boundary } from '../Boundary';
@ -47,6 +47,7 @@ import * as classes from './PanelGroup.css';
* @param model - The target model for this panel group (e.g. 'part' / 'salesorder') * @param model - The target model for this panel group (e.g. 'part' / 'salesorder')
* @param id - The target ID for this panel group (set to *null* for groups which do not target a specific model instance) * @param id - The target ID for this panel group (set to *null* for groups which do not target a specific model instance)
* @param instance - The target model instance for this panel group * @param instance - The target model instance for this panel group
* @param reloadInstance - Function to reload the model instance
* @param selectedPanel - The currently selected panel * @param selectedPanel - The currently selected panel
* @param onPanelChange - Callback when the active panel changes * @param onPanelChange - Callback when the active panel changes
* @param collapsible - If true, the panel group can be collapsed (defaults to true) * @param collapsible - If true, the panel group can be collapsed (defaults to true)
@ -55,6 +56,7 @@ export type PanelProps = {
pageKey: string; pageKey: string;
panels: PanelType[]; panels: PanelType[];
instance?: any; instance?: any;
reloadInstance?: () => void;
model?: ModelType | string; model?: ModelType | string;
id?: number | null; id?: number | null;
selectedPanel?: string; selectedPanel?: string;
@ -67,6 +69,7 @@ function BasePanelGroup({
panels, panels,
onPanelChange, onPanelChange,
selectedPanel, selectedPanel,
reloadInstance,
instance, instance,
model, model,
id, id,
@ -82,9 +85,10 @@ function BasePanelGroup({
// Hook to load plugins for this panel // Hook to load plugins for this panel
const pluginPanelSet = usePluginPanels({ const pluginPanelSet = usePluginPanels({
id: id,
model: model, model: model,
instance: instance, instance: instance,
id: id reloadFunc: reloadInstance
}); });
// Rebuild the list of panels // Rebuild the list of panels

View File

@ -1,12 +1,12 @@
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api';
import type { ApiFormFieldSet } from '@lib/types/Forms';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { IconRadar } from '@tabler/icons-react'; import { IconRadar } from '@tabler/icons-react';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useCreateApiFormModal } from '../../hooks/UseForm';
import { usePluginsWithMixin } from '../../hooks/UsePlugins'; import { usePluginsWithMixin } from '../../hooks/UsePlugins';
import { apiUrl } from '../../states/ApiState';
import { ActionButton } from '../buttons/ActionButton'; import { ActionButton } from '../buttons/ActionButton';
import type { ApiFormFieldSet } from '../forms/fields/ApiFormField';
import type { PluginInterface } from './PluginInterface'; import type { PluginInterface } from './PluginInterface';
export default function LocateItemButton({ export default function LocateItemButton({

View File

@ -1,51 +1,27 @@
import { import { useMantineColorScheme, useMantineTheme } from '@mantine/core';
type MantineColorScheme,
type MantineTheme,
useMantineColorScheme,
useMantineTheme
} from '@mantine/core';
import type { AxiosInstance } from 'axios';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { type NavigateFunction, useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import type { QueryClient } from '@tanstack/react-query';
import { api, queryClient } from '../../App'; import { api, queryClient } from '../../App';
import { useLocalState } from '../../states/LocalState'; import { useLocalState } from '../../states/LocalState';
import { import {
type SettingsStateProps,
useGlobalSettingsState, useGlobalSettingsState,
useUserSettingsState useUserSettingsState
} from '../../states/SettingsState'; } from '../../states/SettingsState';
import { type UserStateProps, useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
/** import {
* A set of properties which are passed to a plugin, INVENTREE_MANTINE_VERSION,
* for rendering an element in the user interface. INVENTREE_PLUGIN_VERSION,
* INVENTREE_REACT_VERSION,
* @param api - The Axios API instance (see ../states/ApiState.tsx) type InvenTreePluginContext
* @param user - The current user instance (see ../states/UserState.tsx) } from '@lib/types/Plugins';
* @param userSettings - The current user settings (see ../states/SettingsState.tsx) import {
* @param globalSettings - The global settings (see ../states/SettingsState.tsx) useBulkEditApiFormModal,
* @param navigate - The navigation function (see react-router-dom) useCreateApiFormModal,
* @param theme - The current Mantine theme useDeleteApiFormModal,
* @param colorScheme - The current Mantine color scheme (e.g. 'light' / 'dark') useEditApiFormModal
* @param host - The current host URL } from '../../hooks/UseForm';
* @param locale - The current locale string (e.g. 'en' / 'de')
* @param context - Any additional context data which may be passed to the plugin
*/
export type InvenTreeContext = {
api: AxiosInstance;
queryClient: QueryClient;
user: UserStateProps;
userSettings: SettingsStateProps;
globalSettings: SettingsStateProps;
host: string;
locale: string;
navigate: NavigateFunction;
theme: MantineTheme;
colorScheme: MantineColorScheme;
context?: any;
};
export const useInvenTreeContext = () => { export const useInvenTreeContext = () => {
const [locale, host] = useLocalState((s) => [s.language, s.host]); const [locale, host] = useLocalState((s) => [s.language, s.host]);
@ -56,8 +32,14 @@ export const useInvenTreeContext = () => {
const globalSettings = useGlobalSettingsState(); const globalSettings = useGlobalSettingsState();
const userSettings = useUserSettingsState(); const userSettings = useUserSettingsState();
const contextData = useMemo<InvenTreeContext>(() => { const contextData = useMemo<InvenTreePluginContext>(() => {
return { return {
version: {
inventree: INVENTREE_PLUGIN_VERSION,
react: INVENTREE_REACT_VERSION,
reactDom: INVENTREE_REACT_VERSION,
mantine: INVENTREE_MANTINE_VERSION
},
user: user, user: user,
host: host, host: host,
locale: locale, locale: locale,
@ -67,7 +49,13 @@ export const useInvenTreeContext = () => {
globalSettings: globalSettings, globalSettings: globalSettings,
userSettings: userSettings, userSettings: userSettings,
theme: theme, theme: theme,
colorScheme: colorScheme colorScheme: colorScheme,
forms: {
bulkEdit: useBulkEditApiFormModal,
create: useCreateApiFormModal,
delete: useDeleteApiFormModal,
edit: useEditApiFormModal
}
}; };
}, [ }, [
user, user,

View File

@ -4,7 +4,7 @@ import { IconExclamationCircle } from '@tabler/icons-react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { InfoItem } from '../items/InfoItem'; import { InfoItem } from '../items/InfoItem';
import { StylishText } from '../items/StylishText'; import { StylishText } from '../items/StylishText';

View File

@ -1,7 +1,7 @@
import { Stack } from '@mantine/core'; import { Stack } from '@mantine/core';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import type { InvenTreeContext } from './PluginContext'; import type { InvenTreePluginContext } from '@lib/types/Plugins';
import type { PluginUIFeature } from './PluginUIFeature'; import type { PluginUIFeature } from './PluginUIFeature';
import RemoteComponent from './RemoteComponent'; import RemoteComponent from './RemoteComponent';
@ -23,7 +23,7 @@ export default function PluginPanelContent({
pluginContext pluginContext
}: Readonly<{ }: Readonly<{
pluginFeature: PluginUIFeature; pluginFeature: PluginUIFeature;
pluginContext: InvenTreeContext; pluginContext: InvenTreePluginContext;
}>): ReactNode { }>): ReactNode {
return ( return (
<Stack gap='xs'> <Stack gap='xs'>

View File

@ -1,3 +1,4 @@
import type { InvenTreePluginContext } from '@lib/types/Plugins';
import { useInvenTreeContext } from './PluginContext'; import { useInvenTreeContext } from './PluginContext';
import RemoteComponent from './RemoteComponent'; import RemoteComponent from './RemoteComponent';
@ -21,13 +22,13 @@ export default function PluginSettingsPanel({
}: Readonly<{ }: Readonly<{
pluginAdmin: PluginAdminInterface; pluginAdmin: PluginAdminInterface;
}>) { }>) {
const pluginContext = useInvenTreeContext(); const ctx: InvenTreePluginContext = useInvenTreeContext();
return ( return (
<RemoteComponent <RemoteComponent
source={pluginAdmin.source} source={pluginAdmin.source}
defaultFunctionName='renderPluginSettings' defaultFunctionName='renderPluginSettings'
context={{ ...pluginContext, context: pluginAdmin.context }} context={{ ...ctx, context: pluginAdmin.context }}
/> />
); );
} }

View File

@ -1,8 +1,8 @@
import type { ModelType } from '../../enums/ModelType'; import type { ModelType } from '@lib/enums/ModelType';
import type { InvenTreeIconType } from '../../functions/icons'; import type { InvenTreeIconType } from '@lib/types/Icons';
import type { InvenTreePluginContext } from '@lib/types/Plugins';
import type { TemplateI } from '../../tables/settings/TemplateTable'; import type { TemplateI } from '../../tables/settings/TemplateTable';
import type { TemplateEditorProps } from '../editors/TemplateEditor/TemplateEditor'; import type { TemplateEditorProps } from '../editors/TemplateEditor/TemplateEditor';
import type { InvenTreeContext } from './PluginContext';
import type { PluginUIFeature } from './PluginUIFeature'; import type { PluginUIFeature } from './PluginUIFeature';
// #region Type Helpers // #region Type Helpers
@ -19,7 +19,7 @@ export type PluginUIGetFeatureType<
ServerContext extends Record<string, unknown> ServerContext extends Record<string, unknown>
> = (params: { > = (params: {
featureContext: T['featureContext']; featureContext: T['featureContext'];
inventreeContext: InvenTreeContext; inventreeContext: InvenTreePluginContext;
serverContext: ServerContext; serverContext: ServerContext;
}) => T['featureReturnType']; }) => T['featureReturnType'];

View File

@ -1,11 +1,15 @@
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { Alert, Stack, Text } from '@mantine/core'; import { Alert, MantineProvider, Stack, Text } from '@mantine/core';
import { IconExclamationCircle } from '@tabler/icons-react'; import { IconExclamationCircle } from '@tabler/icons-react';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { InvenTreePluginContext } from '@lib/types/Plugins';
import { type Root, createRoot } from 'react-dom/client';
import { api, queryClient } from '../../App';
import { ApiProvider } from '../../contexts/ApiContext';
import { LanguageContext } from '../../contexts/LanguageContext';
import { identifierString } from '../../functions/conversion'; import { identifierString } from '../../functions/conversion';
import { Boundary } from '../Boundary'; import { Boundary } from '../Boundary';
import type { InvenTreeContext } from './PluginContext';
import { findExternalPluginFunction } from './PluginSource'; import { findExternalPluginFunction } from './PluginSource';
/** /**
@ -24,9 +28,16 @@ export default function RemoteComponent({
}: Readonly<{ }: Readonly<{
source: string; source: string;
defaultFunctionName: string; defaultFunctionName: string;
context: InvenTreeContext; context: InvenTreePluginContext;
}>) { }>) {
const componentRef = useRef<HTMLDivElement>(); const componentRef = useRef<HTMLDivElement>();
const [rootElement, setRootElement] = useState<Root | null>(null);
useEffect(() => {
if (componentRef.current && !rootElement) {
setRootElement(createRoot(componentRef.current));
}
}, [componentRef.current]);
const [renderingError, setRenderingError] = useState<string | undefined>( const [renderingError, setRenderingError] = useState<string | undefined>(
undefined undefined
@ -47,20 +58,45 @@ export default function RemoteComponent({
return defaultFunctionName; return defaultFunctionName;
}, [source, defaultFunctionName]); }, [source, defaultFunctionName]);
const reloadPluginContent = async () => { const reloadPluginContent = useCallback(() => {
if (!componentRef.current) { if (!rootElement) {
return; return;
} }
const ctx: InvenTreePluginContext = {
...context,
reloadContent: reloadPluginContent
};
if (sourceFile && functionName) { if (sourceFile && functionName) {
findExternalPluginFunction(sourceFile, functionName) findExternalPluginFunction(sourceFile, functionName)
.then((func) => { .then((func) => {
if (func) { if (!!func) {
try { try {
func(componentRef.current, context); if (func.length > 1) {
// Support "legacy" plugin functions which call createRoot() internally
// Ref: https://github.com/inventree/InvenTree/pull/9439/
func(componentRef.current, ctx);
} else {
// Render the plugin component into the target element
// Note that we have to provide the right context(s) to the component
// This approach ensures that the component is rendered in the correct context tree
rootElement.render(
<ApiProvider client={queryClient} api={api}>
<MantineProvider
theme={ctx.theme}
defaultColorScheme={ctx.colorScheme}
>
<LanguageContext>{func(ctx)}</LanguageContext>
</MantineProvider>
</ApiProvider>
);
}
setRenderingError(''); setRenderingError('');
} catch (error) { } catch (error) {
setRenderingError(`${error}`); setRenderingError(`${error}`);
console.error(error);
} }
} else { } else {
setRenderingError(`${sourceFile}:${functionName}`); setRenderingError(`${sourceFile}:${functionName}`);
@ -76,12 +112,12 @@ export default function RemoteComponent({
`${t`Invalid source or function name`} - ${sourceFile}:${functionName}` `${t`Invalid source or function name`} - ${sourceFile}:${functionName}`
); );
} }
}; }, [componentRef, rootElement, sourceFile, functionName, context]);
// Reload the plugin content dynamically // Reload the plugin content dynamically
useEffect(() => { useEffect(() => {
reloadPluginContent(); reloadPluginContent();
}, [sourceFile, functionName, context]); }, [sourceFile, functionName, context, rootElement]);
return ( return (
<Boundary <Boundary
@ -99,7 +135,7 @@ export default function RemoteComponent({
</Text> </Text>
</Alert> </Alert>
)} )}
<div ref={componentRef as any} /> {componentRef && <div ref={componentRef as any} />}
</Stack> </Stack>
</Boundary> </Boundary>
); );

View File

@ -1,7 +1,7 @@
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '@lib/enums/ModelType';
import { getDetailUrl } from '../../functions/urls'; import { getDetailUrl } from '@lib/functions/Navigation';
import { type InstanceRenderInterface, RenderInlineModel } from './Instance'; import { type InstanceRenderInterface, RenderInlineModel } from './Instance';
import { StatusRenderer } from './StatusRenderer'; import { StatusRenderer } from './StatusRenderer';

View File

@ -1,8 +1,8 @@
import { Text } from '@mantine/core'; import { Text } from '@mantine/core';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '@lib/enums/ModelType';
import { getDetailUrl } from '../../functions/urls'; import { getDetailUrl } from '@lib/functions/Navigation';
import { type InstanceRenderInterface, RenderInlineModel } from './Instance'; import { type InstanceRenderInterface, RenderInlineModel } from './Instance';
/** /**

View File

@ -3,11 +3,12 @@ import { Alert, Anchor, Group, Skeleton, Space, Text } from '@mantine/core';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { type ReactNode, useCallback } from 'react'; import { type ReactNode, useCallback } from 'react';
import { ModelInformationDict } from '@lib/enums/ModelInformation';
import { ModelType } from '@lib/enums/ModelType';
import { apiUrl } from '@lib/functions/Api';
import { navigateToLink } from '@lib/functions/Navigation';
import { useApi } from '../../contexts/ApiContext'; import { useApi } from '../../contexts/ApiContext';
import { ModelType } from '../../enums/ModelType';
import { navigateToLink } from '../../functions/navigation';
import { shortenString } from '../../functions/tables'; import { shortenString } from '../../functions/tables';
import { apiUrl } from '../../states/ApiState';
import { Thumbnail } from '../images/Thumbnail'; import { Thumbnail } from '../images/Thumbnail';
import { RenderBuildItem, RenderBuildLine, RenderBuildOrder } from './Build'; import { RenderBuildItem, RenderBuildLine, RenderBuildOrder } from './Build';
import { import {
@ -24,7 +25,6 @@ import {
RenderProjectCode, RenderProjectCode,
RenderSelectionList RenderSelectionList
} from './Generic'; } from './Generic';
import { ModelInformationDict } from './ModelType';
import { import {
RenderPurchaseOrder, RenderPurchaseOrder,
RenderReturnOrder, RenderReturnOrder,

View File

@ -1,8 +1,8 @@
import { Loader } from '@mantine/core'; import { Loader } from '@mantine/core';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import type { ModelType } from '@lib/enums/ModelType';
import { api } from '../../App'; import { api } from '../../App';
import type { ModelType } from '../../enums/ModelType';
import { RenderInstance } from './Instance'; import { RenderInstance } from './Instance';
/** /**

View File

@ -1,283 +1,8 @@
import { t } from '@lingui/core/macro'; import {
ModelInformationDict,
import { ApiEndpoints } from '../../enums/ApiEndpoints'; type ModelInformationInterface
import type { ModelType } from '../../enums/ModelType'; } from '@lib/enums/ModelInformation';
import type { InvenTreeIconType } from '../../functions/icons'; import type { ModelType } from '@lib/enums/ModelType';
export interface ModelInformationInterface {
label: string;
label_multiple: string;
url_overview?: string;
url_detail?: string;
api_endpoint: ApiEndpoints;
admin_url?: string;
icon: InvenTreeIconType;
}
export interface TranslatableModelInformationInterface
extends Omit<ModelInformationInterface, 'label' | 'label_multiple'> {
label: () => string;
label_multiple: () => string;
}
export type ModelDict = {
[key in keyof typeof ModelType]: TranslatableModelInformationInterface;
};
export const ModelInformationDict: ModelDict = {
part: {
label: () => t`Part`,
label_multiple: () => t`Parts`,
url_overview: '/part/category/index/parts',
url_detail: '/part/:pk/',
api_endpoint: ApiEndpoints.part_list,
admin_url: '/part/part/',
icon: 'part'
},
partparametertemplate: {
label: () => t`Part Parameter Template`,
label_multiple: () => t`Part Parameter Templates`,
url_detail: '/partparametertemplate/:pk/',
api_endpoint: ApiEndpoints.part_parameter_template_list,
icon: 'test_templates'
},
parttesttemplate: {
label: () => t`Part Test Template`,
label_multiple: () => t`Part Test Templates`,
url_detail: '/parttesttemplate/:pk/',
api_endpoint: ApiEndpoints.part_test_template_list,
icon: 'test'
},
supplierpart: {
label: () => t`Supplier Part`,
label_multiple: () => t`Supplier Parts`,
url_overview: '/purchasing/index/supplier-parts',
url_detail: '/purchasing/supplier-part/:pk/',
api_endpoint: ApiEndpoints.supplier_part_list,
admin_url: '/company/supplierpart/',
icon: 'supplier_part'
},
manufacturerpart: {
label: () => t`Manufacturer Part`,
label_multiple: () => t`Manufacturer Parts`,
url_overview: '/purchasing/index/manufacturer-parts',
url_detail: '/purchasing/manufacturer-part/:pk/',
api_endpoint: ApiEndpoints.manufacturer_part_list,
admin_url: '/company/manufacturerpart/',
icon: 'manufacturers'
},
partcategory: {
label: () => t`Part Category`,
label_multiple: () => t`Part Categories`,
url_overview: '/part/category/parts/subcategories',
url_detail: '/part/category/:pk/',
api_endpoint: ApiEndpoints.category_list,
admin_url: '/part/partcategory/',
icon: 'category'
},
stockitem: {
label: () => t`Stock Item`,
label_multiple: () => t`Stock Items`,
url_overview: '/stock/location/index/stock-items',
url_detail: '/stock/item/:pk/',
api_endpoint: ApiEndpoints.stock_item_list,
admin_url: '/stock/stockitem/',
icon: 'stock'
},
stocklocation: {
label: () => t`Stock Location`,
label_multiple: () => t`Stock Locations`,
url_overview: '/stock/location',
url_detail: '/stock/location/:pk/',
api_endpoint: ApiEndpoints.stock_location_list,
admin_url: '/stock/stocklocation/',
icon: 'location'
},
stocklocationtype: {
label: () => t`Stock Location Type`,
label_multiple: () => t`Stock Location Types`,
api_endpoint: ApiEndpoints.stock_location_type_list,
icon: 'location'
},
stockhistory: {
label: () => t`Stock History`,
label_multiple: () => t`Stock Histories`,
api_endpoint: ApiEndpoints.stock_tracking_list,
icon: 'history'
},
build: {
label: () => t`Build`,
label_multiple: () => t`Builds`,
url_overview: '/manufacturing/index/buildorders/',
url_detail: '/manufacturing/build-order/:pk/',
api_endpoint: ApiEndpoints.build_order_list,
admin_url: '/build/build/',
icon: 'build_order'
},
buildline: {
label: () => t`Build Line`,
label_multiple: () => t`Build Lines`,
url_overview: '/build/line',
url_detail: '/build/line/:pk/',
api_endpoint: ApiEndpoints.build_line_list,
icon: 'build_order'
},
builditem: {
label: () => t`Build Item`,
label_multiple: () => t`Build Items`,
api_endpoint: ApiEndpoints.build_item_list,
icon: 'build_order'
},
company: {
label: () => t`Company`,
label_multiple: () => t`Companies`,
url_detail: '/company/:pk/',
api_endpoint: ApiEndpoints.company_list,
admin_url: '/company/company/',
icon: 'building'
},
projectcode: {
label: () => t`Project Code`,
label_multiple: () => t`Project Codes`,
url_detail: '/project-code/:pk/',
api_endpoint: ApiEndpoints.project_code_list,
icon: 'list_details'
},
purchaseorder: {
label: () => t`Purchase Order`,
label_multiple: () => t`Purchase Orders`,
url_overview: '/purchasing/index/purchaseorders',
url_detail: '/purchasing/purchase-order/:pk/',
api_endpoint: ApiEndpoints.purchase_order_list,
admin_url: '/order/purchaseorder/',
icon: 'purchase_orders'
},
purchaseorderlineitem: {
label: () => t`Purchase Order Line`,
label_multiple: () => t`Purchase Order Lines`,
api_endpoint: ApiEndpoints.purchase_order_line_list,
icon: 'purchase_orders'
},
salesorder: {
label: () => t`Sales Order`,
label_multiple: () => t`Sales Orders`,
url_overview: '/sales/index/salesorders',
url_detail: '/sales/sales-order/:pk/',
api_endpoint: ApiEndpoints.sales_order_list,
admin_url: '/order/salesorder/',
icon: 'sales_orders'
},
salesordershipment: {
label: () => t`Sales Order Shipment`,
label_multiple: () => t`Sales Order Shipments`,
url_detail: '/sales/shipment/:pk/',
api_endpoint: ApiEndpoints.sales_order_shipment_list,
icon: 'sales_orders'
},
returnorder: {
label: () => t`Return Order`,
label_multiple: () => t`Return Orders`,
url_overview: '/sales/index/returnorders',
url_detail: '/sales/return-order/:pk/',
api_endpoint: ApiEndpoints.return_order_list,
admin_url: '/order/returnorder/',
icon: 'return_orders'
},
returnorderlineitem: {
label: () => t`Return Order Line Item`,
label_multiple: () => t`Return Order Line Items`,
api_endpoint: ApiEndpoints.return_order_line_list,
icon: 'return_orders'
},
address: {
label: () => t`Address`,
label_multiple: () => t`Addresses`,
url_detail: '/address/:pk/',
api_endpoint: ApiEndpoints.address_list,
icon: 'address'
},
contact: {
label: () => t`Contact`,
label_multiple: () => t`Contacts`,
url_detail: '/contact/:pk/',
api_endpoint: ApiEndpoints.contact_list,
icon: 'group'
},
owner: {
label: () => t`Owner`,
label_multiple: () => t`Owners`,
url_detail: '/owner/:pk/',
api_endpoint: ApiEndpoints.owner_list,
icon: 'group'
},
user: {
label: () => t`User`,
label_multiple: () => t`Users`,
url_detail: '/core/user/:pk/',
api_endpoint: ApiEndpoints.user_list,
icon: 'user'
},
group: {
label: () => t`Group`,
label_multiple: () => t`Groups`,
url_detail: '/core/group/:pk/',
api_endpoint: ApiEndpoints.group_list,
admin_url: '/auth/group/',
icon: 'group'
},
importsession: {
label: () => t`Import Session`,
label_multiple: () => t`Import Sessions`,
url_overview: '/settings/admin/import',
url_detail: '/import/:pk/',
api_endpoint: ApiEndpoints.import_session_list,
icon: 'import'
},
labeltemplate: {
label: () => t`Label Template`,
label_multiple: () => t`Label Templates`,
url_overview: '/settings/admin/labels',
url_detail: '/settings/admin/labels/:pk/',
api_endpoint: ApiEndpoints.label_list,
icon: 'labels'
},
reporttemplate: {
label: () => t`Report Template`,
label_multiple: () => t`Report Templates`,
url_overview: '/settings/admin/reports',
url_detail: '/settings/admin/reports/:pk/',
api_endpoint: ApiEndpoints.report_list,
icon: 'reports'
},
pluginconfig: {
label: () => t`Plugin Configuration`,
label_multiple: () => t`Plugin Configurations`,
url_overview: '/settings/admin/plugin',
url_detail: '/settings/admin/plugin/:pk/',
api_endpoint: ApiEndpoints.plugin_list,
icon: 'plugin'
},
contenttype: {
label: () => t`Content Type`,
label_multiple: () => t`Content Types`,
api_endpoint: ApiEndpoints.content_type_list,
icon: 'list_details'
},
selectionlist: {
label: () => t`Selection List`,
label_multiple: () => t`Selection Lists`,
api_endpoint: ApiEndpoints.selectionlist_list,
icon: 'list_details'
},
error: {
label: () => t`Error`,
label_multiple: () => t`Errors`,
api_endpoint: ApiEndpoints.error_report_list,
url_overview: '/settings/admin/errors',
url_detail: '/settings/admin/errors/:pk/',
icon: 'exclamation'
}
};
/* /*
* Extract model definition given the provided type - returns translatable strings for labels as string, not functions * Extract model definition given the provided type - returns translatable strings for labels as string, not functions

View File

@ -1,8 +1,8 @@
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '@lib/enums/ModelType';
import { getDetailUrl } from '../../functions/urls'; import { getDetailUrl } from '@lib/functions/Navigation';
import { type InstanceRenderInterface, RenderInlineModel } from './Instance'; import { type InstanceRenderInterface, RenderInlineModel } from './Instance';
import { StatusRenderer } from './StatusRenderer'; import { StatusRenderer } from './StatusRenderer';

View File

@ -2,8 +2,8 @@ import { t } from '@lingui/core/macro';
import { Badge } from '@mantine/core'; import { Badge } from '@mantine/core';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '@lib/enums/ModelType';
import { getDetailUrl } from '../../functions/urls'; import { getDetailUrl } from '@lib/functions/Navigation';
import { ApiIcon } from '../items/ApiIcon'; import { ApiIcon } from '../items/ApiIcon';
import { type InstanceRenderInterface, RenderInlineModel } from './Instance'; import { type InstanceRenderInterface, RenderInlineModel } from './Instance';

Some files were not shown because too many files have changed in this diff Show More