diff --git a/.gitignore b/.gitignore index bec54dcade..5e43c097ad 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,6 @@ share/ downloads/ eggs/ .eggs/ -lib/ lib64/ parts/ sdist/ diff --git a/docs/docs/extend/how_to_plugin.md b/docs/docs/extend/how_to_plugin.md index 22b60933f6..57d5726362 100644 --- a/docs/docs/extend/how_to_plugin.md +++ b/docs/docs/extend/how_to_plugin.md @@ -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. ### 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))? - 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. ### 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 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): - """ - Use docstrings for everything... pls - """ + """Use docstrings for everything.""" NAME = "SampleActionPlugin" ACTION_NAME = "sample" diff --git a/docs/docs/extend/integrate.md b/docs/docs/extend/integrate.md index bc08656038..6e1bb1442a 100644 --- a/docs/docs/extend/integrate.md +++ b/docs/docs/extend/integrate.md @@ -4,40 +4,8 @@ title: 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. - -### 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. +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. diff --git a/docs/docs/extend/plugins.md b/docs/docs/extend/plugins.md index 3be254e8de..e7287e3e82 100644 --- a/docs/docs/extend/plugins.md +++ b/docs/docs/extend/plugins.md @@ -4,7 +4,7 @@ title: Plugins ## 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: @@ -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. -!!! warning "Namechange" - 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. +!!! warning "Name Change" + The name of the base class was changed with `0.7.0` from `IntegrationPluginBase` to `InvenTreePlugin`. ### 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 !!! 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. 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 | | [ScheduleMixin](./plugins/schedule.md) | Schedule periodic tasks | | [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 | | [ValidationMixin](./plugins/validation.md) | Provide custom validation of database models | diff --git a/docs/docs/extend/plugins/app.md b/docs/docs/extend/plugins/app.md index c9d42de0a7..e607bc7b52 100644 --- a/docs/docs/extend/plugins/app.md +++ b/docs/docs/extend/plugins/app.md @@ -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. !!! 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. diff --git a/docs/docs/extend/plugins/navigation.md b/docs/docs/extend/plugins/navigation.md index 09cb3d4e52..83f5962519 100644 --- a/docs/docs/extend/plugins/navigation.md +++ b/docs/docs/extend/plugins/navigation.md @@ -5,7 +5,7 @@ title: Navigation Mixin ## NavigationMixin 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 class MyNavigationPlugin(NavigationMixin, InvenTreePlugin): diff --git a/docs/docs/extend/plugins/ui.md b/docs/docs/extend/plugins/ui.md index fd6618131d..a1f7f53ab0 100644 --- a/docs/docs/extend/plugins/ui.md +++ b/docs/docs/extend/plugins/ui.md @@ -217,3 +217,47 @@ We are working to develop and distribute a library of custom InvenTree component ### 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. + +## 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. diff --git a/docs/docs/extend/plugins/validation.md b/docs/docs/extend/plugins/validation.md index 5191f79668..d227321f6b 100644 --- a/docs/docs/extend/plugins/validation.md +++ b/docs/docs/extend/plugins/validation.md @@ -69,7 +69,7 @@ from plugin.mixins import ValidationMixin import part.models -class MyValidationMixin(Validationixin, InvenTreePlugin): +class MyValidationMixin(ValidationMixin, InvenTreePlugin): """Custom validation plugin.""" def validate_model_instance(self, instance, deltas=None): diff --git a/src/frontend/.linguirc b/src/frontend/.linguirc index edc5a37fd4..3115f0d9bb 100644 --- a/src/frontend/.linguirc +++ b/src/frontend/.linguirc @@ -41,8 +41,8 @@ "pseudo-LOCALE"], "catalogs": [{ "path": "src/locales/{locale}/messages", - "include": ["src"], - "exclude": ["**/node_modules/**"] + "include": ["src", "lib"], + "exclude": ["**/node_modules/**", "./dist/**"] }], "format": "po", "orderBy": "origin", diff --git a/src/frontend/.npmignore b/src/frontend/.npmignore new file mode 100644 index 0000000000..6a4b221cc4 --- /dev/null +++ b/src/frontend/.npmignore @@ -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 diff --git a/src/frontend/CHANGELOG.md b/src/frontend/CHANGELOG.md new file mode 100644 index 0000000000..459d86d289 --- /dev/null +++ b/src/frontend/CHANGELOG.md @@ -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. diff --git a/src/frontend/LICENSE b/src/frontend/LICENSE new file mode 100644 index 0000000000..3a4626c1c4 --- /dev/null +++ b/src/frontend/LICENSE @@ -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. diff --git a/src/frontend/README.md b/src/frontend/README.md new file mode 100644 index 0000000000..d451ba67d6 --- /dev/null +++ b/src/frontend/README.md @@ -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). diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/lib/enums/ApiEndpoints.tsx similarity index 100% rename from src/frontend/src/enums/ApiEndpoints.tsx rename to src/frontend/lib/enums/ApiEndpoints.tsx diff --git a/src/frontend/lib/enums/ModelInformation.tsx b/src/frontend/lib/enums/ModelInformation.tsx new file mode 100644 index 0000000000..110bf803f2 --- /dev/null +++ b/src/frontend/lib/enums/ModelInformation.tsx @@ -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 { + 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' + } +}; diff --git a/src/frontend/src/enums/ModelType.tsx b/src/frontend/lib/enums/ModelType.tsx similarity index 100% rename from src/frontend/src/enums/ModelType.tsx rename to src/frontend/lib/enums/ModelType.tsx diff --git a/src/frontend/src/enums/Roles.tsx b/src/frontend/lib/enums/Roles.tsx similarity index 100% rename from src/frontend/src/enums/Roles.tsx rename to src/frontend/lib/enums/Roles.tsx diff --git a/src/frontend/lib/functions/Api.tsx b/src/frontend/lib/functions/Api.tsx new file mode 100644 index 0000000000..9fe777e261 --- /dev/null +++ b/src/frontend/lib/functions/Api.tsx @@ -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; +} diff --git a/src/frontend/src/functions/events.tsx b/src/frontend/lib/functions/Events.tsx similarity index 100% rename from src/frontend/src/functions/events.tsx rename to src/frontend/lib/functions/Events.tsx diff --git a/src/frontend/lib/functions/Navigation.tsx b/src/frontend/lib/functions/Navigation.tsx new file mode 100644 index 0000000000..90430eda74 --- /dev/null +++ b/src/frontend/lib/functions/Navigation.tsx @@ -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); + } +}; diff --git a/src/frontend/lib/functions/Plugins.tsx b/src/frontend/lib/functions/Plugins.tsx new file mode 100644 index 0000000000..fabde26e4c --- /dev/null +++ b/src/frontend/lib/functions/Plugins.tsx @@ -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); + } +} diff --git a/src/frontend/lib/index.ts b/src/frontend/lib/index.ts new file mode 100644 index 0000000000..6544d038e0 --- /dev/null +++ b/src/frontend/lib/index.ts @@ -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'; diff --git a/src/frontend/lib/types/Auth.tsx b/src/frontend/lib/types/Auth.tsx new file mode 100644 index 0000000000..a9dfa993f5 --- /dev/null +++ b/src/frontend/lib/types/Auth.tsx @@ -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; +}; diff --git a/src/frontend/lib/types/Core.tsx b/src/frontend/lib/types/Core.tsx new file mode 100644 index 0000000000..19792ac812 --- /dev/null +++ b/src/frontend/lib/types/Core.tsx @@ -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; diff --git a/src/frontend/lib/types/Filters.tsx b/src/frontend/lib/types/Filters.tsx new file mode 100644 index 0000000000..619a33a99e --- /dev/null +++ b/src/frontend/lib/types/Filters.tsx @@ -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; +}; diff --git a/src/frontend/lib/types/Forms.tsx b/src/frontend/lib/types/Forms.tsx new file mode 100644 index 0000000000..9425f43774 --- /dev/null +++ b/src/frontend/lib/types/Forms.tsx @@ -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>; + +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; + +/** + * 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[]; +} diff --git a/src/frontend/lib/types/Icons.tsx b/src/frontend/lib/types/Icons.tsx new file mode 100644 index 0000000000..c06cb197c5 --- /dev/null +++ b/src/frontend/lib/types/Icons.tsx @@ -0,0 +1,9 @@ +import type { Icon, IconProps } from '@tabler/icons-react'; + +export type TablerIconType = React.ForwardRefExoticComponent< + Omit & React.RefAttributes +>; + +export type InvenTreeIconType = { + [key: string]: TablerIconType; +}; diff --git a/src/frontend/lib/types/Modals.tsx b/src/frontend/lib/types/Modals.tsx new file mode 100644 index 0000000000..a0df9c8b0c --- /dev/null +++ b/src/frontend/lib/types/Modals.tsx @@ -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; +} diff --git a/src/frontend/lib/types/Plugins.tsx b/src/frontend/lib/types/Plugins.tsx new file mode 100644 index 0000000000..0f1bb08901 --- /dev/null +++ b/src/frontend/lib/types/Plugins.tsx @@ -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__; diff --git a/src/frontend/lib/types/Server.tsx b/src/frontend/lib/types/Server.tsx new file mode 100644 index 0000000000..308f364380 --- /dev/null +++ b/src/frontend/lib/types/Server.tsx @@ -0,0 +1,8 @@ +export interface Host { + host: string; + name: string; +} + +export interface HostList { + [key: string]: Host; +} diff --git a/src/frontend/lib/types/Settings.tsx b/src/frontend/lib/types/Settings.tsx new file mode 100644 index 0000000000..2203f66195 --- /dev/null +++ b/src/frontend/lib/types/Settings.tsx @@ -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 | 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; + 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 +} diff --git a/src/frontend/lib/types/Tables.tsx b/src/frontend/lib/types/Tables.tsx new file mode 100644 index 0000000000..92a13c5377 --- /dev/null +++ b/src/frontend/lib/types/Tables.tsx @@ -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; +}; diff --git a/src/frontend/lib/types/User.tsx b/src/frontend/lib/types/User.tsx new file mode 100644 index 0000000000..82e7c6b553 --- /dev/null +++ b/src/frontend/lib/types/User.tsx @@ -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; + permissions?: Record; + 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; + setUser: (newUser: UserProps | undefined) => void; + getUser: () => UserProps | undefined; + fetchUserState: () => Promise; + 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; +} diff --git a/src/frontend/package.json b/src/frontend/package.json index d02f84a7b9..383fd83768 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -1,11 +1,40 @@ { - "name": "inventreeui", - "private": true, - "version": "0.1.0", + "name": "@inventreedb/ui", + "description": "UI components for the InvenTree project", + "version": "0.0.8", + "private": false, "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": { "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", "extract": "lingui extract", "compile": "lingui compile --typescript" @@ -101,6 +130,7 @@ "typescript": "^5.8.2", "vite": "^6.2.6", "vite-plugin-babel-macros": "^1.0.6", + "vite-plugin-dts": "^4.5.3", "vite-plugin-istanbul": "^6.0.2" } } diff --git a/src/frontend/src/components/barcodes/BarcodeScanDialog.tsx b/src/frontend/src/components/barcodes/BarcodeScanDialog.tsx index c048c9324a..ea4911f336 100644 --- a/src/frontend/src/components/barcodes/BarcodeScanDialog.tsx +++ b/src/frontend/src/components/barcodes/BarcodeScanDialog.tsx @@ -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 { Box, Divider, Modal } from '@mantine/core'; import { useCallback, useState } from 'react'; import { type NavigateFunction, useNavigate } from 'react-router-dom'; import { api } from '../../App'; -import { ApiEndpoints } from '../../enums/ApiEndpoints'; -import type { ModelType } from '../../enums/ModelType'; import { extractErrorMessage } from '../../functions/api'; -import { getDetailUrl } from '../../functions/urls'; -import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; import { StylishText } from '../items/StylishText'; -import { ModelInformationDict } from '../render/ModelType'; import { BarcodeInput } from './BarcodeInput'; export default function BarcodeScanDialog({ diff --git a/src/frontend/src/components/barcodes/BarcodeScanItem.tsx b/src/frontend/src/components/barcodes/BarcodeScanItem.tsx index 86944859e7..4a38a210f2 100644 --- a/src/frontend/src/components/barcodes/BarcodeScanItem.tsx +++ b/src/frontend/src/components/barcodes/BarcodeScanItem.tsx @@ -1,4 +1,4 @@ -import type { ModelType } from '../../enums/ModelType'; +import type { ModelType } from '@lib/enums/ModelType'; /** * Interface defining a single barcode scan item diff --git a/src/frontend/src/components/barcodes/QRCode.tsx b/src/frontend/src/components/barcodes/QRCode.tsx index 79ad501eaa..2ad407de45 100644 --- a/src/frontend/src/components/barcodes/QRCode.tsx +++ b/src/frontend/src/components/barcodes/QRCode.tsx @@ -18,9 +18,9 @@ import { useQuery } from '@tanstack/react-query'; import QR from 'qrcode'; import { useCallback, useEffect, useMemo, useState } from 'react'; +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { apiUrl } from '@lib/functions/Api'; import { api } from '../../App'; -import { ApiEndpoints } from '../../enums/ApiEndpoints'; -import { apiUrl } from '../../states/ApiState'; import { useGlobalSettingsState } from '../../states/SettingsState'; import { CopyButton } from '../buttons/CopyButton'; import type { QrCodeType } from '../items/ActionDropdown'; diff --git a/src/frontend/src/components/buttons/AdminButton.tsx b/src/frontend/src/components/buttons/AdminButton.tsx index aa84aa0084..df00dcee84 100644 --- a/src/frontend/src/components/buttons/AdminButton.tsx +++ b/src/frontend/src/components/buttons/AdminButton.tsx @@ -2,11 +2,11 @@ import { t } from '@lingui/core/macro'; import { IconUserStar } from '@tabler/icons-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 { useServerApiState } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; -import { ModelInformationDict } from '../render/ModelType'; import { ActionButton } from './ActionButton'; export type AdminButtonProps = { diff --git a/src/frontend/src/components/buttons/PrimaryActionButton.tsx b/src/frontend/src/components/buttons/PrimaryActionButton.tsx index 5bd94c9adc..275fd21379 100644 --- a/src/frontend/src/components/buttons/PrimaryActionButton.tsx +++ b/src/frontend/src/components/buttons/PrimaryActionButton.tsx @@ -1,6 +1,7 @@ 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) @@ -15,7 +16,7 @@ export default function PrimaryActionButton({ }: Readonly<{ title: string; tooltip?: string; - icon?: InvenTreeIconType; + icon?: keyof InvenTreeIconType; color?: string; hidden?: boolean; onClick: () => void; diff --git a/src/frontend/src/components/buttons/PrintingActions.tsx b/src/frontend/src/components/buttons/PrintingActions.tsx index cd1e7aae46..316ee10c8c 100644 --- a/src/frontend/src/components/buttons/PrintingActions.tsx +++ b/src/frontend/src/components/buttons/PrintingActions.tsx @@ -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 { IconPrinter, IconReport, IconTags } from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; import { useMemo, useState } from 'react'; import { api } from '../../App'; -import { ApiEndpoints } from '../../enums/ApiEndpoints'; -import type { ModelType } from '../../enums/ModelType'; import { extractAvailableFields } from '../../functions/forms'; import useDataOutput from '../../hooks/UseDataOutput'; import { useCreateApiFormModal } from '../../hooks/UseForm'; -import { apiUrl } from '../../states/ApiState'; import { useGlobalSettingsState, useUserSettingsState } from '../../states/SettingsState'; -import type { ApiFormFieldSet } from '../forms/fields/ApiFormField'; import { ActionDropdown } from '../items/ActionDropdown'; export function PrintingActions({ diff --git a/src/frontend/src/components/buttons/SSOButton.tsx b/src/frontend/src/components/buttons/SSOButton.tsx index 6f202474ac..4a418b1816 100644 --- a/src/frontend/src/components/buttons/SSOButton.tsx +++ b/src/frontend/src/components/buttons/SSOButton.tsx @@ -14,9 +14,9 @@ import { IconLogin } from '@tabler/icons-react'; +import type { AuthProvider } from '@lib/types/Auth'; import { t } from '@lingui/core/macro'; import { ProviderLogin } from '../../functions/auth'; -import type { Provider } from '../../states/states'; const brandIcons: { [key: string]: JSX.Element } = { google: , @@ -32,7 +32,7 @@ const brandIcons: { [key: string]: JSX.Element } = { microsoft: }; -export function SsoButton({ provider }: Readonly<{ provider: Provider }>) { +export function SsoButton({ provider }: Readonly<{ provider: AuthProvider }>) { return ( ) { ); } -function getBrandIcon(provider: Provider) { +function getBrandIcon(provider: AuthProvider) { return brandIcons[provider.id] || ; } diff --git a/src/frontend/src/components/buttons/SplitButton.tsx b/src/frontend/src/components/buttons/SplitButton.tsx index da2fa81b78..45734aeece 100644 --- a/src/frontend/src/components/buttons/SplitButton.tsx +++ b/src/frontend/src/components/buttons/SplitButton.tsx @@ -10,8 +10,8 @@ import { import { IconChevronDown } from '@tabler/icons-react'; import { useEffect, useMemo, useState } from 'react'; +import type { TablerIconType } from '@lib/types/Icons'; import { identifierString } from '../../functions/conversion'; -import type { TablerIconType } from '../../functions/icons'; import * as classes from './SplitButton.css'; interface SplitButtonOption { diff --git a/src/frontend/src/components/buttons/StarredToggleButton.tsx b/src/frontend/src/components/buttons/StarredToggleButton.tsx index 83f2db76d4..7fd9bbbb7c 100644 --- a/src/frontend/src/components/buttons/StarredToggleButton.tsx +++ b/src/frontend/src/components/buttons/StarredToggleButton.tsx @@ -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 { showNotification } from '@mantine/notifications'; import { IconBell } from '@tabler/icons-react'; import { useApi } from '../../contexts/ApiContext'; -import { ApiEndpoints } from '../../enums/ApiEndpoints'; -import { ModelType } from '../../enums/ModelType'; -import { apiUrl } from '../../states/ApiState'; import { ActionButton } from './ActionButton'; export default function StarredToggleButton({ diff --git a/src/frontend/src/components/calendar/Calendar.tsx b/src/frontend/src/components/calendar/Calendar.tsx index aaa266ec61..a088a8c1f1 100644 --- a/src/frontend/src/components/calendar/Calendar.tsx +++ b/src/frontend/src/components/calendar/Calendar.tsx @@ -4,6 +4,7 @@ import dayGridPlugin from '@fullcalendar/daygrid'; import interactionPlugin from '@fullcalendar/interaction'; import FullCalendar from '@fullcalendar/react'; +import type { TableFilter } from '@lib/types/Filters'; import { t } from '@lingui/core/macro'; import { ActionIcon, @@ -27,7 +28,6 @@ import { import { useCallback, useState } from 'react'; import type { CalendarState } from '../../hooks/UseCalendar'; import { useLocalState } from '../../states/LocalState'; -import type { TableFilter } from '../../tables/Filter'; import { FilterSelectDrawer } from '../../tables/FilterSelectDrawer'; import { TableSearchInput } from '../../tables/Search'; import { Boundary } from '../Boundary'; diff --git a/src/frontend/src/components/calendar/OrderCalendar.tsx b/src/frontend/src/components/calendar/OrderCalendar.tsx index 4b0addf178..b7ae28abd3 100644 --- a/src/frontend/src/components/calendar/OrderCalendar.tsx +++ b/src/frontend/src/components/calendar/OrderCalendar.tsx @@ -3,6 +3,13 @@ import type { EventClickArg, EventContentArg } 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 { ActionIcon, Group, Text } from '@mantine/core'; import { hideNotification, showNotification } from '@mantine/notifications'; @@ -15,22 +22,15 @@ import dayjs from 'dayjs'; import { useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; 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 { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; import { AssignedToMeFilter, HasProjectCodeFilter, OrderStatusFilter, ProjectCodeFilter, - ResponsibleFilter, - type TableFilter + ResponsibleFilter } from '../../tables/Filter'; -import { ModelInformationDict } from '../render/ModelType'; import { StatusRenderer } from '../render/StatusRenderer'; import Calendar from './Calendar'; diff --git a/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx b/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx index f1ab5c0955..dbeab88f1d 100644 --- a/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx +++ b/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx @@ -1,6 +1,6 @@ import { t } from '@lingui/core/macro'; -import { ModelType } from '../../enums/ModelType'; +import { ModelType } from '@lib/enums/ModelType'; import { useGlobalSettingsState } from '../../states/SettingsState'; import type { DashboardWidgetProps } from './DashboardWidget'; import ColorToggleDashboardWidget from './widgets/ColorToggleWidget'; diff --git a/src/frontend/src/components/dashboard/widgets/NewsWidget.tsx b/src/frontend/src/components/dashboard/widgets/NewsWidget.tsx index 02a3607ec5..e7ebac756a 100644 --- a/src/frontend/src/components/dashboard/widgets/NewsWidget.tsx +++ b/src/frontend/src/components/dashboard/widgets/NewsWidget.tsx @@ -14,10 +14,10 @@ import { IconMailCheck } from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; import { useCallback, useMemo } from 'react'; +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { apiUrl } from '@lib/functions/Api'; import { api } from '../../../App'; import { formatDate } from '../../../defaults/formatters'; -import { ApiEndpoints } from '../../../enums/ApiEndpoints'; -import { apiUrl } from '../../../states/ApiState'; import { useUserState } from '../../../states/UserState'; import { StylishText } from '../../items/StylishText'; diff --git a/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx index 542b65e346..7c1f0f82fb 100644 --- a/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx +++ b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx @@ -4,17 +4,15 @@ import { useQuery } from '@tanstack/react-query'; import { type ReactNode, useCallback } from 'react'; 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 type { ModelType } from '../../../enums/ModelType'; -import { - InvenTreeIcon, - type InvenTreeIconType -} from '../../../functions/icons'; -import { navigateToLink } from '../../../functions/navigation'; -import { apiUrl } from '../../../states/ApiState'; +import { InvenTreeIcon } from '../../../functions/icons'; import { useUserState } from '../../../states/UserState'; import { StylishText } from '../../items/StylishText'; -import { ModelInformationDict } from '../../render/ModelType'; import type { DashboardWidgetProps } from '../DashboardWidget'; /** @@ -28,7 +26,7 @@ function QueryCountWidget({ }: Readonly<{ modelType: ModelType; title: string; - icon?: InvenTreeIconType; + icon?: keyof InvenTreeIconType; params: any; }>): ReactNode { const api = useApi(); diff --git a/src/frontend/src/components/details/Details.tsx b/src/frontend/src/components/details/Details.tsx index cbcfe67d9f..2086997845 100644 --- a/src/frontend/src/components/details/Details.tsx +++ b/src/frontend/src/components/details/Details.tsx @@ -17,14 +17,15 @@ import { getValueAtPath } from 'mantine-datatable'; import { useCallback, useMemo } from 'react'; 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 { formatDate } from '../../defaults/formatters'; -import { ApiEndpoints } from '../../enums/ApiEndpoints'; -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 { InvenTreeIcon } from '../../functions/icons'; import { useGlobalSettingsState } from '../../states/SettingsState'; import { CopyButton } from '../buttons/CopyButton'; import { YesNoButton } from '../buttons/YesNoButton'; @@ -35,7 +36,7 @@ import { StatusRenderer } from '../render/StatusRenderer'; export type DetailsField = { hidden?: boolean; - icon?: InvenTreeIconType; + icon?: keyof InvenTreeIconType; name: string; label?: string; badge?: BadgeType; @@ -464,7 +465,7 @@ export function DetailsTableField({ {field.label} diff --git a/src/frontend/src/components/details/DetailsImage.tsx b/src/frontend/src/components/details/DetailsImage.tsx index 9184002bba..8963a7ba84 100644 --- a/src/frontend/src/components/details/DetailsImage.tsx +++ b/src/frontend/src/components/details/DetailsImage.tsx @@ -21,10 +21,10 @@ import { useHover } from '@mantine/hooks'; import { modals } from '@mantine/modals'; import { useMemo, useState } from 'react'; +import type { UserRoles } from '@lib/enums/Roles'; +import { cancelEvent } from '@lib/functions/Events'; import { showNotification } from '@mantine/notifications'; import { api } from '../../App'; -import type { UserRoles } from '../../enums/Roles'; -import { cancelEvent } from '../../functions/events'; import { InvenTreeIcon } from '../../functions/icons'; import { showApiErrorMessage } from '../../functions/notifications'; import { useEditApiFormModal } from '../../hooks/UseForm'; diff --git a/src/frontend/src/components/editors/NotesEditor.tsx b/src/frontend/src/components/editors/NotesEditor.tsx index aa9c0ad636..418ca065d7 100644 --- a/src/frontend/src/components/editors/NotesEditor.tsx +++ b/src/frontend/src/components/editors/NotesEditor.tsx @@ -7,11 +7,11 @@ import 'easymde/dist/easymde.min.css'; import { useCallback, useEffect, useMemo, useState } from 'react'; 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 { 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. diff --git a/src/frontend/src/components/editors/TemplateEditor/TemplateEditor.tsx b/src/frontend/src/components/editors/TemplateEditor/TemplateEditor.tsx index 3024dc9f4c..9398eec487 100644 --- a/src/frontend/src/components/editors/TemplateEditor/TemplateEditor.tsx +++ b/src/frontend/src/components/editors/TemplateEditor/TemplateEditor.tsx @@ -24,14 +24,14 @@ import Split from '@uiw/react-split'; import type React 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 { ModelType } from '../../../enums/ModelType'; -import { apiUrl } from '../../../states/ApiState'; import type { TemplateI } from '../../../tables/settings/TemplateTable'; import { Boundary } from '../../Boundary'; import { SplitButton } from '../../buttons/SplitButton'; import { StandaloneField } from '../../forms/StandaloneField'; -import { ModelInformationDict } from '../../render/ModelType'; type EditorProps = { template: TemplateI; diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index 7841c5b1b6..01d409a2cf 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -2,7 +2,6 @@ import { t } from '@lingui/core/macro'; import { Alert, Button, - type DefaultMantineColor, Divider, Group, LoadingOverlay, @@ -19,14 +18,17 @@ import { FormProvider, type SubmitErrorHandler, type SubmitHandler, - type UseFormReturn, useForm } 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 type { ApiEndpoints } from '../../enums/ApiEndpoints'; -import type { ModelType } from '../../enums/ModelType'; import { type NestedDict, constructField, @@ -38,74 +40,8 @@ import { invalidResponse, showTimeoutNotification } 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 { - 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; -} +import { ApiFormField } from './fields/ApiFormField'; export function OptionsApiForm({ props: _props, @@ -219,7 +155,16 @@ export function ApiForm({ }>) { const api = useApi(); 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( () => props.fields ?? {} @@ -482,7 +427,9 @@ export function ApiForm({ if (props.follow && props.modelType && response.data?.pk) { // 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) { // If we want to automatically update or reload a linked table const pk_field = props.pk_field ?? 'pk'; diff --git a/src/frontend/src/components/forms/AuthenticationForm.tsx b/src/frontend/src/components/forms/AuthenticationForm.tsx index cadceec839..12652f705d 100644 --- a/src/frontend/src/components/forms/AuthenticationForm.tsx +++ b/src/frontend/src/components/forms/AuthenticationForm.tsx @@ -15,9 +15,10 @@ import { useDisclosure } from '@mantine/hooks'; import { useState } from 'react'; 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 { api } from '../../App'; -import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { doBasicLogin, doSimpleLogin, @@ -25,7 +26,7 @@ import { followRedirect } from '../../functions/auth'; import { showLoginNotification } from '../../functions/notifications'; -import { apiUrl, useServerApiState } from '../../states/ApiState'; +import { useServerApiState } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; import { SsoButton } from '../buttons/SSOButton'; diff --git a/src/frontend/src/components/forms/HostOptionsForm.tsx b/src/frontend/src/components/forms/HostOptionsForm.tsx index 123a270383..d485a4dcac 100644 --- a/src/frontend/src/components/forms/HostOptionsForm.tsx +++ b/src/frontend/src/components/forms/HostOptionsForm.tsx @@ -1,3 +1,4 @@ +import type { HostList } from '@lib/types/Server'; import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; import { @@ -13,8 +14,6 @@ import { useForm } from '@mantine/form'; import { randomId } from '@mantine/hooks'; import { IconSquarePlus, IconTrash } from '@tabler/icons-react'; -import type { HostList } from '../../states/states'; - export function HostOptionsForm({ data, saveOptions diff --git a/src/frontend/src/components/forms/InstanceOptions.tsx b/src/frontend/src/components/forms/InstanceOptions.tsx index 0fe361333d..22adc431c7 100644 --- a/src/frontend/src/components/forms/InstanceOptions.tsx +++ b/src/frontend/src/components/forms/InstanceOptions.tsx @@ -20,10 +20,10 @@ import { IconServerSpark } from '@tabler/icons-react'; +import type { HostList } from '@lib/types/Server'; import { Wrapper } from '../../pages/Auth/Layout'; import { useServerApiState } from '../../states/ApiState'; import { useLocalState } from '../../states/LocalState'; -import type { HostList } from '../../states/states'; import { ActionButton } from '../buttons/ActionButton'; import { HostOptionsForm } from './HostOptionsForm'; diff --git a/src/frontend/src/components/forms/StandaloneField.tsx b/src/frontend/src/components/forms/StandaloneField.tsx index f086c12968..0e5d498b1c 100644 --- a/src/frontend/src/components/forms/StandaloneField.tsx +++ b/src/frontend/src/components/forms/StandaloneField.tsx @@ -1,7 +1,8 @@ import { useEffect, useMemo } from 'react'; 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({ fieldDefinition, diff --git a/src/frontend/src/components/forms/fields/ApiFormField.tsx b/src/frontend/src/components/forms/fields/ApiFormField.tsx index 046803117b..a0c3c0f36b 100644 --- a/src/frontend/src/components/forms/fields/ApiFormField.tsx +++ b/src/frontend/src/components/forms/fields/ApiFormField.tsx @@ -1,18 +1,10 @@ import { t } from '@lingui/core/macro'; -import { - Alert, - FileInput, - type MantineStyleProp, - NumberInput, - Stack, - Switch -} from '@mantine/core'; -import type { UseFormReturnType } from '@mantine/form'; +import { Alert, FileInput, NumberInput, Stack, Switch } from '@mantine/core'; 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 { ModelType } from '../../../enums/ModelType'; +import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms'; import { isTrue } from '../../../functions/conversion'; import { ChoiceField } from './ChoiceField'; import DateField from './DateField'; @@ -23,103 +15,6 @@ import { RelatedModelField } from './RelatedModelField'; import { TableField } from './TableField'; import TextField from './TextField'; -export type ApiFormData = UseFormReturnType>; - -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 */ @@ -384,5 +279,3 @@ export function ApiFormField({ ); } - -export type ApiFormFieldSet = Record; diff --git a/src/frontend/src/components/forms/fields/ChoiceField.tsx b/src/frontend/src/components/forms/fields/ChoiceField.tsx index d7dc0dcbd9..7951907321 100644 --- a/src/frontend/src/components/forms/fields/ChoiceField.tsx +++ b/src/frontend/src/components/forms/fields/ChoiceField.tsx @@ -1,10 +1,9 @@ +import type { ApiFormFieldType } from '@lib/types/Forms'; import { Select } from '@mantine/core'; import { useId } from '@mantine/hooks'; import { useCallback, useMemo } from 'react'; import type { FieldValues, UseControllerReturn } from 'react-hook-form'; -import type { ApiFormFieldType } from './ApiFormField'; - /** * Render a 'select' field for selecting from a list of choices */ diff --git a/src/frontend/src/components/forms/fields/DateField.tsx b/src/frontend/src/components/forms/fields/DateField.tsx index 364dafbfb1..4dcb7247d1 100644 --- a/src/frontend/src/components/forms/fields/DateField.tsx +++ b/src/frontend/src/components/forms/fields/DateField.tsx @@ -1,11 +1,10 @@ +import type { ApiFormFieldType } from '@lib/types/Forms'; import { DateInput } from '@mantine/dates'; import dayjs from 'dayjs'; import customParseFormat from 'dayjs/plugin/customParseFormat'; import { useCallback, useId, useMemo } from 'react'; import type { FieldValues, UseControllerReturn } from 'react-hook-form'; -import type { ApiFormFieldType } from './ApiFormField'; - dayjs.extend(customParseFormat); export default function DateField({ diff --git a/src/frontend/src/components/forms/fields/DependentField.tsx b/src/frontend/src/components/forms/fields/DependentField.tsx index 6fc3248507..60fa9497e4 100644 --- a/src/frontend/src/components/forms/fields/DependentField.tsx +++ b/src/frontend/src/components/forms/fields/DependentField.tsx @@ -5,16 +5,13 @@ import { useFormContext } from 'react-hook-form'; +import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms'; import { useApi } from '../../../contexts/ApiContext'; import { constructField, extractAvailableFields } from '../../../functions/forms'; -import { - ApiFormField, - type ApiFormFieldSet, - type ApiFormFieldType -} from './ApiFormField'; +import { ApiFormField } from './ApiFormField'; export function DependentField({ control, diff --git a/src/frontend/src/components/forms/fields/IconField.tsx b/src/frontend/src/components/forms/fields/IconField.tsx index de7ce8bb2d..590a18ab82 100644 --- a/src/frontend/src/components/forms/fields/IconField.tsx +++ b/src/frontend/src/components/forms/fields/IconField.tsx @@ -21,9 +21,9 @@ import { startTransition, useEffect, useMemo, useRef, useState } from 'react'; import type { FieldValues, UseControllerReturn } from 'react-hook-form'; import { FixedSizeGrid as Grid } from 'react-window'; +import type { ApiFormFieldType } from '@lib/types/Forms'; import { useIconState } from '../../../states/IconState'; import { ApiIcon } from '../../items/ApiIcon'; -import type { ApiFormFieldType } from './ApiFormField'; export default function IconField({ controller, diff --git a/src/frontend/src/components/forms/fields/NestedObjectField.tsx b/src/frontend/src/components/forms/fields/NestedObjectField.tsx index fa60893c72..daf3e56752 100644 --- a/src/frontend/src/components/forms/fields/NestedObjectField.tsx +++ b/src/frontend/src/components/forms/fields/NestedObjectField.tsx @@ -1,11 +1,7 @@ +import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms'; import { Accordion, Divider, Stack, Text } from '@mantine/core'; import type { Control, FieldValues } from 'react-hook-form'; - -import { - ApiFormField, - type ApiFormFieldSet, - type ApiFormFieldType -} from './ApiFormField'; +import { ApiFormField } from './ApiFormField'; export function NestedObjectField({ control, diff --git a/src/frontend/src/components/forms/fields/RelatedModelField.tsx b/src/frontend/src/components/forms/fields/RelatedModelField.tsx index 0ece0a7780..8c1038261a 100644 --- a/src/frontend/src/components/forms/fields/RelatedModelField.tsx +++ b/src/frontend/src/components/forms/fields/RelatedModelField.tsx @@ -15,10 +15,10 @@ import { } from 'react-hook-form'; import Select from 'react-select'; +import type { ApiFormFieldType } from '@lib/types/Forms'; import { useApi } from '../../../contexts/ApiContext'; import { vars } from '../../../theme'; import { RenderInstance } from '../../render/Instance'; -import type { ApiFormFieldType } from './ApiFormField'; /** * Render a 'select' field for searching the database against a particular model type diff --git a/src/frontend/src/components/forms/fields/TableField.tsx b/src/frontend/src/components/forms/fields/TableField.tsx index f405b6e556..cfa7ed741f 100644 --- a/src/frontend/src/components/forms/fields/TableField.tsx +++ b/src/frontend/src/components/forms/fields/TableField.tsx @@ -5,11 +5,11 @@ import { IconExclamationCircle } from '@tabler/icons-react'; import { type ReactNode, useCallback, useEffect, useMemo } from 'react'; import type { FieldValues, UseControllerReturn } from 'react-hook-form'; +import type { ApiFormFieldType } from '@lib/types/Forms'; import { identifierString } from '../../../functions/conversion'; import { InvenTreeIcon } from '../../../functions/icons'; import { AddItemButton } from '../../buttons/AddItemButton'; import { StandaloneField } from '../StandaloneField'; -import type { ApiFormFieldType } from './ApiFormField'; export interface TableFieldRowProps { item: any; diff --git a/src/frontend/src/components/importer/ImportDataSelector.tsx b/src/frontend/src/components/importer/ImportDataSelector.tsx index 90a30572a6..36acf4d5bc 100644 --- a/src/frontend/src/components/importer/ImportDataSelector.tsx +++ b/src/frontend/src/components/importer/ImportDataSelector.tsx @@ -9,18 +9,19 @@ import { } from '@tabler/icons-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 { ApiEndpoints } from '../../enums/ApiEndpoints'; -import { cancelEvent } from '../../functions/events'; import { useDeleteApiFormModal, useEditApiFormModal } from '../../hooks/UseForm'; import type { ImportSessionState } from '../../hooks/UseImportSession'; import { useTable } from '../../hooks/UseTable'; -import { apiUrl } from '../../states/ApiState'; import type { TableColumn } from '../../tables/Column'; -import type { TableFilter } from '../../tables/Filter'; import { InvenTreeTable } from '../../tables/InvenTreeTable'; import { type RowAction, @@ -29,7 +30,6 @@ import { } from '../../tables/RowActions'; import { ActionButton } from '../buttons/ActionButton'; import { YesNoButton } from '../buttons/YesNoButton'; -import type { ApiFormFieldSet } from '../forms/fields/ApiFormField'; import { ProgressBar } from '../items/ProgressBar'; import { RenderRemoteInstance } from '../render/Instance'; diff --git a/src/frontend/src/components/importer/ImporterColumnSelector.tsx b/src/frontend/src/components/importer/ImporterColumnSelector.tsx index 3bdd87dbf8..7f3aa487b0 100644 --- a/src/frontend/src/components/importer/ImporterColumnSelector.tsx +++ b/src/frontend/src/components/importer/ImporterColumnSelector.tsx @@ -13,12 +13,12 @@ import { import { IconCheck } from '@tabler/icons-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 { ApiEndpoints } from '../../enums/ApiEndpoints'; import type { ImportSessionState } from '../../hooks/UseImportSession'; -import { apiUrl } from '../../states/ApiState'; import { StandaloneField } from '../forms/StandaloneField'; -import type { ApiFormFieldType } from '../forms/fields/ApiFormField'; function ImporterColumn({ column, diff --git a/src/frontend/src/components/importer/ImporterDrawer.tsx b/src/frontend/src/components/importer/ImporterDrawer.tsx index 6ea4f750a3..7213fac500 100644 --- a/src/frontend/src/components/importer/ImporterDrawer.tsx +++ b/src/frontend/src/components/importer/ImporterDrawer.tsx @@ -16,7 +16,7 @@ import { import { IconCheck } from '@tabler/icons-react'; import { type ReactNode, useMemo } from 'react'; -import { ModelType } from '../../enums/ModelType'; +import { ModelType } from '@lib/enums/ModelType'; import { useImportSession } from '../../hooks/UseImportSession'; import useStatusCodes from '../../hooks/UseStatusCodes'; import { StylishText } from '../items/StylishText'; diff --git a/src/frontend/src/components/importer/ImporterImportProgress.tsx b/src/frontend/src/components/importer/ImporterImportProgress.tsx index 590110db29..3ee15a1a28 100644 --- a/src/frontend/src/components/importer/ImporterImportProgress.tsx +++ b/src/frontend/src/components/importer/ImporterImportProgress.tsx @@ -3,7 +3,7 @@ import { Center, Container, Loader, Stack, Text } from '@mantine/core'; import { useInterval } from '@mantine/hooks'; import { useEffect } from 'react'; -import { ModelType } from '../../enums/ModelType'; +import { ModelType } from '@lib/enums/ModelType'; import type { ImportSessionState } from '../../hooks/UseImportSession'; import useStatusCodes from '../../hooks/UseStatusCodes'; import { StylishText } from '../items/StylishText'; diff --git a/src/frontend/src/components/items/ActionDropdown.tsx b/src/frontend/src/components/items/ActionDropdown.tsx index 47a1ee4c6e..0cb1510375 100644 --- a/src/frontend/src/components/items/ActionDropdown.tsx +++ b/src/frontend/src/components/items/ActionDropdown.tsx @@ -20,7 +20,7 @@ import { } from '@tabler/icons-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 { InvenTreeIcon } from '../../functions/icons'; import { InvenTreeQRCode, QRCodeLink, QRCodeUnlink } from '../barcodes/QRCode'; diff --git a/src/frontend/src/components/items/MenuLinks.tsx b/src/frontend/src/components/items/MenuLinks.tsx index 531b2a81bc..469ab6cd57 100644 --- a/src/frontend/src/components/items/MenuLinks.tsx +++ b/src/frontend/src/components/items/MenuLinks.tsx @@ -11,15 +11,16 @@ import { import { useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; -import { InvenTreeIcon, type InvenTreeIconType } from '../../functions/icons'; -import { navigateToLink } from '../../functions/navigation'; +import { navigateToLink } from '@lib/functions/Navigation'; +import type { InvenTreeIconType } from '@lib/types/Icons'; +import { InvenTreeIcon } from '../../functions/icons'; import { StylishText } from './StylishText'; export interface MenuLinkItem { id: string; title: string | JSX.Element; description?: string; - icon?: InvenTreeIconType; + icon?: keyof InvenTreeIconType; action?: () => void; link?: string; external?: boolean; diff --git a/src/frontend/src/components/items/RoleTable.tsx b/src/frontend/src/components/items/RoleTable.tsx index f63ae17c71..9fc2c28003 100644 --- a/src/frontend/src/components/items/RoleTable.tsx +++ b/src/frontend/src/components/items/RoleTable.tsx @@ -1,3 +1,5 @@ +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { apiUrl } from '@lib/functions/Api'; import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; import { @@ -13,8 +15,6 @@ import { notifications } from '@mantine/notifications'; import { IconCircleCheck, IconReload } from '@tabler/icons-react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { api } from '../../App'; -import { ApiEndpoints } from '../../enums/ApiEndpoints'; -import { apiUrl } from '../../states/ApiState'; export interface RuleSet { pk?: number; diff --git a/src/frontend/src/components/items/StylishText.tsx b/src/frontend/src/components/items/StylishText.tsx index f44c4ab8a2..4521c1e9fd 100644 --- a/src/frontend/src/components/items/StylishText.tsx +++ b/src/frontend/src/components/items/StylishText.tsx @@ -3,33 +3,19 @@ import { Text, darken, getThemeColor, - lighten, - useMantineColorScheme, useMantineTheme } from '@mantine/core'; import { useMemo } from 'react'; -import { useLocalState } from '../../states/LocalState'; // Hook that memoizes the gradient color based on the primary color of the theme const useThematicGradient = () => { - const { usertheme } = useLocalState(); const theme = useMantineTheme(); - const colorScheme = useMantineColorScheme(); const primary = useMemo(() => { - return getThemeColor(usertheme.primaryColor, theme); - }, [usertheme.primaryColor, theme]); + return getThemeColor(theme.primaryColor, theme); + }, [theme]); - const secondary = useMemo(() => { - let secondary = primary; - if (colorScheme.colorScheme == 'dark') { - secondary = lighten(primary, 0.3); - } else { - secondary = darken(primary, 0.3); - } - - return secondary; - }, [usertheme, colorScheme, primary]); + const secondary = useMemo(() => darken(primary, 0.25), [primary]); return useMemo(() => { return { primary, secondary }; diff --git a/src/frontend/src/components/modals/AboutInvenTreeModal.tsx b/src/frontend/src/components/modals/AboutInvenTreeModal.tsx index d4bdc56a4c..30abd00b3b 100644 --- a/src/frontend/src/components/modals/AboutInvenTreeModal.tsx +++ b/src/frontend/src/components/modals/AboutInvenTreeModal.tsx @@ -14,10 +14,11 @@ import { import type { ContextModalProps } from '@mantine/modals'; import { useQuery } from '@tanstack/react-query'; +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { apiUrl } from '@lib/functions/Api'; import { api } from '../../App'; -import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { generateUrl } from '../../functions/urls'; -import { apiUrl, useServerApiState } from '../../states/ApiState'; +import { useServerApiState } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; import { CopyButton } from '../buttons/CopyButton'; import { StylishText } from '../items/StylishText'; diff --git a/src/frontend/src/components/modals/LicenseModal.tsx b/src/frontend/src/components/modals/LicenseModal.tsx index d731d98933..cdd1d86a62 100644 --- a/src/frontend/src/components/modals/LicenseModal.tsx +++ b/src/frontend/src/components/modals/LicenseModal.tsx @@ -14,9 +14,9 @@ import { import { useQuery } from '@tanstack/react-query'; import { useEffect, useMemo, useState } from 'react'; +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { apiUrl } from '@lib/functions/Api'; import { api } from '../../App'; -import { ApiEndpoints } from '../../enums/ApiEndpoints'; -import { apiUrl } from '../../states/ApiState'; export function LicenceView(entries: Readonly) { return ( diff --git a/src/frontend/src/components/nav/BreadcrumbList.tsx b/src/frontend/src/components/nav/BreadcrumbList.tsx index 817617b0db..10985c74a0 100644 --- a/src/frontend/src/components/nav/BreadcrumbList.tsx +++ b/src/frontend/src/components/nav/BreadcrumbList.tsx @@ -10,8 +10,8 @@ import { IconMenu2 } from '@tabler/icons-react'; import { useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; +import { navigateToLink } from '@lib/functions/Navigation'; import { identifierString } from '../../functions/conversion'; -import { navigateToLink } from '../../functions/navigation'; export type Breadcrumb = { icon?: React.ReactNode; diff --git a/src/frontend/src/components/nav/DetailDrawer.tsx b/src/frontend/src/components/nav/DetailDrawer.tsx index 6a48ac5fc4..d47fd4e1ab 100644 --- a/src/frontend/src/components/nav/DetailDrawer.tsx +++ b/src/frontend/src/components/nav/DetailDrawer.tsx @@ -4,7 +4,7 @@ import { useCallback, useMemo } from 'react'; import { Link, Route, Routes, useNavigate, useParams } 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 { StylishText } from '../items/StylishText'; import * as classes from './DetailDrawer.css'; diff --git a/src/frontend/src/components/nav/Header.tsx b/src/frontend/src/components/nav/Header.tsx index 620bda97ba..8d35bb908b 100644 --- a/src/frontend/src/components/nav/Header.tsx +++ b/src/frontend/src/components/nav/Header.tsx @@ -13,13 +13,14 @@ import { useQuery } from '@tanstack/react-query'; import { type ReactNode, useEffect, useMemo, useState } from 'react'; 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 { api } from '../../App'; import { getNavTabs } from '../../defaults/links'; -import { ApiEndpoints } from '../../enums/ApiEndpoints'; -import { navigateToLink } from '../../functions/navigation'; import * as classes from '../../main.css'; -import { apiUrl, useServerApiState } from '../../states/ApiState'; +import { useServerApiState } from '../../states/ApiState'; import { useLocalState } from '../../states/LocalState'; import { useGlobalSettingsState, diff --git a/src/frontend/src/components/nav/InstanceDetail.tsx b/src/frontend/src/components/nav/InstanceDetail.tsx index 1fc966e106..74584e0b10 100644 --- a/src/frontend/src/components/nav/InstanceDetail.tsx +++ b/src/frontend/src/components/nav/InstanceDetail.tsx @@ -1,7 +1,7 @@ import { LoadingOverlay } from '@mantine/core'; -import type { ModelType } from '../../enums/ModelType'; -import type { UserRoles } from '../../enums/Roles'; +import type { ModelType } from '@lib/enums/ModelType'; +import type { UserRoles } from '@lib/enums/Roles'; import { useUserState } from '../../states/UserState'; import ClientError from '../errors/ClientError'; import PermissionDenied from '../errors/PermissionDenied'; diff --git a/src/frontend/src/components/nav/NavigationDrawer.tsx b/src/frontend/src/components/nav/NavigationDrawer.tsx index 7e4d793aa1..35ffe69521 100644 --- a/src/frontend/src/components/nav/NavigationDrawer.tsx +++ b/src/frontend/src/components/nav/NavigationDrawer.tsx @@ -10,9 +10,9 @@ import { import { useViewportSize } from '@mantine/hooks'; 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 { ModelType } from '../../enums/ModelType'; -import { UserRoles } from '../../enums/Roles'; import useInstanceName from '../../hooks/UseInstanceName'; import * as classes from '../../main.css'; import { useGlobalSettingsState } from '../../states/SettingsState'; diff --git a/src/frontend/src/components/nav/NavigationTree.tsx b/src/frontend/src/components/nav/NavigationTree.tsx index 75df079467..decfa835d7 100644 --- a/src/frontend/src/components/nav/NavigationTree.tsx +++ b/src/frontend/src/components/nav/NavigationTree.tsx @@ -21,12 +21,12 @@ import { useQuery } from '@tanstack/react-query'; import { useCallback, useMemo } from 'react'; 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 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 { StylishText } from '../items/StylishText'; diff --git a/src/frontend/src/components/nav/NotificationDrawer.tsx b/src/frontend/src/components/nav/NotificationDrawer.tsx index 6fa77e2555..33b022c933 100644 --- a/src/frontend/src/components/nav/NotificationDrawer.tsx +++ b/src/frontend/src/components/nav/NotificationDrawer.tsx @@ -18,17 +18,17 @@ import { useQuery } from '@tanstack/react-query'; import { useCallback, useMemo } from 'react'; 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 { 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 { Boundary } from '../Boundary'; import { StylishText } from '../items/StylishText'; -import { ModelInformationDict } from '../render/ModelType'; /** * Render a single notification entry in the drawer diff --git a/src/frontend/src/components/nav/SearchDrawer.tsx b/src/frontend/src/components/nav/SearchDrawer.tsx index cc9e0b8c99..481078e0c4 100644 --- a/src/frontend/src/components/nav/SearchDrawer.tsx +++ b/src/frontend/src/components/nav/SearchDrawer.tsx @@ -32,19 +32,20 @@ import { useQuery } from '@tanstack/react-query'; import { useCallback, useEffect, useMemo, useState } from 'react'; 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 { 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 { useUserState } from '../../states/UserState'; import { Boundary } from '../Boundary'; import { RenderInstance } from '../render/Instance'; -import { ModelInformationDict, getModelInfo } from '../render/ModelType'; +import { getModelInfo } from '../render/ModelType'; // Define type for handling individual search queries type SearchQuery = { diff --git a/src/frontend/src/components/panels/AttachmentPanel.tsx b/src/frontend/src/components/panels/AttachmentPanel.tsx index a2d2a4ed89..2c81acdc83 100644 --- a/src/frontend/src/components/panels/AttachmentPanel.tsx +++ b/src/frontend/src/components/panels/AttachmentPanel.tsx @@ -2,7 +2,7 @@ import { t } from '@lingui/core/macro'; import { Skeleton } from '@mantine/core'; 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 type { PanelType } from './Panel'; diff --git a/src/frontend/src/components/panels/NotesPanel.tsx b/src/frontend/src/components/panels/NotesPanel.tsx index 3d3f7e98b2..07ce614ab1 100644 --- a/src/frontend/src/components/panels/NotesPanel.tsx +++ b/src/frontend/src/components/panels/NotesPanel.tsx @@ -2,7 +2,7 @@ import { t } from '@lingui/core/macro'; import { Skeleton } from '@mantine/core'; 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 NotesEditor from '../editors/NotesEditor'; import type { PanelType } from './Panel'; diff --git a/src/frontend/src/components/panels/PanelGroup.tsx b/src/frontend/src/components/panels/PanelGroup.tsx index e101b42770..2928cf25a7 100644 --- a/src/frontend/src/components/panels/PanelGroup.tsx +++ b/src/frontend/src/components/panels/PanelGroup.tsx @@ -28,10 +28,10 @@ import { useParams } 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 { cancelEvent } from '../../functions/events'; -import { navigateToLink } from '../../functions/navigation'; import { usePluginPanels } from '../../hooks/UsePluginPanels'; import { useLocalState } from '../../states/LocalState'; 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 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 reloadInstance - Function to reload the model instance * @param selectedPanel - The currently selected panel * @param onPanelChange - Callback when the active panel changes * @param collapsible - If true, the panel group can be collapsed (defaults to true) @@ -55,6 +56,7 @@ export type PanelProps = { pageKey: string; panels: PanelType[]; instance?: any; + reloadInstance?: () => void; model?: ModelType | string; id?: number | null; selectedPanel?: string; @@ -67,6 +69,7 @@ function BasePanelGroup({ panels, onPanelChange, selectedPanel, + reloadInstance, instance, model, id, @@ -82,9 +85,10 @@ function BasePanelGroup({ // Hook to load plugins for this panel const pluginPanelSet = usePluginPanels({ + id: id, model: model, instance: instance, - id: id + reloadFunc: reloadInstance }); // Rebuild the list of panels diff --git a/src/frontend/src/components/plugins/LocateItemButton.tsx b/src/frontend/src/components/plugins/LocateItemButton.tsx index 691e4bea62..7a2e82aef9 100644 --- a/src/frontend/src/components/plugins/LocateItemButton.tsx +++ b/src/frontend/src/components/plugins/LocateItemButton.tsx @@ -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 { IconRadar } from '@tabler/icons-react'; import { useEffect, useMemo, useState } from 'react'; -import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { useCreateApiFormModal } from '../../hooks/UseForm'; import { usePluginsWithMixin } from '../../hooks/UsePlugins'; -import { apiUrl } from '../../states/ApiState'; import { ActionButton } from '../buttons/ActionButton'; -import type { ApiFormFieldSet } from '../forms/fields/ApiFormField'; import type { PluginInterface } from './PluginInterface'; export default function LocateItemButton({ diff --git a/src/frontend/src/components/plugins/PluginContext.tsx b/src/frontend/src/components/plugins/PluginContext.tsx index 075193d7e4..d6451f0666 100644 --- a/src/frontend/src/components/plugins/PluginContext.tsx +++ b/src/frontend/src/components/plugins/PluginContext.tsx @@ -1,51 +1,27 @@ -import { - type MantineColorScheme, - type MantineTheme, - useMantineColorScheme, - useMantineTheme -} from '@mantine/core'; -import type { AxiosInstance } from 'axios'; +import { useMantineColorScheme, useMantineTheme } from '@mantine/core'; 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 { useLocalState } from '../../states/LocalState'; import { - type SettingsStateProps, useGlobalSettingsState, useUserSettingsState } from '../../states/SettingsState'; -import { type UserStateProps, useUserState } from '../../states/UserState'; +import { useUserState } from '../../states/UserState'; -/** - * A set of properties which are passed to a plugin, - * for rendering an element in the user interface. - * - * @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 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; -}; +import { + INVENTREE_MANTINE_VERSION, + INVENTREE_PLUGIN_VERSION, + INVENTREE_REACT_VERSION, + type InvenTreePluginContext +} from '@lib/types/Plugins'; +import { + useBulkEditApiFormModal, + useCreateApiFormModal, + useDeleteApiFormModal, + useEditApiFormModal +} from '../../hooks/UseForm'; export const useInvenTreeContext = () => { const [locale, host] = useLocalState((s) => [s.language, s.host]); @@ -56,8 +32,14 @@ export const useInvenTreeContext = () => { const globalSettings = useGlobalSettingsState(); const userSettings = useUserSettingsState(); - const contextData = useMemo(() => { + const contextData = useMemo(() => { return { + version: { + inventree: INVENTREE_PLUGIN_VERSION, + react: INVENTREE_REACT_VERSION, + reactDom: INVENTREE_REACT_VERSION, + mantine: INVENTREE_MANTINE_VERSION + }, user: user, host: host, locale: locale, @@ -67,7 +49,13 @@ export const useInvenTreeContext = () => { globalSettings: globalSettings, userSettings: userSettings, theme: theme, - colorScheme: colorScheme + colorScheme: colorScheme, + forms: { + bulkEdit: useBulkEditApiFormModal, + create: useCreateApiFormModal, + delete: useDeleteApiFormModal, + edit: useEditApiFormModal + } }; }, [ user, diff --git a/src/frontend/src/components/plugins/PluginDrawer.tsx b/src/frontend/src/components/plugins/PluginDrawer.tsx index 69cbf8bb92..ce3c7f7342 100644 --- a/src/frontend/src/components/plugins/PluginDrawer.tsx +++ b/src/frontend/src/components/plugins/PluginDrawer.tsx @@ -4,7 +4,7 @@ import { IconExclamationCircle } from '@tabler/icons-react'; import { useMemo } from 'react'; import { useParams } from 'react-router-dom'; -import { ApiEndpoints } from '../../enums/ApiEndpoints'; +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { useInstance } from '../../hooks/UseInstance'; import { InfoItem } from '../items/InfoItem'; import { StylishText } from '../items/StylishText'; diff --git a/src/frontend/src/components/plugins/PluginPanel.tsx b/src/frontend/src/components/plugins/PluginPanel.tsx index d11e64aca0..5eee77edea 100644 --- a/src/frontend/src/components/plugins/PluginPanel.tsx +++ b/src/frontend/src/components/plugins/PluginPanel.tsx @@ -1,7 +1,7 @@ import { Stack } from '@mantine/core'; import type { ReactNode } from 'react'; -import type { InvenTreeContext } from './PluginContext'; +import type { InvenTreePluginContext } from '@lib/types/Plugins'; import type { PluginUIFeature } from './PluginUIFeature'; import RemoteComponent from './RemoteComponent'; @@ -23,7 +23,7 @@ export default function PluginPanelContent({ pluginContext }: Readonly<{ pluginFeature: PluginUIFeature; - pluginContext: InvenTreeContext; + pluginContext: InvenTreePluginContext; }>): ReactNode { return ( diff --git a/src/frontend/src/components/plugins/PluginSettingsPanel.tsx b/src/frontend/src/components/plugins/PluginSettingsPanel.tsx index d365811df6..d2a057478c 100644 --- a/src/frontend/src/components/plugins/PluginSettingsPanel.tsx +++ b/src/frontend/src/components/plugins/PluginSettingsPanel.tsx @@ -1,3 +1,4 @@ +import type { InvenTreePluginContext } from '@lib/types/Plugins'; import { useInvenTreeContext } from './PluginContext'; import RemoteComponent from './RemoteComponent'; @@ -21,13 +22,13 @@ export default function PluginSettingsPanel({ }: Readonly<{ pluginAdmin: PluginAdminInterface; }>) { - const pluginContext = useInvenTreeContext(); + const ctx: InvenTreePluginContext = useInvenTreeContext(); return ( ); } diff --git a/src/frontend/src/components/plugins/PluginUIFeatureTypes.ts b/src/frontend/src/components/plugins/PluginUIFeatureTypes.ts index 98c658061b..3505f366db 100644 --- a/src/frontend/src/components/plugins/PluginUIFeatureTypes.ts +++ b/src/frontend/src/components/plugins/PluginUIFeatureTypes.ts @@ -1,8 +1,8 @@ -import type { ModelType } from '../../enums/ModelType'; -import type { InvenTreeIconType } from '../../functions/icons'; +import type { ModelType } from '@lib/enums/ModelType'; +import type { InvenTreeIconType } from '@lib/types/Icons'; +import type { InvenTreePluginContext } from '@lib/types/Plugins'; import type { TemplateI } from '../../tables/settings/TemplateTable'; import type { TemplateEditorProps } from '../editors/TemplateEditor/TemplateEditor'; -import type { InvenTreeContext } from './PluginContext'; import type { PluginUIFeature } from './PluginUIFeature'; // #region Type Helpers @@ -19,7 +19,7 @@ export type PluginUIGetFeatureType< ServerContext extends Record > = (params: { featureContext: T['featureContext']; - inventreeContext: InvenTreeContext; + inventreeContext: InvenTreePluginContext; serverContext: ServerContext; }) => T['featureReturnType']; diff --git a/src/frontend/src/components/plugins/RemoteComponent.tsx b/src/frontend/src/components/plugins/RemoteComponent.tsx index 7f4605636f..52d216c577 100644 --- a/src/frontend/src/components/plugins/RemoteComponent.tsx +++ b/src/frontend/src/components/plugins/RemoteComponent.tsx @@ -1,11 +1,15 @@ 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 { 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 { Boundary } from '../Boundary'; -import type { InvenTreeContext } from './PluginContext'; import { findExternalPluginFunction } from './PluginSource'; /** @@ -24,9 +28,16 @@ export default function RemoteComponent({ }: Readonly<{ source: string; defaultFunctionName: string; - context: InvenTreeContext; + context: InvenTreePluginContext; }>) { const componentRef = useRef(); + const [rootElement, setRootElement] = useState(null); + + useEffect(() => { + if (componentRef.current && !rootElement) { + setRootElement(createRoot(componentRef.current)); + } + }, [componentRef.current]); const [renderingError, setRenderingError] = useState( undefined @@ -47,20 +58,45 @@ export default function RemoteComponent({ return defaultFunctionName; }, [source, defaultFunctionName]); - const reloadPluginContent = async () => { - if (!componentRef.current) { + const reloadPluginContent = useCallback(() => { + if (!rootElement) { return; } + const ctx: InvenTreePluginContext = { + ...context, + reloadContent: reloadPluginContent + }; + if (sourceFile && functionName) { findExternalPluginFunction(sourceFile, functionName) .then((func) => { - if (func) { + if (!!func) { 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( + + + {func(ctx)} + + + ); + } + setRenderingError(''); } catch (error) { setRenderingError(`${error}`); + console.error(error); } } else { setRenderingError(`${sourceFile}:${functionName}`); @@ -76,12 +112,12 @@ export default function RemoteComponent({ `${t`Invalid source or function name`} - ${sourceFile}:${functionName}` ); } - }; + }, [componentRef, rootElement, sourceFile, functionName, context]); // Reload the plugin content dynamically useEffect(() => { reloadPluginContent(); - }, [sourceFile, functionName, context]); + }, [sourceFile, functionName, context, rootElement]); return ( )} -
+ {componentRef &&
} ); diff --git a/src/frontend/src/components/render/Build.tsx b/src/frontend/src/components/render/Build.tsx index 04ce1e1bb6..5ba72f0809 100644 --- a/src/frontend/src/components/render/Build.tsx +++ b/src/frontend/src/components/render/Build.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from 'react'; -import { ModelType } from '../../enums/ModelType'; -import { getDetailUrl } from '../../functions/urls'; +import { ModelType } from '@lib/enums/ModelType'; +import { getDetailUrl } from '@lib/functions/Navigation'; import { type InstanceRenderInterface, RenderInlineModel } from './Instance'; import { StatusRenderer } from './StatusRenderer'; diff --git a/src/frontend/src/components/render/Company.tsx b/src/frontend/src/components/render/Company.tsx index 75c713e4c9..d95d23c158 100644 --- a/src/frontend/src/components/render/Company.tsx +++ b/src/frontend/src/components/render/Company.tsx @@ -1,8 +1,8 @@ import { Text } from '@mantine/core'; import type { ReactNode } from 'react'; -import { ModelType } from '../../enums/ModelType'; -import { getDetailUrl } from '../../functions/urls'; +import { ModelType } from '@lib/enums/ModelType'; +import { getDetailUrl } from '@lib/functions/Navigation'; import { type InstanceRenderInterface, RenderInlineModel } from './Instance'; /** diff --git a/src/frontend/src/components/render/Instance.tsx b/src/frontend/src/components/render/Instance.tsx index 86dc86ce4e..006256fc6f 100644 --- a/src/frontend/src/components/render/Instance.tsx +++ b/src/frontend/src/components/render/Instance.tsx @@ -3,11 +3,12 @@ import { Alert, Anchor, Group, Skeleton, Space, Text } from '@mantine/core'; import { useQuery } from '@tanstack/react-query'; 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 { ModelType } from '../../enums/ModelType'; -import { navigateToLink } from '../../functions/navigation'; import { shortenString } from '../../functions/tables'; -import { apiUrl } from '../../states/ApiState'; import { Thumbnail } from '../images/Thumbnail'; import { RenderBuildItem, RenderBuildLine, RenderBuildOrder } from './Build'; import { @@ -24,7 +25,6 @@ import { RenderProjectCode, RenderSelectionList } from './Generic'; -import { ModelInformationDict } from './ModelType'; import { RenderPurchaseOrder, RenderReturnOrder, diff --git a/src/frontend/src/components/render/InstanceFromUrl.tsx b/src/frontend/src/components/render/InstanceFromUrl.tsx index c9a9c8e23a..57be00aff3 100644 --- a/src/frontend/src/components/render/InstanceFromUrl.tsx +++ b/src/frontend/src/components/render/InstanceFromUrl.tsx @@ -1,8 +1,8 @@ import { Loader } from '@mantine/core'; import { useMemo, useState } from 'react'; +import type { ModelType } from '@lib/enums/ModelType'; import { api } from '../../App'; -import type { ModelType } from '../../enums/ModelType'; import { RenderInstance } from './Instance'; /** diff --git a/src/frontend/src/components/render/ModelType.tsx b/src/frontend/src/components/render/ModelType.tsx index 6057e40c5c..2c1b9bbbba 100644 --- a/src/frontend/src/components/render/ModelType.tsx +++ b/src/frontend/src/components/render/ModelType.tsx @@ -1,283 +1,8 @@ -import { t } from '@lingui/core/macro'; - -import { ApiEndpoints } from '../../enums/ApiEndpoints'; -import type { ModelType } from '../../enums/ModelType'; -import type { InvenTreeIconType } from '../../functions/icons'; - -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 { - 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' - } -}; +import { + ModelInformationDict, + type ModelInformationInterface +} from '@lib/enums/ModelInformation'; +import type { ModelType } from '@lib/enums/ModelType'; /* * Extract model definition given the provided type - returns translatable strings for labels as string, not functions diff --git a/src/frontend/src/components/render/Order.tsx b/src/frontend/src/components/render/Order.tsx index e1462cd3a0..b382313648 100644 --- a/src/frontend/src/components/render/Order.tsx +++ b/src/frontend/src/components/render/Order.tsx @@ -1,8 +1,8 @@ import { t } from '@lingui/core/macro'; import type { ReactNode } from 'react'; -import { ModelType } from '../../enums/ModelType'; -import { getDetailUrl } from '../../functions/urls'; +import { ModelType } from '@lib/enums/ModelType'; +import { getDetailUrl } from '@lib/functions/Navigation'; import { type InstanceRenderInterface, RenderInlineModel } from './Instance'; import { StatusRenderer } from './StatusRenderer'; diff --git a/src/frontend/src/components/render/Part.tsx b/src/frontend/src/components/render/Part.tsx index f2b57863ef..ad61c7228c 100644 --- a/src/frontend/src/components/render/Part.tsx +++ b/src/frontend/src/components/render/Part.tsx @@ -2,8 +2,8 @@ import { t } from '@lingui/core/macro'; import { Badge } from '@mantine/core'; import type { ReactNode } from 'react'; -import { ModelType } from '../../enums/ModelType'; -import { getDetailUrl } from '../../functions/urls'; +import { ModelType } from '@lib/enums/ModelType'; +import { getDetailUrl } from '@lib/functions/Navigation'; import { ApiIcon } from '../items/ApiIcon'; import { type InstanceRenderInterface, RenderInlineModel } from './Instance'; diff --git a/src/frontend/src/components/render/StatusRenderer.tsx b/src/frontend/src/components/render/StatusRenderer.tsx index 3b145732ec..8ae2c6bf5b 100644 --- a/src/frontend/src/components/render/StatusRenderer.tsx +++ b/src/frontend/src/components/render/StatusRenderer.tsx @@ -1,7 +1,7 @@ import { Badge, Center, type MantineSize } from '@mantine/core'; +import type { ModelType } from '@lib/enums/ModelType'; import { statusColorMap } from '../../defaults/backendMappings'; -import type { ModelType } from '../../enums/ModelType'; import { resolveItem } from '../../functions/conversion'; import { useGlobalStatusState } from '../../states/StatusState'; diff --git a/src/frontend/src/components/render/Stock.tsx b/src/frontend/src/components/render/Stock.tsx index a83b4b366a..30aec4db01 100644 --- a/src/frontend/src/components/render/Stock.tsx +++ b/src/frontend/src/components/render/Stock.tsx @@ -2,8 +2,8 @@ import { t } from '@lingui/core/macro'; import { Text } from '@mantine/core'; import type { ReactNode } from 'react'; -import { ModelType } from '../../enums/ModelType'; -import { getDetailUrl } from '../../functions/urls'; +import { ModelType } from '@lib/enums/ModelType'; +import { getDetailUrl } from '@lib/functions/Navigation'; import { ApiIcon } from '../items/ApiIcon'; import { type InstanceRenderInterface, RenderInlineModel } from './Instance'; diff --git a/src/frontend/src/components/settings/SettingItem.tsx b/src/frontend/src/components/settings/SettingItem.tsx index 3dc6aef715..71fa6f0ead 100644 --- a/src/frontend/src/components/settings/SettingItem.tsx +++ b/src/frontend/src/components/settings/SettingItem.tsx @@ -11,14 +11,14 @@ import { import { IconEdit } from '@tabler/icons-react'; import { useEffect, useMemo, useState } from 'react'; +import { ModelInformationDict } from '@lib/enums/ModelInformation'; +import { ModelType } from '@lib/enums/ModelType'; +import { apiUrl } from '@lib/functions/Api'; +import type { Setting } from '@lib/types/Settings'; import { api } from '../../App'; -import { ModelType } from '../../enums/ModelType'; -import { apiUrl } from '../../states/ApiState'; -import type { Setting } from '../../states/states'; import { vars } from '../../theme'; import { Boundary } from '../Boundary'; import { RenderInstance } from '../render/Instance'; -import { ModelInformationDict } from '../render/ModelType'; /** * Render a single setting value diff --git a/src/frontend/src/components/settings/SettingList.tsx b/src/frontend/src/components/settings/SettingList.tsx index 401712f683..a706a112b3 100644 --- a/src/frontend/src/components/settings/SettingList.tsx +++ b/src/frontend/src/components/settings/SettingList.tsx @@ -11,18 +11,17 @@ import React, { } from 'react'; import { useStore } from 'zustand'; +import type { ModelType } from '@lib/enums/ModelType'; +import { apiUrl } from '@lib/functions/Api'; +import type { Setting, SettingsStateProps } from '@lib/types/Settings'; import { useApi } from '../../contexts/ApiContext'; -import type { ModelType } from '../../enums/ModelType'; import { useEditApiFormModal } from '../../hooks/UseForm'; -import { apiUrl } from '../../states/ApiState'; import { - type SettingsStateProps, createMachineSettingsState, createPluginSettingsState, useGlobalSettingsState, useUserSettingsState } from '../../states/SettingsState'; -import type { Setting } from '../../states/states'; import { SettingItem } from './SettingItem'; /** diff --git a/src/frontend/src/components/wizards/OrderPartsWizard.tsx b/src/frontend/src/components/wizards/OrderPartsWizard.tsx index 6234f333ec..34326603e4 100644 --- a/src/frontend/src/components/wizards/OrderPartsWizard.tsx +++ b/src/frontend/src/components/wizards/OrderPartsWizard.tsx @@ -1,22 +1,22 @@ +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { 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 { Alert, Group, Paper, Tooltip } from '@mantine/core'; import { showNotification } from '@mantine/notifications'; import { IconShoppingCart } from '@tabler/icons-react'; import { DataTable } from 'mantine-datatable'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import { ApiEndpoints } from '../../enums/ApiEndpoints'; -import { ModelType } from '../../enums/ModelType'; import { useSupplierPartFields } from '../../forms/CompanyForms'; import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms'; import { useCreateApiFormModal } from '../../hooks/UseForm'; import useWizard from '../../hooks/UseWizard'; -import { apiUrl } from '../../states/ApiState'; import { PartColumn } from '../../tables/ColumnRenderers'; import { ActionButton } from '../buttons/ActionButton'; import { AddItemButton } from '../buttons/AddItemButton'; import RemoveRowButton from '../buttons/RemoveRowButton'; import { StandaloneField } from '../forms/StandaloneField'; -import type { ApiFormFieldSet } from '../forms/fields/ApiFormField'; import Expand from '../items/Expand'; /** diff --git a/src/frontend/src/contexts/ThemeContext.tsx b/src/frontend/src/contexts/ThemeContext.tsx index 16e4eb06a3..6373c660b2 100644 --- a/src/frontend/src/contexts/ThemeContext.tsx +++ b/src/frontend/src/contexts/ThemeContext.tsx @@ -15,14 +15,14 @@ import { colorSchema } from './colorSchema'; export function ThemeContext({ children }: Readonly<{ children: JSX.Element }>) { - const [usertheme] = useLocalState((state) => [state.usertheme]); + const [userTheme] = useLocalState((state) => [state.userTheme]); // Theme const myTheme = createTheme({ - primaryColor: usertheme.primaryColor, - white: usertheme.whiteColor, - black: usertheme.blackColor, - defaultRadius: usertheme.radius, + primaryColor: userTheme.primaryColor, + white: userTheme.whiteColor, + black: userTheme.blackColor, + defaultRadius: userTheme.radius, breakpoints: { xs: '30em', sm: '48em', diff --git a/src/frontend/src/defaults/backendMappings.tsx b/src/frontend/src/defaults/backendMappings.tsx index 96b3f15e4f..4754d9abb9 100644 --- a/src/frontend/src/defaults/backendMappings.tsx +++ b/src/frontend/src/defaults/backendMappings.tsx @@ -1,4 +1,4 @@ -import { ModelType } from '../enums/ModelType'; +import { ModelType } from '@lib/enums/ModelType'; /* Lookup tables for mapping backend responses to internal types */ diff --git a/src/frontend/src/defaults/defaultHostList.tsx b/src/frontend/src/defaults/defaultHostList.tsx index 467bcb6f25..0ed11acacb 100644 --- a/src/frontend/src/defaults/defaultHostList.tsx +++ b/src/frontend/src/defaults/defaultHostList.tsx @@ -1,4 +1,4 @@ -import type { HostList } from '../states/states'; +import type { HostList } from '@lib/types/Server'; export const defaultHostList: HostList = window.INVENTREE_SETTINGS.server_list; export const defaultHostKey = window.INVENTREE_SETTINGS.default_server; diff --git a/src/frontend/src/defaults/formatters.tsx b/src/frontend/src/defaults/formatters.tsx index 63dbf510fe..9b5ad1dc2b 100644 --- a/src/frontend/src/defaults/formatters.tsx +++ b/src/frontend/src/defaults/formatters.tsx @@ -1,4 +1,3 @@ -import type { MantineSize } from '@mantine/core'; import dayjs from 'dayjs'; import { @@ -175,5 +174,3 @@ export function formatDate( return date; } } - -export type UiSizeType = MantineSize | string | number; diff --git a/src/frontend/src/defaults/links.tsx b/src/frontend/src/defaults/links.tsx index b913dcac37..1e0a055168 100644 --- a/src/frontend/src/defaults/links.tsx +++ b/src/frontend/src/defaults/links.tsx @@ -2,6 +2,9 @@ import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; import { openContextModal } from '@mantine/modals'; +import { UserRoles } from '@lib/enums/Roles'; +import type { SettingsStateProps } from '@lib/types/Settings'; +import type { UserStateProps } from '@lib/types/User'; import { IconBox, IconBuildingFactory2, @@ -13,9 +16,6 @@ import { import type { ReactNode } from 'react'; import type { MenuLinkItem } from '../components/items/MenuLinks'; import { StylishText } from '../components/items/StylishText'; -import { UserRoles } from '../enums/Roles'; -import type { SettingsStateProps } from '../states/SettingsState'; -import type { UserStateProps } from '../states/UserState'; type NavTab = { name: string; diff --git a/src/frontend/src/forms/BomForms.tsx b/src/frontend/src/forms/BomForms.tsx index 18c43bc801..7487303d13 100644 --- a/src/frontend/src/forms/BomForms.tsx +++ b/src/frontend/src/forms/BomForms.tsx @@ -1,4 +1,4 @@ -import type { ApiFormFieldSet } from '../components/forms/fields/ApiFormField'; +import type { ApiFormFieldSet } from '@lib/types/Forms'; /** * Field set for BomItem form diff --git a/src/frontend/src/forms/BuildForms.tsx b/src/frontend/src/forms/BuildForms.tsx index df69c2192e..a1cf34c6ae 100644 --- a/src/frontend/src/forms/BuildForms.tsx +++ b/src/frontend/src/forms/BuildForms.tsx @@ -11,24 +11,22 @@ import { } from '@tabler/icons-react'; import { useEffect, useMemo, useState } from 'react'; +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { ModelType } from '@lib/enums/ModelType'; import RemoveRowButton from '../components/buttons/RemoveRowButton'; import { StandaloneField } from '../components/forms/StandaloneField'; -import type { - ApiFormFieldSet, - ApiFormFieldType -} from '../components/forms/fields/ApiFormField'; + +import { apiUrl } from '@lib/functions/Api'; +import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms'; import { TableFieldErrorWrapper, type TableFieldRowProps } from '../components/forms/fields/TableField'; import { ProgressBar } from '../components/items/ProgressBar'; import { StatusRenderer } from '../components/render/StatusRenderer'; -import { ApiEndpoints } from '../enums/ApiEndpoints'; -import { ModelType } from '../enums/ModelType'; import { useCreateApiFormModal } from '../hooks/UseForm'; import { useBatchCodeGenerator } from '../hooks/UseGenerator'; import { useSerialNumberPlaceholder } from '../hooks/UsePlaceholder'; -import { apiUrl } from '../states/ApiState'; import { useGlobalSettingsState } from '../states/SettingsState'; import { PartColumn } from '../tables/ColumnRenderers'; diff --git a/src/frontend/src/forms/CommonForms.tsx b/src/frontend/src/forms/CommonForms.tsx index ba4b3d09e1..f611214fa5 100644 --- a/src/frontend/src/forms/CommonForms.tsx +++ b/src/frontend/src/forms/CommonForms.tsx @@ -1,7 +1,7 @@ import { IconUsers } from '@tabler/icons-react'; import { useMemo, useState } from 'react'; -import type { ApiFormFieldSet } from '../components/forms/fields/ApiFormField'; +import type { ApiFormFieldSet } from '@lib/types/Forms'; import type { StatusCodeInterface, StatusCodeListInterface diff --git a/src/frontend/src/forms/CompanyForms.tsx b/src/frontend/src/forms/CompanyForms.tsx index ad9a3d63c9..d7be251e12 100644 --- a/src/frontend/src/forms/CompanyForms.tsx +++ b/src/frontend/src/forms/CompanyForms.tsx @@ -1,3 +1,7 @@ +import type { + ApiFormAdjustFilterType, + ApiFormFieldSet +} from '@lib/types/Forms'; import { IconAt, IconCurrencyDollar, @@ -10,11 +14,6 @@ import { } from '@tabler/icons-react'; import { useMemo } from 'react'; -import type { - ApiFormAdjustFilterType, - ApiFormFieldSet -} from '../components/forms/fields/ApiFormField'; - /** * Field set for SupplierPart instance */ diff --git a/src/frontend/src/forms/ImporterForms.tsx b/src/frontend/src/forms/ImporterForms.tsx index 3065a34fe5..d8be70f158 100644 --- a/src/frontend/src/forms/ImporterForms.tsx +++ b/src/frontend/src/forms/ImporterForms.tsx @@ -1,4 +1,4 @@ -import type { ApiFormFieldSet } from '../components/forms/fields/ApiFormField'; +import type { ApiFormFieldSet } from '@lib/types/Forms'; export function dataImporterSessionFields(): ApiFormFieldSet { return { diff --git a/src/frontend/src/forms/PartForms.tsx b/src/frontend/src/forms/PartForms.tsx index 7ebdd0c64a..ab90a38b32 100644 --- a/src/frontend/src/forms/PartForms.tsx +++ b/src/frontend/src/forms/PartForms.tsx @@ -2,10 +2,10 @@ import { t } from '@lingui/core/macro'; import { IconPackages } from '@tabler/icons-react'; import { useMemo, useState } from 'react'; -import type { ApiFormFieldSet } from '../components/forms/fields/ApiFormField'; +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { apiUrl } from '@lib/functions/Api'; +import type { ApiFormFieldSet } from '@lib/types/Forms'; import { useApi } from '../contexts/ApiContext'; -import { ApiEndpoints } from '../enums/ApiEndpoints'; -import { apiUrl } from '../states/ApiState'; import { useGlobalSettingsState } from '../states/SettingsState'; /** diff --git a/src/frontend/src/forms/PurchaseOrderForms.tsx b/src/frontend/src/forms/PurchaseOrderForms.tsx index 2280ff544f..e80ec6548e 100644 --- a/src/frontend/src/forms/PurchaseOrderForms.tsx +++ b/src/frontend/src/forms/PurchaseOrderForms.tsx @@ -24,15 +24,19 @@ import { } from '@tabler/icons-react'; import { useEffect, useMemo, useState } from 'react'; +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { ModelType } from '@lib/enums/ModelType'; import { IconCalendarExclamation } from '@tabler/icons-react'; import dayjs from 'dayjs'; import { ActionButton } from '../components/buttons/ActionButton'; import RemoveRowButton from '../components/buttons/RemoveRowButton'; import { StandaloneField } from '../components/forms/StandaloneField'; + +import { apiUrl } from '@lib/functions/Api'; import type { ApiFormAdjustFilterType, ApiFormFieldSet -} from '../components/forms/fields/ApiFormField'; +} from '@lib/types/Forms'; import { TableFieldExtraRow, type TableFieldRowProps @@ -41,15 +45,12 @@ import { Thumbnail } from '../components/images/Thumbnail'; import { ProgressBar } from '../components/items/ProgressBar'; import { StylishText } from '../components/items/StylishText'; import { getStatusCodeOptions } from '../components/render/StatusRenderer'; -import { ApiEndpoints } from '../enums/ApiEndpoints'; -import { ModelType } from '../enums/ModelType'; import { InvenTreeIcon } from '../functions/icons'; import { useCreateApiFormModal } from '../hooks/UseForm'; import { useBatchCodeGenerator, useSerialNumberGenerator } from '../hooks/UseGenerator'; -import { apiUrl } from '../states/ApiState'; import { useGlobalSettingsState } from '../states/SettingsState'; /* * Construct a set of fields for creating / editing a PurchaseOrderLineItem instance diff --git a/src/frontend/src/forms/ReturnOrderForms.tsx b/src/frontend/src/forms/ReturnOrderForms.tsx index 1918af96e8..892b038226 100644 --- a/src/frontend/src/forms/ReturnOrderForms.tsx +++ b/src/frontend/src/forms/ReturnOrderForms.tsx @@ -8,18 +8,19 @@ import { } from '@tabler/icons-react'; import { useMemo } from 'react'; +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { ModelType } from '@lib/enums/ModelType'; import RemoveRowButton from '../components/buttons/RemoveRowButton'; import { StandaloneField } from '../components/forms/StandaloneField'; + +import { apiUrl } from '@lib/functions/Api'; import type { ApiFormAdjustFilterType, ApiFormFieldSet -} from '../components/forms/fields/ApiFormField'; +} from '@lib/types/Forms'; import type { TableFieldRowProps } from '../components/forms/fields/TableField'; import { Thumbnail } from '../components/images/Thumbnail'; -import { ApiEndpoints } from '../enums/ApiEndpoints'; -import { ModelType } from '../enums/ModelType'; import { useCreateApiFormModal } from '../hooks/UseForm'; -import { apiUrl } from '../states/ApiState'; import { useGlobalSettingsState } from '../states/SettingsState'; import { StatusFilterOptions } from '../tables/Filter'; diff --git a/src/frontend/src/forms/SalesOrderForms.tsx b/src/frontend/src/forms/SalesOrderForms.tsx index d70e5eb06c..96c1805707 100644 --- a/src/frontend/src/forms/SalesOrderForms.tsx +++ b/src/frontend/src/forms/SalesOrderForms.tsx @@ -8,19 +8,20 @@ import { } from '@tabler/icons-react'; import { useEffect, useMemo, useState } from 'react'; +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { ModelType } from '@lib/enums/ModelType'; import RemoveRowButton from '../components/buttons/RemoveRowButton'; import { StandaloneField } from '../components/forms/StandaloneField'; + +import { apiUrl } from '@lib/functions/Api'; import type { ApiFormAdjustFilterType, ApiFormFieldSet, ApiFormFieldType -} from '../components/forms/fields/ApiFormField'; +} from '@lib/types/Forms'; import type { TableFieldRowProps } from '../components/forms/fields/TableField'; import { ProgressBar } from '../components/items/ProgressBar'; -import { ApiEndpoints } from '../enums/ApiEndpoints'; -import { ModelType } from '../enums/ModelType'; import { useCreateApiFormModal } from '../hooks/UseForm'; -import { apiUrl } from '../states/ApiState'; import { useGlobalSettingsState } from '../states/SettingsState'; import { PartColumn } from '../tables/ColumnRenderers'; diff --git a/src/frontend/src/forms/StockForms.tsx b/src/frontend/src/forms/StockForms.tsx index 18b573961e..6fc9bfdf95 100644 --- a/src/frontend/src/forms/StockForms.tsx +++ b/src/frontend/src/forms/StockForms.tsx @@ -13,17 +13,23 @@ import { import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; import { Suspense, useEffect, useMemo, useState } from 'react'; +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { ModelType } from '@lib/enums/ModelType'; import dayjs from 'dayjs'; import { useNavigate } from 'react-router-dom'; import { api } from '../App'; import { ActionButton } from '../components/buttons/ActionButton'; import RemoveRowButton from '../components/buttons/RemoveRowButton'; import { StandaloneField } from '../components/forms/StandaloneField'; + +import { apiUrl } from '@lib/functions/Api'; +import { getDetailUrl } from '@lib/index'; import type { ApiFormAdjustFilterType, ApiFormFieldChoice, - ApiFormFieldSet -} from '../components/forms/fields/ApiFormField'; + ApiFormFieldSet, + ApiFormModalProps +} from '@lib/types/Forms'; import { TableFieldExtraRow, type TableFieldRowProps @@ -31,12 +37,8 @@ import { import { Thumbnail } from '../components/images/Thumbnail'; import { StylishText } from '../components/items/StylishText'; import { StatusRenderer } from '../components/render/StatusRenderer'; -import { ApiEndpoints } from '../enums/ApiEndpoints'; -import { ModelType } from '../enums/ModelType'; import { InvenTreeIcon } from '../functions/icons'; -import { getDetailUrl } from '../functions/urls'; import { - type ApiFormModalProps, useApiFormModal, useCreateApiFormModal, useDeleteApiFormModal @@ -46,7 +48,6 @@ import { useSerialNumberGenerator } from '../hooks/UseGenerator'; import { useSerialNumberPlaceholder } from '../hooks/UsePlaceholder'; -import { apiUrl } from '../states/ApiState'; import { useGlobalSettingsState } from '../states/SettingsState'; import { StatusFilterOptions } from '../tables/Filter'; diff --git a/src/frontend/src/forms/selectionListFields.tsx b/src/frontend/src/forms/selectionListFields.tsx index 718c95ccef..d26af88a00 100644 --- a/src/frontend/src/forms/selectionListFields.tsx +++ b/src/frontend/src/forms/selectionListFields.tsx @@ -4,10 +4,8 @@ import { useMemo } from 'react'; import RemoveRowButton from '../components/buttons/RemoveRowButton'; import { StandaloneField } from '../components/forms/StandaloneField'; -import type { - ApiFormFieldSet, - ApiFormFieldType -} from '../components/forms/fields/ApiFormField'; + +import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms'; import type { TableFieldRowProps } from '../components/forms/fields/TableField'; function BuildAllocateLineRow({ diff --git a/src/frontend/src/functions/auth.tsx b/src/frontend/src/functions/auth.tsx index 9d3f24d0a5..f2e11f7434 100644 --- a/src/frontend/src/functions/auth.tsx +++ b/src/frontend/src/functions/auth.tsx @@ -1,14 +1,16 @@ +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { apiUrl } from '@lib/functions/Api'; +import { type AuthProvider, FlowEnum } from '@lib/types/Auth'; import { t } from '@lingui/core/macro'; import { notifications, showNotification } from '@mantine/notifications'; import axios from 'axios'; import type { AxiosRequestConfig } from 'axios'; import type { Location, NavigateFunction } from 'react-router-dom'; import { api, setApiDefaults } from '../App'; -import { ApiEndpoints } from '../enums/ApiEndpoints'; -import { apiUrl, useServerApiState } from '../states/ApiState'; +import { useServerApiState } from '../states/ApiState'; import { useLocalState } from '../states/LocalState'; import { useUserState } from '../states/UserState'; -import { FlowEnum, type Provider, fetchGlobalStates } from '../states/states'; +import { fetchGlobalStates } from '../states/states'; import { showLoginNotification } from './notifications'; import { generateUrl } from './urls'; @@ -181,7 +183,7 @@ function observeProfile() { // overwrite language and theme info in session with profile info const user = useUserState.getState().getUser(); - const { language, setLanguage, usertheme, setTheme } = + const { language, setLanguage, userTheme, setTheme } = useLocalState.getState(); if (user) { if (user.profile?.language && language != user.profile.language) { @@ -196,14 +198,14 @@ function observeProfile() { if (user.profile?.theme) { // extract keys of usertheme and set them to the values of user.profile.theme - const newTheme = Object.keys(usertheme).map((key) => { + const newTheme = Object.keys(userTheme).map((key) => { return { - key: key as keyof typeof usertheme, + key: key as keyof typeof userTheme, value: user.profile.theme[key] as string }; }); const diff = newTheme.filter( - (item) => usertheme[item.key] !== item.value + (item) => userTheme[item.key] !== item.value ); if (diff.length > 0) { showNotification({ @@ -361,7 +363,7 @@ export function clearCsrfCookie() { } export async function ProviderLogin( - provider: Provider, + provider: AuthProvider, process: 'login' | 'connect' = 'login' ) { await ensureCsrf(); diff --git a/src/frontend/src/functions/forms.tsx b/src/frontend/src/functions/forms.tsx index 02f7a40497..6b2c6fb563 100644 --- a/src/frontend/src/functions/forms.tsx +++ b/src/frontend/src/functions/forms.tsx @@ -1,11 +1,9 @@ import type { AxiosResponse } from 'axios'; -import type { - ApiFormFieldSet, - ApiFormFieldType -} from '../components/forms/fields/ApiFormField'; -import type { ApiEndpoints } from '../enums/ApiEndpoints'; -import { type PathParams, apiUrl } from '../states/ApiState'; +import type { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { apiUrl } from '@lib/functions/Api'; +import type { PathParams } from '@lib/types/Core'; +import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms'; import { invalidResponse, permissionDenied } from './notifications'; /** diff --git a/src/frontend/src/functions/icons.tsx b/src/frontend/src/functions/icons.tsx index d7e75eece8..35d45c934b 100644 --- a/src/frontend/src/functions/icons.tsx +++ b/src/frontend/src/functions/icons.tsx @@ -1,5 +1,5 @@ +import type { InvenTreeIconType, TablerIconType } from '@lib/types/Icons'; import { - type Icon, Icon123, IconArrowBigDownLineFilled, IconArrowMerge, @@ -108,7 +108,7 @@ import { } from '@tabler/icons-react'; import type React from 'react'; -const icons = { +const icons: InvenTreeIconType = { name: IconPoint, description: IconInfoCircle, variant_of: IconHierarchy, @@ -259,24 +259,19 @@ const icons = { search: IconSearch }; -export type InvenTreeIconType = keyof typeof icons; -export type TablerIconType = React.ForwardRefExoticComponent< - Omit & React.RefAttributes ->; - /** * Returns a Tabler Icon for the model field name supplied * @param field string defining field name */ -export function GetIcon(field: string): TablerIconType { - return icons[field as InvenTreeIconType]; +export function GetIcon(field: string | number): TablerIconType { + return icons[field]; } // Aliasing the new type name to make it distinct type TablerIconProps = IconProps; type InvenTreeIconProps = { - icon: InvenTreeIconType; + icon: string | keyof InvenTreeIconType; iconProps?: TablerIconProps; }; diff --git a/src/frontend/src/functions/navigation.tsx b/src/frontend/src/functions/navigation.tsx deleted file mode 100644 index 5d10c1a45f..0000000000 --- a/src/frontend/src/functions/navigation.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { getBaseUrl } from '../main'; -import { cancelEvent } from './events'; - -/* - * 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: any, event: any) => { - cancelEvent(event); - - if (event?.ctrlKey || event?.shiftKey) { - // Open the link in a new tab - const url = `/${getBaseUrl()}${link}`; - window.open(url, '_blank'); - } else { - // Navigate internally - navigate(link); - } -}; diff --git a/src/frontend/src/functions/urls.tsx b/src/frontend/src/functions/urls.tsx index 08ccfcdd2d..830b82b215 100644 --- a/src/frontend/src/functions/urls.tsx +++ b/src/frontend/src/functions/urls.tsx @@ -1,37 +1,5 @@ -import { ModelInformationDict } from '../components/render/ModelType'; -import type { ModelType } from '../enums/ModelType'; -import { getBaseUrl } from '../main'; import { useLocalState } from '../states/LocalState'; -/** - * 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 ''; -} - /** * Returns the edit view URL for a given model type */ diff --git a/src/frontend/src/hooks/UseCalendar.tsx b/src/frontend/src/hooks/UseCalendar.tsx index 9f9a0a78aa..e728e399b4 100644 --- a/src/frontend/src/hooks/UseCalendar.tsx +++ b/src/frontend/src/hooks/UseCalendar.tsx @@ -1,15 +1,16 @@ import type FullCalendar from '@fullcalendar/react'; +import type { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { apiUrl } from '@lib/functions/Api'; +import type { FilterSetState } from '@lib/types/Filters'; +import type { UseModalReturn } from '@lib/types/Modals'; import type { DateValue } from '@mantine/dates'; import { type UseQueryResult, useQuery } from '@tanstack/react-query'; import dayjs from 'dayjs'; import { useCallback, useMemo, useRef, useState } from 'react'; import { api } from '../App'; -import type { ApiEndpoints } from '../enums/ApiEndpoints'; import { showApiErrorMessage } from '../functions/notifications'; -import { apiUrl } from '../states/ApiState'; import useDataExport from './UseDataExport'; -import { type FilterSetState, useFilterSet } from './UseFilterSet'; -import type { UseModalReturn } from './UseModal'; +import { useFilterSet } from './UseFilterSet'; /* * Type definition for representing the state of a calendar: diff --git a/src/frontend/src/hooks/UseDashboardItems.tsx b/src/frontend/src/hooks/UseDashboardItems.tsx index 4eb506f982..52202fbfad 100644 --- a/src/frontend/src/hooks/UseDashboardItems.tsx +++ b/src/frontend/src/hooks/UseDashboardItems.tsx @@ -1,6 +1,8 @@ import { useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { apiUrl } from '@lib/functions/Api'; import { api } from '../App'; import type { DashboardWidgetProps } from '../components/dashboard/DashboardWidget'; import DashboardWidgetLibrary from '../components/dashboard/DashboardWidgetLibrary'; @@ -10,9 +12,7 @@ import { PluginUIFeatureType } from '../components/plugins/PluginUIFeature'; import RemoteComponent from '../components/plugins/RemoteComponent'; -import { ApiEndpoints } from '../enums/ApiEndpoints'; import { identifierString } from '../functions/conversion'; -import { apiUrl } from '../states/ApiState'; import { useGlobalSettingsState } from '../states/SettingsState'; import { useUserState } from '../states/UserState'; diff --git a/src/frontend/src/hooks/UseDataExport.tsx b/src/frontend/src/hooks/UseDataExport.tsx index 2e712814a0..18465b5215 100644 --- a/src/frontend/src/hooks/UseDataExport.tsx +++ b/src/frontend/src/hooks/UseDataExport.tsx @@ -1,7 +1,7 @@ +import type { ApiFormFieldSet } from '@lib/types/Forms'; import { t } from '@lingui/core/macro'; import { useQuery } from '@tanstack/react-query'; import { useMemo, useState } from 'react'; -import type { ApiFormFieldSet } from '../components/forms/fields/ApiFormField'; import { useApi } from '../contexts/ApiContext'; import { extractAvailableFields } from '../functions/forms'; import useDataOutput from './UseDataOutput'; diff --git a/src/frontend/src/hooks/UseDataOutput.tsx b/src/frontend/src/hooks/UseDataOutput.tsx index 023efe4feb..f0bb815dd8 100644 --- a/src/frontend/src/hooks/UseDataOutput.tsx +++ b/src/frontend/src/hooks/UseDataOutput.tsx @@ -1,3 +1,5 @@ +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { apiUrl } from '@lib/functions/Api'; import { t } from '@lingui/core/macro'; import { notifications, showNotification } from '@mantine/notifications'; import { IconCircleCheck } from '@tabler/icons-react'; @@ -5,9 +7,7 @@ import { useQuery } from '@tanstack/react-query'; import { useEffect, useState } from 'react'; import { ProgressBar } from '../components/items/ProgressBar'; import { useApi } from '../contexts/ApiContext'; -import { ApiEndpoints } from '../enums/ApiEndpoints'; import { generateUrl } from '../functions/urls'; -import { apiUrl } from '../states/ApiState'; /** * Hook for monitoring a data output process running on the server diff --git a/src/frontend/src/hooks/UseFilter.tsx b/src/frontend/src/hooks/UseFilter.tsx index 90bf32ea40..31ad5ed244 100644 --- a/src/frontend/src/hooks/UseFilter.tsx +++ b/src/frontend/src/hooks/UseFilter.tsx @@ -5,9 +5,9 @@ import { useQuery } from '@tanstack/react-query'; import { useCallback, useMemo } from 'react'; +import type { TableFilterChoice } from '@lib/types/Filters'; import { useApi } from '../contexts/ApiContext'; import { resolveItem } from '../functions/conversion'; -import type { TableFilterChoice } from '../tables/Filter'; type UseFilterProps = { url: string; diff --git a/src/frontend/src/hooks/UseFilterSet.tsx b/src/frontend/src/hooks/UseFilterSet.tsx index 41a6d9b295..db5c2aba0e 100644 --- a/src/frontend/src/hooks/UseFilterSet.tsx +++ b/src/frontend/src/hooks/UseFilterSet.tsx @@ -1,22 +1,6 @@ +import type { FilterSetState, TableFilter } from '@lib/types/Filters'; import { useLocalStorage } from '@mantine/hooks'; import { useCallback } from 'react'; -import type { TableFilter } from '../tables/Filter'; - -/* - * 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; -}; export function useFilterSet(filterKey: string): FilterSetState { // Array of active filters (saved to local storage) diff --git a/src/frontend/src/hooks/UseForm.tsx b/src/frontend/src/hooks/UseForm.tsx index d651cc53ac..dd8b10f1dc 100644 --- a/src/frontend/src/hooks/UseForm.tsx +++ b/src/frontend/src/hooks/UseForm.tsx @@ -3,27 +3,13 @@ import { Alert, Divider, Stack } from '@mantine/core'; import { useId } from '@mantine/hooks'; import { useEffect, useMemo, useRef } from 'react'; -import { type ApiFormProps, OptionsApiForm } from '../components/forms/ApiForm'; -import type { UiSizeType } from '../defaults/formatters'; +import type { + ApiFormModalProps, + BulkEditApiFormModalProps +} from '@lib/types/Forms'; +import { OptionsApiForm } from '../components/forms/ApiForm'; import { useModal } from './UseModal'; -/** - * @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; -} - /** * Construct and open a modal form */ @@ -118,10 +104,6 @@ export function useEditApiFormModal(props: ApiFormModalProps) { return useApiFormModal(editProps); } -interface BulkEditApiFormModalProps extends ApiFormModalProps { - items: number[]; -} - export function useBulkEditApiFormModal({ items, ...props diff --git a/src/frontend/src/hooks/UseGenerator.tsx b/src/frontend/src/hooks/UseGenerator.tsx index 1b11fb9399..aaaca15862 100644 --- a/src/frontend/src/hooks/UseGenerator.tsx +++ b/src/frontend/src/hooks/UseGenerator.tsx @@ -2,9 +2,9 @@ import { useDebouncedValue } from '@mantine/hooks'; import { useQuery } from '@tanstack/react-query'; import { useCallback, useState } from 'react'; +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { apiUrl } from '@lib/functions/Api'; import { api } from '../App'; -import { ApiEndpoints } from '../enums/ApiEndpoints'; -import { apiUrl } from '../states/ApiState'; export type GeneratorState = { query: Record; diff --git a/src/frontend/src/hooks/UseImportSession.tsx b/src/frontend/src/hooks/UseImportSession.tsx index f12adc4566..dde146f019 100644 --- a/src/frontend/src/hooks/UseImportSession.tsx +++ b/src/frontend/src/hooks/UseImportSession.tsx @@ -1,7 +1,7 @@ import { useCallback, useMemo } from 'react'; -import { ApiEndpoints } from '../enums/ApiEndpoints'; -import { ModelType } from '../enums/ModelType'; +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { ModelType } from '@lib/enums/ModelType'; import { useInstance } from './UseInstance'; import useStatusCodes from './UseStatusCodes'; diff --git a/src/frontend/src/hooks/UseInstance.tsx b/src/frontend/src/hooks/UseInstance.tsx index 08a8ae8a9c..b9a81a19ef 100644 --- a/src/frontend/src/hooks/UseInstance.tsx +++ b/src/frontend/src/hooks/UseInstance.tsx @@ -1,9 +1,10 @@ import { type QueryObserverResult, useQuery } from '@tanstack/react-query'; import { useCallback, useMemo, useState } from 'react'; +import type { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { apiUrl } from '@lib/functions/Api'; +import type { PathParams } from '@lib/types/Core'; import { useApi } from '../contexts/ApiContext'; -import type { ApiEndpoints } from '../enums/ApiEndpoints'; -import { type PathParams, apiUrl } from '../states/ApiState'; export interface UseInstanceResult { instance: any; diff --git a/src/frontend/src/hooks/UseModal.tsx b/src/frontend/src/hooks/UseModal.tsx index bb1ab82852..47c6d53d08 100644 --- a/src/frontend/src/hooks/UseModal.tsx +++ b/src/frontend/src/hooks/UseModal.tsx @@ -1,26 +1,9 @@ import { Modal } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; -import type React from 'react'; import { useCallback } from 'react'; +import type { UseModalProps, UseModalReturn } from '@lib/types/Modals'; import { StylishText } from '../components/items/StylishText'; -import type { UiSizeType } from '../defaults/formatters'; - -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; -} export function useModal(props: UseModalProps): UseModalReturn { const onOpen = useCallback(() => { diff --git a/src/frontend/src/hooks/UsePlaceholder.tsx b/src/frontend/src/hooks/UsePlaceholder.tsx index 127412e608..3a655862c4 100644 --- a/src/frontend/src/hooks/UsePlaceholder.tsx +++ b/src/frontend/src/hooks/UsePlaceholder.tsx @@ -2,9 +2,9 @@ import { t } from '@lingui/core/macro'; import { useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { apiUrl } from '@lib/functions/Api'; import { api } from '../App'; -import { ApiEndpoints } from '../enums/ApiEndpoints'; -import { apiUrl } from '../states/ApiState'; /** * Hook for generating a placeholder text for a serial number input diff --git a/src/frontend/src/hooks/UsePluginPanels.tsx b/src/frontend/src/hooks/UsePluginPanels.tsx index f0d798c215..e7e28d50b6 100644 --- a/src/frontend/src/hooks/UsePluginPanels.tsx +++ b/src/frontend/src/hooks/UsePluginPanels.tsx @@ -1,34 +1,21 @@ import { type UseQueryResult, useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import type { ModelType } from '@lib/enums/ModelType'; +import { apiUrl } from '@lib/functions/Api'; +import type { InvenTreePluginContext } from '@lib/types/Plugins'; import { api } from '../App'; import { ApiIcon } from '../components/items/ApiIcon'; import type { PanelType } from '../components/panels/Panel'; -import { - type InvenTreeContext, - useInvenTreeContext -} from '../components/plugins/PluginContext'; +import { useInvenTreeContext } from '../components/plugins/PluginContext'; import PluginPanelContent from '../components/plugins/PluginPanel'; import { type PluginUIFeature, PluginUIFeatureType } from '../components/plugins/PluginUIFeature'; -import { ApiEndpoints } from '../enums/ApiEndpoints'; -import type { ModelType } from '../enums/ModelType'; -import { apiUrl } from '../states/ApiState'; import { useGlobalSettingsState } from '../states/SettingsState'; -/** - * @param model - The model type for the plugin (e.g. 'part' / 'purchaseorder') - * @param id - The ID (primary key) of the model instance for the plugin - * @param instance - The model instance data (if available) - */ -export type PluginPanelContext = InvenTreeContext & { - model?: ModelType | string; - id?: string | number | null; - instance?: any; -}; - /** * Type definition for a plugin panel which extends the standard PanelType * @param pluginName - The name of the plugin which provides this panel @@ -46,10 +33,12 @@ export type PluginPanelSet = { export function usePluginPanels({ instance, + reloadFunc, model, id }: { instance?: any; + reloadFunc?: () => void; model?: ModelType | string; id?: string | number | null; }): PluginPanelSet { @@ -91,21 +80,23 @@ export function usePluginPanels({ // Cache the context data which is delivered to the plugins const inventreeContext = useInvenTreeContext(); - const contextData = useMemo(() => { - return { - model: model, - id: id, - instance: instance, - ...inventreeContext - }; - }, [model, id, instance, inventreeContext]); + const contextData: InvenTreePluginContext = + useMemo(() => { + return { + ...inventreeContext, + id: id, + model: model, + instance: instance, + reloadInstance: reloadFunc + }; + }, [model, id, instance, inventreeContext]); const pluginPanels: PluginPanelType[] = useMemo(() => { return ( pluginQuery?.data?.map((props: PluginUIFeature) => { const iconName: string = props?.icon || 'ti:plug:outline'; - const pluginContext: any = { + const ctx: InvenTreePluginContext = { ...contextData, context: props.context }; @@ -116,10 +107,7 @@ export function usePluginPanels({ label: props.title, icon: , content: ( - + ) }; }) ?? [] diff --git a/src/frontend/src/hooks/UsePluginUIFeature.tsx b/src/frontend/src/hooks/UsePluginUIFeature.tsx index f5fe533d02..0e8cfccebd 100644 --- a/src/frontend/src/hooks/UsePluginUIFeature.tsx +++ b/src/frontend/src/hooks/UsePluginUIFeature.tsx @@ -1,6 +1,8 @@ import { useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { apiUrl } from '@lib/functions/Api'; import { api } from '../App'; import { useInvenTreeContext } from '../components/plugins/PluginContext'; import { findExternalPluginFunction } from '../components/plugins/PluginSource'; @@ -9,8 +11,6 @@ import type { PluginUIFeatureAPIResponse, PluginUIFuncWithoutInvenTreeContextType } from '../components/plugins/PluginUIFeatureTypes'; -import { ApiEndpoints } from '../enums/ApiEndpoints'; -import { apiUrl } from '../states/ApiState'; import { useGlobalSettingsState } from '../states/SettingsState'; export function usePluginUIFeature({ diff --git a/src/frontend/src/hooks/UsePlugins.tsx b/src/frontend/src/hooks/UsePlugins.tsx index c5d8141933..137bd6f046 100644 --- a/src/frontend/src/hooks/UsePlugins.tsx +++ b/src/frontend/src/hooks/UsePlugins.tsx @@ -1,6 +1,6 @@ +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { useCallback } from 'react'; import type { PluginInterface } from '../components/plugins/PluginInterface'; -import { ApiEndpoints } from '../enums/ApiEndpoints'; import { useInstance } from './UseInstance'; export interface UsePluginResult { diff --git a/src/frontend/src/hooks/UseStatusCodes.tsx b/src/frontend/src/hooks/UseStatusCodes.tsx index 9316ea9adf..cad7b60178 100644 --- a/src/frontend/src/hooks/UseStatusCodes.tsx +++ b/src/frontend/src/hooks/UseStatusCodes.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; +import type { ModelType } from '@lib/enums/ModelType'; import { getStatusCodes } from '../components/render/StatusRenderer'; -import type { ModelType } from '../enums/ModelType'; import { useGlobalStatusState } from '../states/StatusState'; /** diff --git a/src/frontend/src/hooks/UseTable.tsx b/src/frontend/src/hooks/UseTable.tsx index 4099014040..adace60100 100644 --- a/src/frontend/src/hooks/UseTable.tsx +++ b/src/frontend/src/hooks/UseTable.tsx @@ -1,75 +1,10 @@ import { randomId, useLocalStorage } from '@mantine/hooks'; import { useCallback, useMemo, useState } from 'react'; -import { type SetURLSearchParams, useSearchParams } from 'react-router-dom'; +import { useSearchParams } from 'react-router-dom'; -import { type FilterSetState, useFilterSet } from './UseFilterSet'; - -/* - * 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; -}; +import type { FilterSetState } from '@lib/types/Filters'; +import type { TableState } from '@lib/types/Tables'; +import { useFilterSet } from './UseFilterSet'; /** * A custom hook for managing the state of an component. diff --git a/src/frontend/src/main.tsx b/src/frontend/src/main.tsx index 761575707f..a9a68a4c3b 100644 --- a/src/frontend/src/main.tsx +++ b/src/frontend/src/main.tsx @@ -7,13 +7,17 @@ import '@mantine/spotlight/styles.css'; import * as Sentry from '@sentry/react'; import 'mantine-contextmenu/styles.css'; import 'mantine-datatable/styles.css'; -import React from 'react'; -import ReactDOM from 'react-dom/client'; import 'react-grid-layout/css/styles.css'; import 'react-resizable/css/styles.css'; +import * as MantineCore from '@mantine/core'; +import * as MantineNotifications from '@mantine/notifications'; +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import * as ReactDOMClient from 'react-dom/client'; import './styles/overrides.css'; -import type { HostList } from './states/states'; +import { getBaseUrl } from '@lib/functions/Navigation'; +import type { HostList } from '@lib/types/Server'; import MainView from './views/MainView'; // define settings @@ -28,7 +32,12 @@ declare global { sentry_dsn?: string; environment?: string; }; + react: typeof React; React: typeof React; + ReactDOM: typeof ReactDOM; + ReactDOMClient: typeof ReactDOMClient; + MantineCore: typeof MantineCore; + MantineNotifications: typeof MantineNotifications; } } @@ -93,18 +102,21 @@ if (window.INVENTREE_SETTINGS.sentry_dsn) { }); } -export const getBaseUrl = (): string => - window.INVENTREE_SETTINGS?.base_url || 'web'; - -ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - - - -); +(window as any).React = React; +(window as any).ReactDOM = ReactDOM; +(window as any).ReactDOMClient = ReactDOMClient; +(window as any).MantineCore = MantineCore; +(window as any).MantineNotifications = MantineNotifications; // Redirect to base url if on / if (window.location.pathname === '/') { window.location.replace(`/${getBaseUrl()}`); } -window.React = React; +ReactDOMClient.createRoot( + document.getElementById('root') as HTMLElement +).render( + + + +); diff --git a/src/frontend/src/pages/ErrorPage.tsx b/src/frontend/src/pages/ErrorPage.tsx index e060f72c93..b7a0aed8ba 100644 --- a/src/frontend/src/pages/ErrorPage.tsx +++ b/src/frontend/src/pages/ErrorPage.tsx @@ -3,8 +3,8 @@ import { useDocumentTitle } from '@mantine/hooks'; import { useEffect, useState } from 'react'; import { useRouteError } from 'react-router-dom'; +import type { ErrorResponse } from '@lib/types/Auth'; import GenericErrorPage from '../components/errors/GenericErrorPage'; -import type { ErrorResponse } from '../states/states'; export default function ErrorPage() { const error = useRouteError() as ErrorResponse; diff --git a/src/frontend/src/pages/Index/Scan.tsx b/src/frontend/src/pages/Index/Scan.tsx index d6f3a1e266..51d77a055b 100644 --- a/src/frontend/src/pages/Index/Scan.tsx +++ b/src/frontend/src/pages/Index/Scan.tsx @@ -19,6 +19,10 @@ import { } from '@tabler/icons-react'; import { useCallback, useEffect, useMemo, useState } from 'react'; +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 { hideNotification, showNotification } from '@mantine/notifications'; import dayjs from 'dayjs'; import { api } from '../../App'; @@ -26,14 +30,10 @@ import { BarcodeInput } from '../../components/barcodes/BarcodeInput'; import type { BarcodeScanItem } from '../../components/barcodes/BarcodeScanItem'; import { StylishText } from '../../components/items/StylishText'; import PageTitle from '../../components/nav/PageTitle'; -import { ModelInformationDict } from '../../components/render/ModelType'; -import { ApiEndpoints } from '../../enums/ApiEndpoints'; -import type { ModelType } from '../../enums/ModelType'; import { notYetImplemented, showApiErrorMessage } from '../../functions/notifications'; -import { apiUrl } from '../../states/ApiState'; import BarcodeScanTable from '../../tables/general/BarcodeScanTable'; export default function Scan() { diff --git a/src/frontend/src/pages/Index/Settings/AccountSettings/AccountDetailPanel.tsx b/src/frontend/src/pages/Index/Settings/AccountSettings/AccountDetailPanel.tsx index 3a89b75784..4cfa95179e 100644 --- a/src/frontend/src/pages/Index/Settings/AccountSettings/AccountDetailPanel.tsx +++ b/src/frontend/src/pages/Index/Settings/AccountSettings/AccountDetailPanel.tsx @@ -4,13 +4,13 @@ import { Badge, Group, Stack, Table } from '@mantine/core'; import { IconEdit, IconKey, IconUser } from '@tabler/icons-react'; import { useMemo } from 'react'; +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import type { ApiFormFieldSet } from '@lib/types/Forms'; import { useNavigate } from 'react-router-dom'; import { ActionButton } from '../../../../components/buttons/ActionButton'; import { YesNoUndefinedButton } from '../../../../components/buttons/YesNoButton'; -import type { ApiFormFieldSet } from '../../../../components/forms/fields/ApiFormField'; import { ActionDropdown } from '../../../../components/items/ActionDropdown'; import { StylishText } from '../../../../components/items/StylishText'; -import { ApiEndpoints } from '../../../../enums/ApiEndpoints'; import { useEditApiFormModal } from '../../../../hooks/UseForm'; import { useUserState } from '../../../../states/UserState'; diff --git a/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx b/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx index ac6655aba7..fbaf3a6e76 100644 --- a/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx +++ b/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx @@ -1,3 +1,6 @@ +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { apiUrl } from '@lib/functions/Api'; +import { type AuthConfig, type AuthProvider, FlowEnum } from '@lib/types/Auth'; import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; import { @@ -30,14 +33,8 @@ import { useQuery } from '@tanstack/react-query'; import { useMemo, useState } from 'react'; import { api } from '../../../../App'; import { StylishText } from '../../../../components/items/StylishText'; -import { ApiEndpoints } from '../../../../enums/ApiEndpoints'; import { ProviderLogin, authApi } from '../../../../functions/auth'; -import { apiUrl, useServerApiState } from '../../../../states/ApiState'; -import { - type AuthConfig, - FlowEnum, - type Provider -} from '../../../../states/states'; +import { useServerApiState } from '../../../../states/ApiState'; import { ApiTokenTable } from '../../../../tables/settings/ApiTokenTable'; import { QrRegistrationForm } from './QrRegistrationForm'; import { useReauth } from './useConfirm'; @@ -241,7 +238,7 @@ function EmailSection() { ); } -function ProviderButton({ provider }: Readonly<{ provider: Provider }>) { +function ProviderButton({ provider }: Readonly<{ provider: AuthProvider }>) { return (