2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-23 05:06:32 +00:00

[Plugin] Plugin context ()

* 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
276 changed files with 2797 additions and 1854 deletions
.gitignore
docs/docs/extend
src/frontend
.linguirc.npmignoreCHANGELOG.mdLICENSEREADME.md
lib
package.json
src
components
contexts
defaults
forms
functions
hooks
main.tsx
pages
states
tables
Column.tsxColumnRenderers.tsxFilter.tsxFilterSelectDrawer.tsxInvenTreeTable.tsxInvenTreeTableHeader.tsxRowActions.tsxTableHoverCard.tsx
bom
build
company
general
machine
notifications
part
plugin
purchasing
sales
settings
stock
views
vite-env.d.ts
tests
tsconfig.jsontsconfig.lib.jsontsconfig.node.jsonversion-info.tsvite-env.d.tsvite.config.tsvite.lib.config.tsyarn.lock
tasks.py

@@ -0,0 +1,235 @@
/*
* Enumeration of available API endpoints.
*
* In the cases where endpoints can be accessed with a primary key,
* the primary key should be appended to the endpoint.
* The exception to this is when the endpoint provides an :id parameter.
*/
export enum ApiEndpoints {
api_server_info = '',
// User API endpoints
user_list = 'user/',
user_me = 'user/me/',
user_profile = 'user/profile/',
user_roles = 'user/roles/',
user_token = 'user/token/',
user_tokens = 'user/tokens/',
user_simple_login = 'email/generate/',
// User auth endpoints
user_reset = 'auth/v1/auth/password/request',
user_reset_set = 'auth/v1/auth/password/reset',
auth_pwd_change = 'auth/v1/account/password/change',
auth_login = 'auth/v1/auth/login',
auth_login_2fa = 'auth/v1/auth/2fa/authenticate',
auth_session = 'auth/v1/auth/session',
auth_signup = 'auth/v1/auth/signup',
auth_authenticators = 'auth/v1/account/authenticators',
auth_recovery = 'auth/v1/account/authenticators/recovery-codes',
auth_mfa_reauthenticate = 'auth/v1/auth/2fa/reauthenticate',
auth_totp = 'auth/v1/account/authenticators/totp',
auth_reauthenticate = 'auth/v1/auth/reauthenticate',
auth_email = 'auth/v1/account/email',
auth_email_verify = 'auth/v1/auth/email/verify',
auth_providers = 'auth/v1/account/providers',
auth_provider_redirect = 'auth/v1/auth/provider/redirect',
auth_config = 'auth/v1/config',
// Generic API endpoints
currency_list = 'currency/exchange/',
currency_refresh = 'currency/refresh/',
all_units = 'units/all/',
task_overview = 'background-task/',
task_pending_list = 'background-task/pending/',
task_scheduled_list = 'background-task/scheduled/',
task_failed_list = 'background-task/failed/',
api_search = 'search/',
settings_global_list = 'settings/global/',
settings_user_list = 'settings/user/',
news = 'news/',
global_status = 'generic/status/',
custom_state_list = 'generic/status/custom/',
version = 'version/',
license = 'license/',
group_list = 'user/group/',
owner_list = 'user/owner/',
ruleset_list = 'user/ruleset/',
content_type_list = 'contenttype/',
icons = 'icons/',
selectionlist_list = 'selection/',
selectionlist_detail = 'selection/:id/',
// Barcode API endpoints
barcode = 'barcode/',
barcode_history = 'barcode/history/',
barcode_link = 'barcode/link/',
barcode_unlink = 'barcode/unlink/',
barcode_generate = 'barcode/generate/',
// Data output endpoints
data_output = 'data-output/',
// Data import endpoints
import_session_list = 'importer/session/',
import_session_accept_fields = 'importer/session/:id/accept_fields/',
import_session_accept_rows = 'importer/session/:id/accept_rows/',
import_session_column_mapping_list = 'importer/column-mapping/',
import_session_row_list = 'importer/row/',
// Notification endpoints
notifications_list = 'notifications/',
notifications_readall = 'notifications/readall/',
// Build API endpoints
build_order_list = 'build/',
build_order_issue = 'build/:id/issue/',
build_order_cancel = 'build/:id/cancel/',
build_order_hold = 'build/:id/hold/',
build_order_complete = 'build/:id/finish/',
build_output_complete = 'build/:id/complete/',
build_output_create = 'build/:id/create-output/',
build_output_scrap = 'build/:id/scrap-outputs/',
build_output_delete = 'build/:id/delete-outputs/',
build_order_auto_allocate = 'build/:id/auto-allocate/',
build_order_allocate = 'build/:id/allocate/',
build_order_deallocate = 'build/:id/unallocate/',
build_line_list = 'build/line/',
build_item_list = 'build/item/',
bom_list = 'bom/',
bom_item_validate = 'bom/:id/validate/',
bom_validate = 'part/:id/bom-validate/',
// Part API endpoints
part_list = 'part/',
part_parameter_list = 'part/parameter/',
part_parameter_template_list = 'part/parameter/template/',
part_thumbs_list = 'part/thumbs/',
part_pricing = 'part/:id/pricing/',
part_serial_numbers = 'part/:id/serial-numbers/',
part_scheduling = 'part/:id/scheduling/',
part_pricing_internal = 'part/internal-price/',
part_pricing_sale = 'part/sale-price/',
part_stocktake_list = 'part/stocktake/',
part_stocktake_report_list = 'part/stocktake/report/',
part_stocktake_report_generate = 'part/stocktake/report/generate/',
category_list = 'part/category/',
category_tree = 'part/category/tree/',
category_parameter_list = 'part/category/parameters/',
related_part_list = 'part/related/',
part_test_template_list = 'part/test-template/',
// Company API endpoints
company_list = 'company/',
contact_list = 'company/contact/',
address_list = 'company/address/',
supplier_part_list = 'company/part/',
supplier_part_pricing_list = 'company/price-break/',
manufacturer_part_list = 'company/part/manufacturer/',
manufacturer_part_parameter_list = 'company/part/manufacturer/parameter/',
// Stock location endpoints
stock_location_list = 'stock/location/',
stock_location_type_list = 'stock/location-type/',
stock_location_tree = 'stock/location/tree/',
// Stock item API endpoints
stock_item_list = 'stock/',
stock_tracking_list = 'stock/track/',
stock_test_result_list = 'stock/test/',
stock_transfer = 'stock/transfer/',
stock_remove = 'stock/remove/',
stock_add = 'stock/add/',
stock_count = 'stock/count/',
stock_change_status = 'stock/change_status/',
stock_merge = 'stock/merge/',
stock_assign = 'stock/assign/',
stock_status = 'stock/status/',
stock_install = 'stock/:id/install/',
stock_uninstall = 'stock/:id/uninstall/',
stock_serialize = 'stock/:id/serialize/',
stock_return = 'stock/:id/return/',
stock_serial_info = 'stock/:id/serial-numbers/',
// Generator API endpoints
generate_batch_code = 'generate/batch-code/',
generate_serial_number = 'generate/serial-number/',
// Order API endpoints
purchase_order_list = 'order/po/',
purchase_order_issue = 'order/po/:id/issue/',
purchase_order_hold = 'order/po/:id/hold/',
purchase_order_cancel = 'order/po/:id/cancel/',
purchase_order_complete = 'order/po/:id/complete/',
purchase_order_line_list = 'order/po-line/',
purchase_order_extra_line_list = 'order/po-extra-line/',
purchase_order_receive = 'order/po/:id/receive/',
sales_order_list = 'order/so/',
sales_order_issue = 'order/so/:id/issue/',
sales_order_hold = 'order/so/:id/hold/',
sales_order_cancel = 'order/so/:id/cancel/',
sales_order_ship = 'order/so/:id/ship/',
sales_order_complete = 'order/so/:id/complete/',
sales_order_allocate = 'order/so/:id/allocate/',
sales_order_allocate_serials = 'order/so/:id/allocate-serials/',
sales_order_line_list = 'order/so-line/',
sales_order_extra_line_list = 'order/so-extra-line/',
sales_order_allocation_list = 'order/so-allocation/',
sales_order_shipment_list = 'order/so/shipment/',
sales_order_shipment_complete = 'order/so/shipment/:id/ship/',
return_order_list = 'order/ro/',
return_order_issue = 'order/ro/:id/issue/',
return_order_hold = 'order/ro/:id/hold/',
return_order_cancel = 'order/ro/:id/cancel/',
return_order_complete = 'order/ro/:id/complete/',
return_order_receive = 'order/ro/:id/receive/',
return_order_line_list = 'order/ro-line/',
return_order_extra_line_list = 'order/ro-extra-line/',
// Template API endpoints
label_list = 'label/template/',
label_print = 'label/print/',
report_list = 'report/template/',
report_print = 'report/print/',
report_snippet = 'report/snippet/',
report_asset = 'report/asset/',
// Plugin API endpoints
plugin_list = 'plugins/',
plugin_setting_list = 'plugins/:plugin/settings/',
plugin_registry_status = 'plugins/status/',
plugin_install = 'plugins/install/',
plugin_reload = 'plugins/reload/',
plugin_activate = 'plugins/:key/activate/',
plugin_uninstall = 'plugins/:key/uninstall/',
plugin_admin = 'plugins/:key/admin/',
// User interface plugin endpoints
plugin_ui_features_list = 'plugins/ui/features/:feature_type/',
// Special plugin endpoints
plugin_locate_item = 'locate/',
// Machine API endpoints
machine_types_list = 'machine/types/',
machine_driver_list = 'machine/drivers/',
machine_registry_status = 'machine/status/',
machine_list = 'machine/',
machine_restart = 'machine/:machine/restart/',
machine_setting_list = 'machine/:machine/settings/',
machine_setting_detail = 'machine/:machine/settings/:config_type/',
// Miscellaneous API endpoints
attachment_list = 'attachment/',
error_report_list = 'error-report/',
project_code_list = 'project-code/',
custom_unit_list = 'units/',
notes_image_upload = 'notes-image-upload/'
}

@@ -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'
}
};

@@ -0,0 +1,38 @@
/*
* Enumeration of available API model types
*/
export enum ModelType {
part = 'part',
supplierpart = 'supplierpart',
manufacturerpart = 'manufacturerpart',
partcategory = 'partcategory',
partparametertemplate = 'partparametertemplate',
parttesttemplate = 'parttesttemplate',
projectcode = 'projectcode',
stockitem = 'stockitem',
stocklocation = 'stocklocation',
stocklocationtype = 'stocklocationtype',
stockhistory = 'stockhistory',
build = 'build',
buildline = 'buildline',
builditem = 'builditem',
company = 'company',
purchaseorder = 'purchaseorder',
purchaseorderlineitem = 'purchaseorderlineitem',
salesorder = 'salesorder',
salesordershipment = 'salesordershipment',
returnorder = 'returnorder',
returnorderlineitem = 'returnorderlineitem',
importsession = 'importsession',
address = 'address',
contact = 'contact',
owner = 'owner',
user = 'user',
group = 'group',
reporttemplate = 'reporttemplate',
labeltemplate = 'labeltemplate',
pluginconfig = 'pluginconfig',
contenttype = 'contenttype',
selectionlist = 'selectionlist',
error = 'error'
}

@@ -0,0 +1,54 @@
import { t } from '@lingui/core/macro';
/*
* Enumeration of available user role groups
*/
export enum UserRoles {
admin = 'admin',
build = 'build',
part = 'part',
part_category = 'part_category',
purchase_order = 'purchase_order',
return_order = 'return_order',
sales_order = 'sales_order',
stock = 'stock',
stock_location = 'stock_location',
stocktake = 'stocktake'
}
/*
* Enumeration of available user permissions within each role group
*/
export enum UserPermissions {
view = 'view',
add = 'add',
change = 'change',
delete = 'delete'
}
export function userRoleLabel(role: UserRoles): string {
switch (role) {
case UserRoles.admin:
return t`Admin`;
case UserRoles.build:
return t`Build Orders`;
case UserRoles.part:
return t`Parts`;
case UserRoles.part_category:
return t`Part Categories`;
case UserRoles.purchase_order:
return t`Purchase Orders`;
case UserRoles.return_order:
return t`Return Orders`;
case UserRoles.sales_order:
return t`Sales Orders`;
case UserRoles.stock:
return t`Stock Items`;
case UserRoles.stock_location:
return t`Stock Location`;
case UserRoles.stocktake:
return t`Stocktake`;
default:
return role as string;
}
}

@@ -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;
}

@@ -0,0 +1,6 @@
// Helper function to cancel event propagation
export function cancelEvent(event: any) {
event?.preventDefault();
event?.stopPropagation();
event?.nativeEvent?.stopImmediatePropagation();
}

@@ -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);
}
};

@@ -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

@@ -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';

@@ -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;
};

@@ -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>;

@@ -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;
};

@@ -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[];
}

@@ -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;
};

@@ -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;
}

@@ -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__;

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

@@ -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
}

@@ -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;
};

@@ -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;
}